fraude-code 0.1.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 (127) hide show
  1. package/README.md +68 -0
  2. package/dist/index.js +179297 -0
  3. package/package.json +88 -0
  4. package/src/agent/agent.ts +475 -0
  5. package/src/agent/contextManager.ts +141 -0
  6. package/src/agent/index.ts +14 -0
  7. package/src/agent/pendingChanges.ts +270 -0
  8. package/src/agent/prompts/AskPrompt.txt +10 -0
  9. package/src/agent/prompts/FastPrompt.txt +40 -0
  10. package/src/agent/prompts/PlannerPrompt.txt +51 -0
  11. package/src/agent/prompts/ReviewerPrompt.txt +57 -0
  12. package/src/agent/prompts/WorkerPrompt.txt +33 -0
  13. package/src/agent/subagents/askAgent.ts +37 -0
  14. package/src/agent/subagents/extractionAgent.ts +123 -0
  15. package/src/agent/subagents/fastAgent.ts +45 -0
  16. package/src/agent/subagents/managerAgent.ts +36 -0
  17. package/src/agent/subagents/relationAgent.ts +76 -0
  18. package/src/agent/subagents/researchSubAgent.ts +79 -0
  19. package/src/agent/subagents/reviewerSubAgent.ts +42 -0
  20. package/src/agent/subagents/workerSubAgent.ts +42 -0
  21. package/src/agent/tools/bashTool.ts +94 -0
  22. package/src/agent/tools/descriptions/bash.txt +47 -0
  23. package/src/agent/tools/descriptions/edit.txt +7 -0
  24. package/src/agent/tools/descriptions/glob.txt +4 -0
  25. package/src/agent/tools/descriptions/grep.txt +8 -0
  26. package/src/agent/tools/descriptions/lsp.txt +20 -0
  27. package/src/agent/tools/descriptions/plan.txt +3 -0
  28. package/src/agent/tools/descriptions/read.txt +9 -0
  29. package/src/agent/tools/descriptions/todo.txt +12 -0
  30. package/src/agent/tools/descriptions/write.txt +8 -0
  31. package/src/agent/tools/editTool.ts +44 -0
  32. package/src/agent/tools/globTool.ts +59 -0
  33. package/src/agent/tools/grepTool.ts +343 -0
  34. package/src/agent/tools/lspTool.ts +429 -0
  35. package/src/agent/tools/planTool.ts +118 -0
  36. package/src/agent/tools/readTool.ts +78 -0
  37. package/src/agent/tools/rememberTool.ts +91 -0
  38. package/src/agent/tools/testRunnerTool.ts +77 -0
  39. package/src/agent/tools/testTool.ts +44 -0
  40. package/src/agent/tools/todoTool.ts +224 -0
  41. package/src/agent/tools/writeTool.ts +33 -0
  42. package/src/commands/COMMANDS.ts +38 -0
  43. package/src/commands/cerebras/auth.ts +27 -0
  44. package/src/commands/cerebras/index.ts +31 -0
  45. package/src/commands/forget.ts +29 -0
  46. package/src/commands/google/auth.ts +24 -0
  47. package/src/commands/google/index.ts +31 -0
  48. package/src/commands/groq/add_model.ts +60 -0
  49. package/src/commands/groq/auth.ts +24 -0
  50. package/src/commands/groq/index.ts +33 -0
  51. package/src/commands/index.ts +65 -0
  52. package/src/commands/knowledge.ts +92 -0
  53. package/src/commands/log.ts +32 -0
  54. package/src/commands/mistral/auth.ts +27 -0
  55. package/src/commands/mistral/index.ts +31 -0
  56. package/src/commands/model/index.ts +145 -0
  57. package/src/commands/models/index.ts +16 -0
  58. package/src/commands/ollama/index.ts +29 -0
  59. package/src/commands/openrouter/add_model.ts +64 -0
  60. package/src/commands/openrouter/auth.ts +24 -0
  61. package/src/commands/openrouter/index.ts +33 -0
  62. package/src/commands/remember.ts +48 -0
  63. package/src/commands/serve.ts +31 -0
  64. package/src/commands/session/index.ts +21 -0
  65. package/src/commands/usage.ts +15 -0
  66. package/src/commands/visualize.ts +773 -0
  67. package/src/components/App.tsx +55 -0
  68. package/src/components/IntroComponent.tsx +70 -0
  69. package/src/components/LoaderComponent.tsx +68 -0
  70. package/src/components/OutputRenderer.tsx +88 -0
  71. package/src/components/SettingsRenderer.tsx +23 -0
  72. package/src/components/input/CommandSuggestions.tsx +41 -0
  73. package/src/components/input/FileSuggestions.tsx +61 -0
  74. package/src/components/input/InputBox.tsx +371 -0
  75. package/src/components/output/CheckpointView.tsx +13 -0
  76. package/src/components/output/CommandView.tsx +13 -0
  77. package/src/components/output/CommentView.tsx +12 -0
  78. package/src/components/output/ConfirmationView.tsx +179 -0
  79. package/src/components/output/ContextUsage.tsx +62 -0
  80. package/src/components/output/DiffView.tsx +202 -0
  81. package/src/components/output/ErrorView.tsx +14 -0
  82. package/src/components/output/InteractiveServerView.tsx +69 -0
  83. package/src/components/output/KnowledgeView.tsx +220 -0
  84. package/src/components/output/MarkdownView.tsx +15 -0
  85. package/src/components/output/ModelSelectView.tsx +71 -0
  86. package/src/components/output/ReasoningView.tsx +21 -0
  87. package/src/components/output/ToolCallView.tsx +45 -0
  88. package/src/components/settings/ModelList.tsx +250 -0
  89. package/src/components/settings/TokenUsage.tsx +274 -0
  90. package/src/config/schema.ts +19 -0
  91. package/src/config/settings.ts +229 -0
  92. package/src/index.tsx +100 -0
  93. package/src/parsers/tree-sitter-python.wasm +0 -0
  94. package/src/providers/providers.ts +71 -0
  95. package/src/services/PluginLoader.ts +123 -0
  96. package/src/services/cerebras.ts +69 -0
  97. package/src/services/embeddingService.ts +229 -0
  98. package/src/services/google.ts +65 -0
  99. package/src/services/graphSerializer.ts +248 -0
  100. package/src/services/groq.ts +23 -0
  101. package/src/services/knowledgeOrchestrator.ts +286 -0
  102. package/src/services/mistral.ts +79 -0
  103. package/src/services/ollama.ts +109 -0
  104. package/src/services/openrouter.ts +23 -0
  105. package/src/services/symbolExtractor.ts +277 -0
  106. package/src/store/useFraudeStore.ts +123 -0
  107. package/src/store/useSettingsStore.ts +38 -0
  108. package/src/theme.ts +26 -0
  109. package/src/types/Agent.ts +147 -0
  110. package/src/types/CommandDefinition.ts +8 -0
  111. package/src/types/Model.ts +94 -0
  112. package/src/types/OutputItem.ts +24 -0
  113. package/src/types/PluginContext.ts +55 -0
  114. package/src/types/TokenUsage.ts +5 -0
  115. package/src/types/assets.d.ts +4 -0
  116. package/src/utils/agentCognition.ts +1152 -0
  117. package/src/utils/fileSuggestions.ts +111 -0
  118. package/src/utils/index.ts +17 -0
  119. package/src/utils/initFraude.ts +8 -0
  120. package/src/utils/logger.ts +24 -0
  121. package/src/utils/lspClient.ts +1415 -0
  122. package/src/utils/paths.ts +24 -0
  123. package/src/utils/queryHandler.ts +227 -0
  124. package/src/utils/router.ts +278 -0
  125. package/src/utils/streamHandler.ts +132 -0
  126. package/src/utils/treeSitterQueries.ts +125 -0
  127. package/tsconfig.json +33 -0
