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,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,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 };