thatgfsj-code 2.2.9 → 3.0.1

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 (190) 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 +76 -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/ModelSelector.d.ts +3 -1
  86. package/dist/tui/components/ModelSelector.d.ts.map +1 -1
  87. package/dist/tui/components/ModelSelector.js +24 -7
  88. package/dist/tui/components/ModelSelector.js.map +1 -1
  89. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  90. package/dist/tui/components/ToolCall.js +1 -6
  91. package/dist/tui/components/ToolCall.js.map +1 -1
  92. package/dist/tui/components/UserInput.d.ts.map +1 -1
  93. package/dist/tui/components/UserInput.js +20 -0
  94. package/dist/tui/components/UserInput.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 +82 -46
  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 +57 -0
  101. package/dist/tui/hooks/useCommands.js.map +1 -1
  102. package/dist/tui/welcome.d.ts.map +1 -1
  103. package/dist/tui/welcome.js +2 -3
  104. package/dist/tui/welcome.js.map +1 -1
  105. package/dist/types.d.ts +79 -6
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +7 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/stableStringify.d.ts +21 -0
  110. package/dist/utils/stableStringify.d.ts.map +1 -0
  111. package/dist/utils/stableStringify.js +53 -0
  112. package/dist/utils/stableStringify.js.map +1 -0
  113. package/dist/utils/thinking.d.ts.map +1 -1
  114. package/dist/utils/thinking.js +0 -3
  115. package/dist/utils/thinking.js.map +1 -1
  116. package/package.json +7 -6
  117. package/src/app/index.ts +189 -124
  118. package/src/cache/fingerprint.ts +101 -0
  119. package/src/cache/index.ts +15 -0
  120. package/src/cache/smartModel.ts +52 -0
  121. package/src/cache/stats.ts +199 -0
  122. package/src/cache/volatile.ts +47 -0
  123. package/src/cmd/index.tsx +288 -292
  124. package/src/config/index.ts +156 -148
  125. package/src/config/providers.ts +234 -234
  126. package/src/config/types.ts +67 -53
  127. package/src/hooks/index.ts +111 -111
  128. package/src/llm/anthropic.ts +388 -243
  129. package/src/llm/gemini.ts +169 -169
  130. package/src/llm/index.ts +265 -200
  131. package/src/llm/openai.ts +243 -196
  132. package/src/llm/provider.ts +66 -34
  133. package/src/prompts/index.ts +260 -220
  134. package/src/session/compactor.ts +103 -103
  135. package/src/session/index.ts +181 -168
  136. package/src/session/message.ts +42 -42
  137. package/src/skills/brainstorming.ts +43 -43
  138. package/src/skills/code-review.ts +45 -45
  139. package/src/skills/executing-plans.ts +27 -27
  140. package/src/skills/frontend-design.ts +35 -35
  141. package/src/skills/git-workflow.ts +36 -36
  142. package/src/skills/improve-architecture.ts +38 -38
  143. package/src/skills/index.ts +136 -136
  144. package/src/skills/neuroweave.ts +47 -47
  145. package/src/skills/playwright.ts +72 -72
  146. package/src/skills/prototype.ts +30 -30
  147. package/src/skills/subagent.ts +28 -28
  148. package/src/skills/supabase.ts +44 -44
  149. package/src/skills/systematic-debugging.ts +44 -44
  150. package/src/skills/tdd.ts +39 -39
  151. package/src/skills/triage.ts +35 -35
  152. package/src/skills/verification.ts +31 -31
  153. package/src/skills/writing-plans.ts +42 -42
  154. package/src/tools/nwt.ts +598 -598
  155. package/src/tools/types.ts +122 -122
  156. package/src/tui/app.tsx +200 -186
  157. package/src/tui/components/ChatList.tsx +41 -41
  158. package/src/tui/components/ChatMessage.tsx +54 -54
  159. package/src/tui/components/Header.tsx +56 -29
  160. package/src/tui/components/InitWizard.tsx +217 -132
  161. package/src/tui/components/Markdown.tsx +35 -35
  162. package/src/tui/components/ModelSelector.tsx +107 -87
  163. package/src/tui/components/StatusBar.tsx +30 -30
  164. package/src/tui/components/Thinking.tsx +17 -17
  165. package/src/tui/components/ToolCall.tsx +102 -107
  166. package/src/tui/components/UserInput.tsx +151 -131
  167. package/src/tui/hooks/useChat.ts +287 -238
  168. package/src/tui/hooks/useCommands.ts +234 -176
  169. package/src/tui/index.ts +6 -6
  170. package/src/tui/welcome.ts +177 -178
  171. package/src/types.ts +104 -42
  172. package/src/utils/diff.ts +71 -71
  173. package/src/utils/project.ts +99 -99
  174. package/src/utils/stableStringify.ts +47 -0
  175. package/src/utils/thinking.ts +118 -121
  176. package/tests/cache/fingerprint.test.ts +75 -0
  177. package/tests/cache/providerCatalog.test.ts +31 -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
  185. package/.nwt/meta.json +0 -5
  186. package/dist/version.d.ts +0 -16
  187. package/dist/version.d.ts.map +0 -1
  188. package/dist/version.js +0 -16
  189. package/dist/version.js.map +0 -1
  190. package/src/version.ts +0 -16
