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,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,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
-
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
+
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,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,71 +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 }: Props) {
44
- const label = formatToolName(tool.name, tool.args);
45
- const result = tool.result ? 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 */}
57
- {result && (
58
- <Box paddingLeft={2} flexDirection="column">
59
- {result.text.split('\n').map((line, i) => (
60
- <Text key={i} color={tool.isError ? '#EF4444' : '#64748B'} wrap="truncate">
61
- {line}
62
- </Text>
63
- ))}
64
- {result.truncated && <Text dimColor> ...</Text>}
65
- </Box>
66
- )}
67
- </Box>
68
- );
69
- }
70
-
71
- 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 };