@@ -0,0 +1,371 @@
1
+ import { useState, useMemo, useCallback, useEffect } from "react";
2
+ import { useApp, Box, Text, useInput } from "ink";
3
+ import { TextInput } from "@inkjs/ui";
4
+ import QueryHandler from "@/utils/queryHandler";
5
+ import useSettingsStore from "@/store/useSettingsStore";
6
+ import useFraudeStore from "@/store/useFraudeStore";
7
+ import CommandCenter from "@/commands";
8
+ import { addHistory } from "@/config/settings";
9
+ import CommandSuggestions from "./CommandSuggestions";
10
+ import FileSuggestions from "./FileSuggestions";
11
+ import {
12
+ getFileSuggestions,
13
+ type FileSuggestion,
14
+ } from "@/utils/fileSuggestions";
15
+ import { shortenPath } from "@/utils";
16
+
17
+ import { getModelDisplayId } from "@/types/Model";
18
+ import { THEME } from "@/theme";
19
+
20
+ const InputBoxComponent = () => {
21
+ const { exit } = useApp();
22
+ const [inputKey, setInputKey] = useState(0);
23
+ const [selectedIndex, setSelectedIndex] = useState(0);
24
+ const [currentInput, setCurrentInput] = useState("");
25
+ const [historyIndex, setHistoryIndex] = useState(-1);
26
+ const [allFiles, setAllFiles] = useState<FileSuggestion[]>([]);
27
+ const [scrollOffset, setScrollOffset] = useState(0);
28
+ const [ghostIndex, setGhostIndex] = useState(0);
29
+
30
+ useEffect(() => {
31
+ getFileSuggestions(process.cwd()).then(setAllFiles);
32
+ }, []);
33
+
34
+ const history = useSettingsStore((state) => state.history);
35
+ const models = useSettingsStore((state) => state.models);
36
+ // Create display identifiers that include provider to differentiate same-name models
37
+ const modelNames = useMemo(
38
+ () => models.map((m) => getModelDisplayId(m)).sort(),
39
+ [models],
40
+ );
41
+ const MAX_VISIBLE_SUGGESTIONS = 5;
42
+
43
+ const suggestions = useMemo(() => {
44
+ const allCommands = CommandCenter.getAllCommands();
45
+ const suggestions = allCommands.map((s) => {
46
+ if (s.usage?.includes("<model-name>")) {
47
+ const options = [];
48
+ for (const modelName of modelNames) {
49
+ const renderedUsage = s.usage.replace("<model-name>", modelName);
50
+ options.push(renderedUsage);
51
+ }
52
+ return { ...s, renderedOptions: options };
53
+ }
54
+ return s;
55
+ });
56
+ return suggestions;
57
+ }, [modelNames]);
58
+
59
+ const fileTokenMatch = useMemo(
60
+ () => currentInput.match(/@([^ ]*)$/),
61
+ [currentInput],
62
+ );
63
+ const isFileMode = !!fileTokenMatch;
64
+ const fileQuery = fileTokenMatch ? fileTokenMatch[1] : "";
65
+ const filePrefix = fileTokenMatch
66
+ ? currentInput.slice(0, fileTokenMatch.index! + 1)
67
+ : "";
68
+
69
+ const fileDropdownSuggestions = useMemo(() => {
70
+ if (!isFileMode || !allFiles.length) return [];
71
+ return allFiles.filter((f) =>
72
+ f.path.toLowerCase().includes(fileQuery!.toLowerCase()),
73
+ );
74
+ }, [isFileMode, allFiles, fileQuery]);
75
+ const dropdownSuggestions = useMemo(() => {
76
+ if (!currentInput.startsWith("/")) return [];
77
+ const filteredTemplates = suggestions
78
+ .filter((s) => {
79
+ if (s.renderedOptions && s.renderedOptions.length > 0) {
80
+ const check = s.renderedOptions.find((option) => {
81
+ return option.startsWith(currentInput);
82
+ });
83
+ return check != undefined;
84
+ }
85
+ return s.usage?.startsWith(currentInput) ?? false;
86
+ })
87
+ .slice(0, MAX_VISIBLE_SUGGESTIONS);
88
+ return filteredTemplates;
89
+ }, [suggestions, currentInput]);
90
+
91
+ const dynamicSuggestions = useMemo(() => {
92
+ const allSuggestions = suggestions.flatMap((s) => {
93
+ if (s.renderedOptions && s.renderedOptions.length > 0) {
94
+ return s.renderedOptions;
95
+ }
96
+ return s.usage || "";
97
+ });
98
+
99
+ const dropdownSuggestion = dropdownSuggestions[selectedIndex];
100
+ if (dropdownSuggestion) {
101
+ // Put the dropdown suggestion first so it shows as ghost text
102
+ const renderedSuggestion =
103
+ dropdownSuggestion.renderedOptions &&
104
+ dropdownSuggestion.renderedOptions.length > 0
105
+ ? dropdownSuggestion.renderedOptions.find((option) =>
106
+ option.startsWith(currentInput),
107
+ )
108
+ : dropdownSuggestion.usage || "";
109
+ if (renderedSuggestion) {
110
+ return [
111
+ renderedSuggestion,
112
+ ...allSuggestions.filter((s) => s !== renderedSuggestion),
113
+ ];
114
+ }
115
+ }
116
+
117
+ // File suggestions
118
+ if (isFileMode && fileDropdownSuggestions.length > 0) {
119
+ const selectedFile = fileDropdownSuggestions[selectedIndex];
120
+ if (selectedFile) {
121
+ return [filePrefix + selectedFile.path + " "];
122
+ }
123
+ }
124
+
125
+ return allSuggestions;
126
+ }, [
127
+ suggestions,
128
+ dropdownSuggestions,
129
+ selectedIndex,
130
+ currentInput,
131
+ isFileMode,
132
+ fileDropdownSuggestions,
133
+ filePrefix,
134
+ ]);
135
+
136
+ const hiddenSuggestions = useMemo(() => {
137
+ if (currentInput.startsWith("/")) {
138
+ return new Set(
139
+ dynamicSuggestions
140
+ .filter((s) => s.startsWith(currentInput))
141
+ .slice(0, ghostIndex),
142
+ );
143
+ }
144
+ return new Set();
145
+ }, [dynamicSuggestions, currentInput, ghostIndex]);
146
+
147
+ // Calculate what ghost text the TextInput is ACTUALLY showing
148
+ // This mirrors TextInput's internal logic: first suggestion that starts with input
149
+ const actualGhostTextSuggestion = useMemo(() => {
150
+ if (currentInput.length === 0) return null;
151
+ const match = dynamicSuggestions.find((s) => s.startsWith(currentInput));
152
+ if (!match) return null;
153
+ const currentArg = currentInput.split(" ").length;
154
+ const dropdownSuggestion = dropdownSuggestions[selectedIndex];
155
+ if (
156
+ dropdownSuggestion?.usage &&
157
+ dropdownSuggestion.usage.split(" ")[currentArg] !== "<model-name>"
158
+ ) {
159
+ return dropdownSuggestion.usage.replace(/<[^>]+>.*$/, "");
160
+ }
161
+ return match.replace(/<[^>]+>.*$/, "");
162
+ }, [currentInput, dynamicSuggestions, selectedIndex, dropdownSuggestions]);
163
+
164
+ useInput((input, key) => {
165
+ if (key.tab) {
166
+ if (isFileMode && fileDropdownSuggestions.length > 0) {
167
+ const selectedFile = fileDropdownSuggestions[selectedIndex];
168
+ if (selectedFile) {
169
+ const suggestion = filePrefix + selectedFile.path + " ";
170
+ if (suggestion !== currentInput) {
171
+ setCurrentInput(suggestion);
172
+ setInputKey((k) => k + 1);
173
+ setHistoryIndex(-1);
174
+ return;
175
+ }
176
+ }
177
+ }
178
+
179
+ if (
180
+ actualGhostTextSuggestion &&
181
+ actualGhostTextSuggestion.toLowerCase() != currentInput.toLowerCase()
182
+ ) {
183
+ setCurrentInput(actualGhostTextSuggestion);
184
+ setInputKey((k) => k + 1);
185
+ setHistoryIndex(-1);
186
+ } else {
187
+ useFraudeStore.setState({
188
+ executionMode: ((useFraudeStore.getState().executionMode + 1) % 3) as
189
+ | 0
190
+ | 1
191
+ | 2,
192
+ });
193
+ }
194
+ return;
195
+ }
196
+
197
+ // If input starts with "/" and there are multiple suggestions, use arrow keys for command dropdown
198
+ if (currentInput.startsWith("/") && dropdownSuggestions.length > 1) {
199
+ if (key.upArrow && selectedIndex > 0) {
200
+ setSelectedIndex(selectedIndex - 1);
201
+ return;
202
+ }
203
+ if (key.downArrow && selectedIndex < dropdownSuggestions.length - 1) {
204
+ const newIndex = selectedIndex + 1;
205
+ setSelectedIndex(newIndex);
206
+ return;
207
+ }
208
+ }
209
+
210
+ if (currentInput.startsWith("/") && dropdownSuggestions.length === 1) {
211
+ const filtered = dynamicSuggestions.filter((s) =>
212
+ s.startsWith(currentInput),
213
+ );
214
+ const isModelArg =
215
+ dropdownSuggestions[0]?.usage.includes("<model-name>") ?? false;
216
+ if (isModelArg && filtered.length > 0) {
217
+ setGhostIndex((ghostIndex) => (ghostIndex + 1) % filtered.length);
218
+ return;
219
+ }
220
+ }
221
+
222
+ // Input starts with @ (or contains @ at end)
223
+ if (isFileMode && fileDropdownSuggestions.length > 1) {
224
+ const listLen = fileDropdownSuggestions.length;
225
+ if (key.upArrow) {
226
+ const newIndex = selectedIndex - 1;
227
+ if (newIndex >= 0) {
228
+ setSelectedIndex(newIndex);
229
+ if (newIndex < scrollOffset) {
230
+ setScrollOffset(newIndex);
231
+ } else if (newIndex >= scrollOffset + MAX_VISIBLE_SUGGESTIONS) {
232
+ setScrollOffset(newIndex - MAX_VISIBLE_SUGGESTIONS + 1);
233
+ }
234
+ return;
235
+ }
236
+ }
237
+ if (key.downArrow) {
238
+ const newIndex = selectedIndex + 1;
239
+ if (newIndex < listLen) {
240
+ setSelectedIndex(newIndex);
241
+ if (newIndex >= scrollOffset + MAX_VISIBLE_SUGGESTIONS) {
242
+ setScrollOffset(newIndex - MAX_VISIBLE_SUGGESTIONS + 1);
243
+ } else if (newIndex < scrollOffset) {
244
+ setScrollOffset(newIndex);
245
+ }
246
+ return;
247
+ }
248
+ }
249
+ }
250
+
251
+ // // Otherwise, use arrow keys for history navigation
252
+ // // Note: history[0] is the most recent item in useFraudeStore
253
+ if (key.upArrow && history.length > 0) {
254
+ const newIndex =
255
+ historyIndex < history.length - 1 ? historyIndex + 1 : historyIndex;
256
+ setHistoryIndex(newIndex);
257
+ const historyItem = history[newIndex];
258
+ if (historyItem) {
259
+ setCurrentInput(historyItem);
260
+ setInputKey((k) => k + 1);
261
+ }
262
+ return;
263
+ }
264
+ // Down arrow: go to newer history or clear input
265
+ if (key.downArrow) {
266
+ if (historyIndex > 0) {
267
+ const newIndex = historyIndex - 1;
268
+ setHistoryIndex(newIndex);
269
+ const historyItem = history[newIndex];
270
+ if (historyItem) {
271
+ setCurrentInput(historyItem);
272
+ setInputKey((k) => k + 1);
273
+ }
274
+ } else {
275
+ // Clear input when at the end of history or not in history
276
+ setHistoryIndex(-1);
277
+ setCurrentInput("");
278
+ setInputKey((k) => k + 1);
279
+ }
280
+ return;
281
+ }
282
+ });
283
+
284
+ const handleChange = useCallback((value: string) => {
285
+ setCurrentInput(value);
286
+ setSelectedIndex(0);
287
+ setScrollOffset(0);
288
+ setGhostIndex(0);
289
+ }, []);
290
+
291
+ const processSubmit = () => {
292
+ const v = currentInput;
293
+ if (v.trim().toLowerCase() === "exit") {
294
+ exit();
295
+ return;
296
+ }
297
+ if (v.trim() === "") return;
298
+ addHistory(v);
299
+ setCurrentInput("");
300
+ setInputKey((k) => k + 1); // Clear input by remounting TextInput
301
+ QueryHandler(v);
302
+ };
303
+
304
+ const getExecutionMode = (mode: 0 | 1 | 2) => {
305
+ switch (mode) {
306
+ case 0:
307
+ return "Fast";
308
+ case 1:
309
+ return "Planning";
310
+ case 2:
311
+ return "Ask";
312
+ }
313
+ };
314
+
315
+ const status = useFraudeStore((state) => state.status);
316
+ if (status === 3) return null;
317
+
318
+ return (
319
+ <Box flexDirection="column">
320
+ <Box paddingX={0}>
321
+ <Text bold color={THEME.primary}>
322
+ &gt;{" "}
323
+ </Text>
324
+ <Box flexGrow={1}>
325
+ <TextInput
326
+ key={inputKey}
327
+ placeholder="Type a command or ask a question..."
328
+ onChange={handleChange}
329
+ suggestions={dynamicSuggestions.filter(
330
+ (s) => !hiddenSuggestions.has(s),
331
+ )}
332
+ defaultValue={currentInput}
333
+ onSubmit={processSubmit}
334
+ />
335
+ </Box>
336
+ </Box>
337
+ <Box marginTop={1}>
338
+ {dropdownSuggestions.length > 0 ? (
339
+ <CommandSuggestions
340
+ selectedIndex={selectedIndex}
341
+ filteredTemplates={dropdownSuggestions}
342
+ />
343
+ ) : (
344
+ <Box width={70} justifyContent="space-between" paddingX={1}>
345
+ {fileDropdownSuggestions.length > 0 ? (
346
+ <FileSuggestions
347
+ selectedIndex={selectedIndex - scrollOffset}
348
+ suggestions={fileDropdownSuggestions.slice(
349
+ scrollOffset,
350
+ scrollOffset + MAX_VISIBLE_SUGGESTIONS,
351
+ )}
352
+ />
353
+ ) : (
354
+ <>
355
+ <Text color={THEME.dim}>{shortenPath(process.cwd())}</Text>
356
+ <Text color={THEME.primaryLight}>
357
+ <Text bold>
358
+ {getExecutionMode(useFraudeStore.getState().executionMode)}{" "}
359
+ [Tab]
360
+ </Text>
361
+ </Text>
362
+ </>
363
+ )}
364
+ </Box>
365
+ )}
366
+ </Box>
367
+ </Box>
368
+ );
369
+ };
370
+
371
+ export default InputBoxComponent;
@@ -0,0 +1,13 @@
1
+ import { Box, Text } from "ink";
2
+ import type { OutputItem } from "@/types/OutputItem";
3
+ import { THEME } from "@/theme";
4
+
5
+ const CheckpointView = ({ item }: { item: OutputItem }) => {
6
+ return (
7
+ <Box flexDirection="column">
8
+ {item.content && <Text color={THEME.primaryDim}>{item.content}</Text>}
9
+ </Box>
10
+ );
11
+ };
12
+
13
+ export default CheckpointView;
@@ -0,0 +1,13 @@
1
+ import { Box, Text } from "ink";
2
+ import { THEME } from "@/theme";
3
+
4
+ const CommandView = ({ command }: { command: string }) => {
5
+ return (
6
+ <Box marginY={0}>
7
+ <Text color={THEME.dim}>&gt; </Text>
8
+ <Text color={THEME.text}>{command}</Text>
9
+ </Box>
10
+ );
11
+ };
12
+
13
+ export default CommandView;
@@ -0,0 +1,12 @@
1
+ import { Box, Text } from "ink";
2
+ import { THEME } from "@/theme";
3
+
4
+ const CommentView = ({ comment }: { comment: string }) => {
5
+ return (
6
+ <Box marginY={0}>
7
+ <Text color={THEME.dim}># {comment}</Text>
8
+ </Box>
9
+ );
10
+ };
11
+
12
+ export default CommentView;
@@ -0,0 +1,179 @@
1
+ import { Box, Text } from "ink";
2
+ import { Select, TextInput } from "@inkjs/ui";
3
+ import React, { useState, useMemo } from "react";
4
+ import pendingChanges from "@/agent/pendingChanges";
5
+ import DiffView from "./DiffView";
6
+ import QueryHandler from "@/utils/queryHandler";
7
+ import { projectPath } from "@/utils";
8
+ import { type PendingChange } from "@/agent/pendingChanges";
9
+
10
+ import useFraudeStore from "@/store/useFraudeStore";
11
+ import { THEME } from "@/theme";
12
+
13
+ export default function ConfirmationView() {
14
+ const [status, setStatus] = useState<
15
+ "pending" | "applied" | "rejected" | "viewing_file" | "revising"
16
+ >("pending");
17
+ const [selectedFile, setSelectedFile] = useState<string | null>(null);
18
+
19
+ // Get grouped changes directly from manager
20
+ const groupedChanges = useMemo(
21
+ () => pendingChanges.getAllChangesGrouped(),
22
+ [],
23
+ );
24
+ const filePaths = Object.keys(groupedChanges);
25
+
26
+ const handleMainSelect = (value: string) => {
27
+ if (value === "apply_all") {
28
+ pendingChanges.applyAll().then(() => {
29
+ setStatus("applied");
30
+ useFraudeStore.setState({ status: 0, statusText: "" });
31
+ });
32
+ } else if (value === "reject_all") {
33
+ pendingChanges.rejectAll();
34
+ setStatus("rejected");
35
+ useFraudeStore.setState({ status: 0, statusText: "" });
36
+ } else if (value.startsWith("view_")) {
37
+ const path = value.replace("view_", "");
38
+ setSelectedFile(path);
39
+ setStatus("viewing_file");
40
+ }
41
+ };
42
+
43
+ const handleFileAction = (value: string) => {
44
+ if (value === "back") {
45
+ setSelectedFile(null);
46
+ setStatus("pending");
47
+ }
48
+ };
49
+
50
+ if (status === "applied") {
51
+ return (
52
+ <Box borderStyle="single" borderColor={THEME.success} paddingX={1}>
53
+ <Text color={THEME.success}>✓ Changes applied</Text>
54
+ </Box>
55
+ );
56
+ }
57
+
58
+ if (status === "rejected") {
59
+ return (
60
+ <Box borderStyle="single" borderColor={THEME.error} paddingX={1}>
61
+ <Text color={THEME.error}>✗ Changes rejected</Text>
62
+ </Box>
63
+ );
64
+ }
65
+
66
+ // View specific file details
67
+ if (status === "viewing_file" && selectedFile) {
68
+ const changes = groupedChanges[selectedFile];
69
+
70
+ return (
71
+ <Box
72
+ flexDirection="column"
73
+ borderStyle="single"
74
+ borderColor={THEME.border}
75
+ paddingX={1}
76
+ >
77
+ <Text bold color={THEME.primary}>
78
+ {projectPath(selectedFile)}
79
+ </Text>
80
+ <Box flexDirection="column" marginY={0}>
81
+ {changes?.map((change) => (
82
+ <Box key={change.id} flexDirection="column">
83
+ <Text color={THEME.dim}>Type: {change.type}</Text>
84
+ <DiffView patches={[change.diff]} />
85
+ </Box>
86
+ ))}
87
+ </Box>
88
+ <Box marginTop={1}>
89
+ <Select
90
+ options={[{ label: "← Back", value: "back" }]}
91
+ onChange={handleFileAction}
92
+ />
93
+ </Box>
94
+ </Box>
95
+ );
96
+ }
97
+
98
+ // Handle revision feedback
99
+ if (status === "revising") {
100
+ return (
101
+ <Box
102
+ flexDirection="column"
103
+ borderStyle="single"
104
+ borderColor={THEME.primary}
105
+ paddingX={1}
106
+ >
107
+ <Text bold color={THEME.primary}>
108
+ Request Revision
109
+ </Text>
110
+ <Text dimColor>Enter feedback to guide the agent:</Text>
111
+ <Box marginTop={1}>
112
+ <TextInput
113
+ onSubmit={(value) => {
114
+ if (!value.trim()) return;
115
+ useFraudeStore.setState({
116
+ status: 1,
117
+ statusText: "Revising...",
118
+ });
119
+ pendingChanges.clear();
120
+ QueryHandler(`Revising changes: ${value}`);
121
+ }}
122
+ placeholder="What should be changed?"
123
+ />
124
+ </Box>
125
+ </Box>
126
+ );
127
+ }
128
+
129
+ // Main Summary View
130
+ const options = [
131
+ { label: "✓ Apply All Changes", value: "apply_all" },
132
+ { label: "✗ Reject All Changes", value: "reject_all" },
133
+ { label: "Request Revision", value: "revise_all" },
134
+ // Add options to inspect specific files
135
+ ...filePaths.map((path) => ({
136
+ label: `Review ${projectPath(path)}`,
137
+ value: `view_${path}`,
138
+ })),
139
+ ];
140
+
141
+ return (
142
+ <Box flexDirection="column">
143
+ <Text bold color={THEME.primary}>
144
+ Pending Changes
145
+ </Text>
146
+ <Box flexDirection="column" marginY={0}>
147
+ {filePaths.map((path) => {
148
+ const changes = groupedChanges[path];
149
+ const type = changes?.[0]?.type || "unknown";
150
+ const patches = changes?.map((c) => c.diff) || [];
151
+
152
+ return (
153
+ <Box key={path} flexDirection="column" marginTop={1}>
154
+ <Text>
155
+ {projectPath(path)} <Text color={THEME.dim}>({type})</Text>
156
+ </Text>
157
+ <Box paddingLeft={2}>
158
+ <DiffView patches={patches} />
159
+ </Box>
160
+ </Box>
161
+ );
162
+ })}
163
+ </Box>
164
+ <Box marginTop={1} flexDirection="column">
165
+ <Text color={THEME.dim}>Select action:</Text>
166
+ <Select
167
+ options={options}
168
+ onChange={(value) => {
169
+ if (value === "revise_all") {
170
+ setStatus("revising");
171
+ } else {
172
+ handleMainSelect(value);
173
+ }
174
+ }}
175
+ />
176
+ </Box>
177
+ </Box>
178
+ );
179
+ }
@@ -0,0 +1,62 @@
1
+ import { Box, Text } from "ink";
2
+ import { useMemo } from "react";
3
+ import useFraudeStore from "@/store/useFraudeStore";
4
+ import useSettingsStore from "@/store/useSettingsStore";
5
+ import { THEME } from "@/theme";
6
+
7
+ const ContextUsage = () => {
8
+ const { contextManager } = useFraudeStore();
9
+ const { primaryModel, models } = useSettingsStore();
10
+
11
+ const { usedTokens, maxTokens, percentage, progressBar } = useMemo(() => {
12
+ // Find the current model from the models list
13
+ const currentModel = models.find((m) => m.name === primaryModel);
14
+ const contextLength = currentModel?.details?.context_length || 128000; // Default fallback
15
+
16
+ const used = contextManager.estimateContextTokens();
17
+ const pct = Math.min((used / contextLength) * 100, 100);
18
+
19
+ // Create a simple visual progress bar
20
+ const barWidth = 20;
21
+ const filledWidth = Math.round((pct / 100) * barWidth);
22
+ const emptyWidth = barWidth - filledWidth;
23
+ const bar = "█".repeat(filledWidth) + "░".repeat(emptyWidth);
24
+
25
+ return {
26
+ usedTokens: used,
27
+ maxTokens: contextLength,
28
+ percentage: pct,
29
+ progressBar: bar,
30
+ };
31
+ }, [contextManager, primaryModel, models]);
32
+
33
+ // Color based on usage percentage
34
+ const getColor = () => {
35
+ if (percentage >= 90) return THEME.error;
36
+ if (percentage >= 70) return THEME.primaryDim;
37
+ if (percentage >= 50) return THEME.warning;
38
+ return THEME.info;
39
+ };
40
+
41
+ const formatTokens = (tokens: number) => {
42
+ if (tokens >= 1000) {
43
+ return `${(tokens / 1000).toFixed(1)}k`;
44
+ }
45
+ return tokens.toString();
46
+ };
47
+
48
+ return (
49
+ <Box flexDirection="row" gap={1}>
50
+ <Text color={THEME.dim}>[</Text>
51
+ <Text color={getColor()}>{progressBar}</Text>
52
+ <Text color={THEME.dim}>]</Text>
53
+ <Box paddingLeft={1}>
54
+ <Text color={THEME.text}>{formatTokens(usedTokens)}</Text>
55
+ <Text color={THEME.dim}>/{formatTokens(maxTokens)}</Text>
56
+ <Text color={getColor()}> {percentage.toFixed(0)}%</Text>
57
+ </Box>
58
+ </Box>
59
+ );
60
+ };
61
+
62
+ export default ContextUsage;