@@ -1,35 +1,35 @@
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
+ /** @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,87 +1,107 @@
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
-
9
- interface Props {
10
- currentModel: string;
11
- onSelect: (model: string) => void;
12
- onAddNew: () => void;
13
- }
14
-
15
- interface SavedModel {
16
- label: string;
17
- value: string;
18
- }
19
-
20
- function loadSavedModels(): SavedModel[] {
21
- const configPath = join(homedir(), '.thatgfsj', 'config.json');
22
- const models: SavedModel[] = [];
23
- const seen = new Set<string>();
24
-
25
- // Load from history if exists
26
- const historyPath = join(homedir(), '.thatgfsj', 'models.json');
27
- if (existsSync(historyPath)) {
28
- try {
29
- const history = JSON.parse(readFileSync(historyPath, 'utf-8'));
30
- for (const m of history) {
31
- if (!seen.has(m)) {
32
- seen.add(m);
33
- models.push({ label: m, value: m });
34
- }
35
- }
36
- } catch {}
37
- }
38
-
39
- // Always include current model
40
- if (existsSync(configPath)) {
41
- try {
42
- const config = JSON.parse(readFileSync(configPath, 'utf-8'));
43
- if (config.model && !seen.has(config.model)) {
44
- seen.add(config.model);
45
- models.push({ label: `${config.model} (当前)`, value: config.model });
46
- }
47
- } catch {}
48
- }
49
-
50
- // Add some common defaults if list is empty
51
- if (models.length === 0) {
52
- const defaults = ['deepseek-chat', 'gpt-4o', 'mimo-v2.5-pro'];
53
- for (const m of defaults) {
54
- models.push({ label: m, value: m });
55
- }
56
- }
57
-
58
- return models;
59
- }
60
-
61
- export function ModelSelector({ currentModel, onSelect, onAddNew }: Props) {
62
- const [items, setItems] = useState<SavedModel[]>([]);
63
-
64
- useEffect(() => {
65
- const saved = loadSavedModels();
66
- // Add "add new" option at the end
67
- saved.push({ label: '+ 添加新模型', value: '__add_new__' });
68
- setItems(saved);
69
- }, []);
70
-
71
- return (
72
- <Box flexDirection="column" paddingLeft={1}>
73
- <Text color="#06B6D4" bold>当前模型: {currentModel}</Text>
74
- <Text dimColor>选择模型 (↑↓ 回车):</Text>
75
- <SelectInput
76
- items={items}
77
- onSelect={(item) => {
78
- if (item.value === '__add_new__') {
79
- onAddNew();
80
- } else {
81
- onSelect(item.value);
82
- }
83
- }}
84
- />
85
- </Box>
86
- );
87
- }
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 +1,30 @@
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
+ /** @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 +1,17 @@
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
- }
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
+ }
@@ -1,107 +1,102 @@
1
- /** @jsxImportSource react */
2
- import React from 'react';
3
- import { Box, Text } from 'ink';
4
-
5
- interface ToolCallData {
6
- name: string;
7
- args: string;
8
- result?: string;
9
- isError?: boolean;
10
- }
11
-
12
- interface Props {
13
- tool: ToolCallData;
14
- width?: number;
15
- }
16
-
17
- function formatToolName(name: string, args: string): string {
18
- try {
19
- const obj = JSON.parse(args);
20
- switch (name) {
21
- case 'file': {
22
- const action = obj.action || '';
23
- const path = (obj.path || '').replace(/.*[/\\]/, '');
24
- return `${action} ${path || obj.path || ''}`;
25
- }
26
- case 'shell': return obj.command?.slice(0, 60) || '';
27
- case 'git': return obj.command || '';
28
- case 'search': return obj.query || obj.pattern || '';
29
- case 'nwt': return obj.action || '';
30
- default: return Object.keys(obj).slice(0, 2).join(', ');
31
- }
32
- } catch {
33
- return '';
34
- }
35
- }
36
-
37
- function truncateOutput(output: string, maxLines = 8): { text: string; truncated: boolean } {
38
- const lines = output.split('\n');
39
- if (lines.length <= maxLines) return { text: output, truncated: false };
40
- return { text: lines.slice(0, maxLines).join('\n'), truncated: true };
41
- }
42
-
43
- export function ToolCall({ tool, width }: Props) {
44
- const label = formatToolName(tool.name, tool.args);
45
- // v2.2.7 edge: treat null same as undefined (both mean "still
46
- // running"). Previous check `result !== undefined` would render
47
- // null as an empty string output, causing confusion.
48
- const result = (tool.result !== undefined && tool.result !== null)
49
- ? truncateOutput(tool.result)
50
- : null;
51
-
52
- return (
53
- <Box flexDirection="column" marginBottom={0} paddingLeft={1}>
54
- {/* Tool header */}
55
- <Box>
56
- <Text color="#06B6D4" bold>⚙ </Text>
57
- <Text color="#06B6D4">{tool.name}</Text>
58
- {label && <Text color="#64748B"> {label}</Text>}
59
- </Box>
60
-
61
- {/* Result — v2.2.6 fix:
62
- - Use `!== undefined` (not truthy) so empty string results
63
- still render (truthy check dropped them, making it look
64
- like the tool result was missing).
65
- - Switch wrap from "truncate" to "wrap" so long lines don't
66
- silently disappear off-screen.
67
- - Always render the Box even when result is empty (just
68
- with a "(no output)" marker), so the visual frame stays
69
- consistent.
70
- - If truncated, show explicit "(+N more lines)" indicator
71
- so users know the result was clipped. */}
72
- {result !== null && (
73
- <Box paddingLeft={2} flexDirection="column">
74
- {result.text.length === 0 ? (
75
- <Text color="#94A3B8" dimColor>(no output)</Text>
76
- ) : (
77
- <>
78
- {result.text.split('\n').map((line, i) => (
79
- <Text
80
- key={i}
81
- color={tool.isError ? '#EF4444' : '#64748B'}
82
- wrap="wrap"
83
- >
84
- {line || ' '}
85
- </Text>
86
- ))}
87
- {result.truncated && (
88
- <Text color="#94A3B8" dimColor>
89
- {' '}(+{tool.result!.split('\n').length - 8} more lines)
90
- </Text>
91
- )}
92
- </>
93
- )}
94
- </Box>
95
- )}
96
- {/* v2.2.6: if result is undefined (still running), show a
97
- pending indicator so the user knows the tool is in flight. */}
98
- {result === null && tool.result === undefined && (
99
- <Box paddingLeft={2}>
100
- <Text color="#94A3B8" dimColor> ⏳ running...</Text>
101
- </Box>
102
- )}
103
- </Box>
104
- );
105
- }
106
-
107
- export type { ToolCallData };
1
+ /** @jsxImportSource react */
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+
5
+ interface ToolCallData {
6
+ name: string;
7
+ args: string;
8
+ result?: string;
9
+ isError?: boolean;
10
+ }
11
+
12
+ interface Props {
13
+ tool: ToolCallData;
14
+ width?: number;
15
+ }
16
+
17
+ function formatToolName(name: string, args: string): string {
18
+ try {
19
+ const obj = JSON.parse(args);
20
+ switch (name) {
21
+ case 'file': {
22
+ const action = obj.action || '';
23
+ const path = (obj.path || '').replace(/.*[/\\]/, '');
24
+ return `${action} ${path || obj.path || ''}`;
25
+ }
26
+ case 'shell': return obj.command?.slice(0, 60) || '';
27
+ case 'git': return obj.command || '';
28
+ case 'search': return obj.query || obj.pattern || '';
29
+ case 'nwt': return obj.action || '';
30
+ default: return Object.keys(obj).slice(0, 2).join(', ');
31
+ }
32
+ } catch {
33
+ return '';
34
+ }
35
+ }
36
+
37
+ function truncateOutput(output: string, maxLines = 8): { text: string; truncated: boolean } {
38
+ const lines = output.split('\n');
39
+ if (lines.length <= maxLines) return { text: output, truncated: false };
40
+ return { text: lines.slice(0, maxLines).join('\n'), truncated: true };
41
+ }
42
+
43
+ export function ToolCall({ tool, width }: Props) {
44
+ const label = formatToolName(tool.name, tool.args);
45
+ const result = tool.result !== undefined ? truncateOutput(tool.result) : null;
46
+
47
+ return (
48
+ <Box flexDirection="column" marginBottom={0} paddingLeft={1}>
49
+ {/* Tool header */}
50
+ <Box>
51
+ <Text color="#06B6D4" bold>⚙ </Text>
52
+ <Text color="#06B6D4">{tool.name}</Text>
53
+ {label && <Text color="#64748B"> {label}</Text>}
54
+ </Box>
55
+
56
+ {/* Result v2.2.6 fix:
57
+ - Use `!== undefined` (not truthy) so empty string results
58
+ still render (truthy check dropped them, making it look
59
+ like the tool result was missing).
60
+ - Switch wrap from "truncate" to "wrap" so long lines don't
61
+ silently disappear off-screen.
62
+ - Always render the Box even when result is empty (just
63
+ with a "(no output)" marker), so the visual frame stays
64
+ consistent.
65
+ - If truncated, show explicit "(+N more lines)" indicator
66
+ so users know the result was clipped. */}
67
+ {result !== null && (
68
+ <Box paddingLeft={2} flexDirection="column">
69
+ {result.text.length === 0 ? (
70
+ <Text color="#94A3B8" dimColor>(no output)</Text>
71
+ ) : (
72
+ <>
73
+ {result.text.split('\n').map((line, i) => (
74
+ <Text
75
+ key={i}
76
+ color={tool.isError ? '#EF4444' : '#64748B'}
77
+ wrap="wrap"
78
+ >
79
+ {line || ' '}
80
+ </Text>
81
+ ))}
82
+ {result.truncated && (
83
+ <Text color="#94A3B8" dimColor>
84
+ {' '}(+{tool.result!.split('\n').length - 8} more lines)
85
+ </Text>
86
+ )}
87
+ </>
88
+ )}
89
+ </Box>
90
+ )}
91
+ {/* v2.2.6: if result is undefined (still running), show a
92
+ pending indicator so the user knows the tool is in flight. */}
93
+ {result === null && tool.result === undefined && (
94
+ <Box paddingLeft={2}>
95
+ <Text color="#94A3B8" dimColor> ⏳ running...</Text>
96
+ </Box>
97
+ )}
98
+ </Box>
99
+ );
100
+ }
101
+
102
+ export type { ToolCallData };