FrederickSundeep commited on
Commit
7f234e3
·
1 Parent(s): e3e0a75

commit initial 12-12-2025 0002

Browse files
Files changed (2) hide show
  1. src/App.js +262 -216
  2. src/Terminal.js +60 -91
src/App.js CHANGED
@@ -1,8 +1,8 @@
1
  // src/App.js
2
  import { useState, useEffect, useRef } from "react";
3
  import Editor from "@monaco-editor/react";
 
4
  import { askAgent } from "./agent/assistant";
5
- import { runCode } from "./agent/runner";
6
  import {
7
  loadTree,
8
  saveTree,
@@ -18,9 +18,10 @@ import { downloadProjectZip } from "./zipExport";
18
  import { parseProblems } from "./problemParser";
19
  import "./App.css";
20
  import "xterm/css/xterm.css";
21
- import XTerm from "./Terminal"; // your existing wrapper
22
 
23
- // =================== SUPPORTED LANGUAGES ===================
 
 
24
  const LANGUAGE_OPTIONS = [
25
  { id: "python", ext: ".py", icon: "🐍", monaco: "python" },
26
  { id: "javascript", ext: ".js", icon: "🟨", monaco: "javascript" },
@@ -33,92 +34,22 @@ const LANGUAGE_OPTIONS = [
33
  { id: "json", ext: ".json", icon: "🧾", monaco: "json" },
34
  ];
35
 
36
- const RUNNABLE_LANGS = ["python", "javascript", "java"];
37
-
38
- // =================== Heuristics ===================
39
- // patterns that indicate program is waiting for input
40
- function outputLooksForInput(output) {
41
- if (!output) return false;
42
- const o = output.toString();
43
- const patterns = [
44
- /enter.*:/i,
45
- /input.*:/i,
46
- /please enter/i,
47
- /scanner/i,
48
- /press enter/i,
49
- /: $/,
50
- /:\n$/,
51
- /> $/,
52
- /awaiting input/i,
53
- /provide input/i,
54
- /stdin/i,
55
- /enter a value/i,
56
- ];
57
- return patterns.some((p) => p.test(o));
58
- }
59
 
60
- // code-level heuristics to detect input calls
61
- function codeNeedsInput(code, langId) {
62
- if (!code) return false;
63
- try {
64
- const c = code.toString();
65
- if (langId === "python") {
66
- if (/\binput\s*\(/i.test(c)) return true;
67
- if (/\bsys\.stdin\.(read|readline|readlines)\s*\(/i.test(c)) return true;
68
- if (/\braw_input\s*\(/i.test(c)) return true;
69
- }
70
- if (langId === "java") {
71
- if (/\bScanner\s*\(/i.test(c)) return true;
72
- if (/\bBufferedReader\b.*readLine/i.test(c)) return true;
73
- if (/\bSystem\.console\(\)/i.test(c)) return true;
74
- if (/\bnext(Int|Line|Double|)\b/i.test(c)) return true;
75
- }
76
- if (langId === "javascript") {
77
- if (/process\.stdin|readline|readlineSync|prompt\(|require\(['"]readline['"]\)/i.test(c)) return true;
78
- }
79
- if (langId === "cpp" || langId === "c") {
80
- if (/\bscanf\s*\(/i.test(c)) return true;
81
- if (/\bstd::cin\b|cin\s*>>/i.test(c)) return true;
82
- if (/\bgets?\s*\(/i.test(c)) return true;
83
- }
84
- if (/\binput\b|\bscanf\b|\bscanf_s\b|\bcin\b|\bScanner\b|readLine|readline/i.test(c)) return true;
85
- return false;
86
- } catch {
87
- return false;
88
- }
89
- }
90
-
91
- // Helper: focus xterm's hidden textarea (works with xterm.js default markup)
92
- function focusXtermHelper() {
93
- setTimeout(() => {
94
- const ta = document.querySelector("#terminal-container .xterm-helper-textarea");
95
- if (ta) {
96
- try {
97
- ta.focus();
98
- const len = ta.value?.length ?? 0;
99
- ta.setSelectionRange(len, len);
100
- } catch {}
101
- } else {
102
- const cont = document.getElementById("terminal-container");
103
- if (cont) cont.focus();
104
- }
105
- }, 120);
106
- }
107
-
108
- // =================== APP ===================
109
  function App() {
110
- // ----- file tree + selection -----
111
  const [tree, setTree] = useState(loadTree());
112
  const [activePath, setActivePath] = useState("main.py");
113
 
114
- // ----- terminal / interactive state -----
115
- const [accumStdin, setAccumStdin] = useState(""); // accumulated input for interactive runs
 
 
116
  const [awaitingInput, setAwaitingInput] = useState(false);
117
- const [terminalLines, setTerminalLines] = useState([]); // visible lines in terminal area
118
- const [output, setOutput] = useState(""); // "write" prop for XTerm (Terminal component picks this up)
119
  const [interactivePromptShown, setInteractivePromptShown] = useState(false);
 
120
 
121
- // ----- AI + editor state -----
122
  const [prompt, setPrompt] = useState("");
123
  const [explanation, setExplanation] = useState("");
124
  const [problems, setProblems] = useState([]);
@@ -127,24 +58,21 @@ function App() {
127
  const [searchQuery, setSearchQuery] = useState("");
128
  const [aiSuggestions, setAiSuggestions] = useState([]);
129
  const [contextMenu, setContextMenu] = useState(null);
130
- const [isRunning, setIsRunning] = useState(false);
131
  const [isFixing, setIsFixing] = useState(false);
132
  const [isExplaining, setIsExplaining] = useState(false);
133
 
134
- // refs & helpers
135
  const editorRef = useRef(null);
136
  const fileInputRef = useRef(null);
 
137
 
138
- useEffect(() => {
139
- saveTree(tree);
140
- }, [tree]);
141
 
142
  const currentNode = getNodeByPath(tree, activePath);
143
  const langMeta =
144
  LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) ||
145
  LANGUAGE_OPTIONS[0];
146
 
147
- // ---------- File / Folder actions ----------
148
  const collectFolderPaths = (node, acc = []) => {
149
  if (!node) return acc;
150
  if (node.type === "folder") acc.push(node.path || "");
@@ -242,141 +170,225 @@ function App() {
242
  e.target.value = "";
243
  };
244
 
245
- // ---------- Terminal helpers ----------
246
  const appendTerminal = (text) => {
247
- // push to visible lines and set `output` (which Terminal writes)
 
248
  setTerminalLines((prev) => {
249
- const next = [...prev, text];
250
- // also keep the XTerm single-output prop to trigger Terminal.writeln
251
- setOutput(text);
252
  return next;
253
  });
254
  };
255
 
256
- const clearTerminal = () => {
257
- // ANSI sequence to clear screen + move cursor home (xterm will honor)
258
  setTerminalLines([]);
259
- setOutput("\x1b[2J\x1b[H");
260
- setAccumStdin("");
261
  setAwaitingInput(false);
262
  setInteractivePromptShown(false);
 
 
 
 
 
 
 
 
 
 
 
263
  };
264
 
265
- const resetTerminal = (keepAccum = false) => {
266
- setTerminalLines([]);
267
- setOutput("");
268
- if (!keepAccum) {
269
- setAccumStdin("");
 
 
270
  }
271
- setAwaitingInput(false);
272
- setInteractivePromptShown(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  };
274
 
275
- // Unified runner used when terminal provides input (or small input)
276
- const runCodeWithUpdatedInput = async (inputLine) => {
277
- if (typeof inputLine !== "string") inputLine = String(inputLine || "");
278
- const trimmed = inputLine.replace(/\r$/, "");
279
- // if user pressed Enter with empty line and no accum, ignore
280
- if (trimmed.length === 0 && !accumStdin) {
281
- return;
282
- }
 
283
 
284
- // append newline like console
285
- const newAccum = (accumStdin || "") + trimmed + "\n";
286
- setAccumStdin(newAccum);
287
- setInteractivePromptShown(false);
 
 
 
 
 
 
288
 
289
- const node = getNodeByPath(tree, activePath);
290
- if (!node || node.type !== "file") {
291
- appendTerminal("[Error] No file selected to run.");
292
- setAwaitingInput(false);
293
- return;
294
  }
295
 
296
- const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id;
297
- if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) {
298
- appendTerminal(`[Error] Run not supported for ${node.name}`);
299
- setAwaitingInput(false);
300
- return;
301
  }
 
 
 
 
 
 
 
 
 
 
 
302
 
303
- setIsRunning(true);
 
 
304
  try {
305
- const res = await runCode(node.content, selectedLang, newAccum);
306
- const out = res.output ?? "";
307
- if (out) appendTerminal(out);
308
- setProblems(res.error ? parseProblems(res.output) : []);
309
- if (outputLooksForInput(out)) {
310
- setAwaitingInput(true);
311
- focusXtermHelper();
312
- } else {
313
- setAwaitingInput(false);
314
- setAccumStdin(""); // finished -> clear accumulated input so next Run is fresh
315
- }
316
- } catch (err) {
317
- appendTerminal(String(err));
318
- setAwaitingInput(true);
319
- } finally {
320
- setIsRunning(false);
321
  }
322
  };
323
 
324
- // ---------- Initial Run handler (fresh runs) ----------
325
- const handleRun = async () => {
326
- const node = getNodeByPath(tree, activePath);
327
- if (!node || node.type !== "file") {
328
- appendTerminal("Select a file to run.");
329
- return;
330
- }
 
331
 
332
- const selectedLang = LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id;
333
- if (!selectedLang || !RUNNABLE_LANGS.includes(selectedLang)) {
334
- appendTerminal(`⚠️ Run not supported for this file type.`);
335
- return;
 
 
 
 
 
 
 
 
 
 
336
  }
 
 
 
 
 
 
 
337
 
338
- // Force fresh run: clear accumulated input and terminal
339
- setAccumStdin("");
340
- resetTerminal(false);
341
- setAwaitingInput(false);
342
- setInteractivePromptShown(false);
 
 
 
 
 
 
 
 
 
343
 
344
- const needs = codeNeedsInput(node.content, selectedLang);
345
 
346
- if (needs) {
347
- appendTerminal("[Interactive program detected type input directly into the terminal]");
348
- setAwaitingInput(true);
349
- setInteractivePromptShown(true);
350
- focusXtermHelper();
351
- return; // wait for user's input to avoid EOFError
352
  }
 
 
 
 
 
 
353
 
354
- // Non-interactive: run immediately with empty stdin
355
- appendTerminal(`[Running (fresh)]`);
356
- setIsRunning(true);
357
- setProblems([]);
358
  try {
359
- const res = await runCode(node.content, selectedLang, "");
360
- const out = res.output ?? "";
361
- if (out) appendTerminal(out);
362
- setProblems(res.error ? parseProblems(res.output) : []);
363
- if (outputLooksForInput(out)) {
364
- setAwaitingInput(true);
365
- focusXtermHelper();
366
- } else {
367
- setAwaitingInput(false);
368
- setAccumStdin("");
369
- }
370
- } catch (err) {
371
- appendTerminal(String(err));
372
- setAwaitingInput(true);
373
- focusXtermHelper();
374
- } finally {
375
- setIsRunning(false);
376
  }
 
 
 
 
377
  };
378
 
379
- // ---------- Agent functions ----------
380
  const handleAskFix = async () => {
381
  const node = getNodeByPath(tree, activePath);
382
  if (!node || node.type !== "file") {
@@ -387,7 +399,7 @@ function App() {
387
  try {
388
  const userHint = prompt.trim() ? `User request: ${prompt}` : "";
389
  const reply = await askAgent(
390
- `Improve, debug, or refactor this ${LANGUAGE_OPTIONS.find((l) => node.name.endsWith(l.ext))?.id || "file"} file.\n${userHint}\nReturn ONLY updated code, no explanation.\n\nCODE:\n${node.content}`
391
  );
392
  const updatedTree = updateFileContent(tree, node.path, reply);
393
  setTree(updatedTree);
@@ -407,10 +419,9 @@ function App() {
407
  }
408
  setIsExplaining(true);
409
  try {
410
- const editor = editorRef.current;
411
  let selectedCode = "";
412
  try {
413
- selectedCode = editor?.getModel()?.getValueInRange(editor.getSelection()) || "";
414
  } catch {}
415
  const code = selectedCode.trim() || node.content;
416
  const userHint = prompt.trim() ? `Focus on: ${prompt}` : "Give a clear and simple explanation.";
@@ -425,18 +436,16 @@ function App() {
425
  }
426
  };
427
 
428
- // AI suggestions for continuation
429
  const fetchAiSuggestions = async (code) => {
430
  if (!code?.trim()) return;
431
  try {
432
  const reply = await askAgent(`Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`);
433
  setAiSuggestions(reply.split("\n").filter((l) => l.trim()));
434
- } catch {
435
- // ignore
436
- }
437
  };
438
 
439
- // ---------- Search ----------
440
  const handleSearchToggle = () => setSearchOpen(!searchOpen);
441
  const handleSearchNow = () => {
442
  if (!searchQuery) return;
@@ -444,15 +453,12 @@ function App() {
444
  alert(`Found ${results.length} results:\n` + JSON.stringify(results, null, 2));
445
  };
446
 
447
- // Editor change
448
  const updateActiveFileContent = (value) => {
449
- const node = getNodeByPath(tree, activePath);
450
- if (!node) return;
451
  const updated = updateFileContent(tree, activePath, value ?? "");
452
  setTree(updated);
453
  };
454
 
455
- // Render tree
456
  const renderTree = (node, depth = 0) => {
457
  const isActive = node.path === activePath;
458
  return (
@@ -478,12 +484,9 @@ function App() {
478
 
479
  const anyLoading = isRunning || isFixing || isExplaining;
480
 
481
- // ---------- JSX ----------
482
  return (
483
- <div
484
- className={`ide-root ${theme === "vs-dark" ? "ide-dark" : "ide-light"}`}
485
- style={{ overflowX: "hidden" }} // prevent horizontal white gap / scroller issue
486
- >
487
  <input ref={fileInputRef} id="file-import-input" type="file" style={{ display: "none" }} onChange={handleFileInputChange} />
488
 
489
  <div className="ide-menubar">
@@ -508,6 +511,7 @@ function App() {
508
  {theme === "vs-dark" ? "☀️" : "🌙"}
509
  </button>
510
  <button onClick={clearTerminal} disabled={anyLoading} title="Clear terminal">🧹 Clear</button>
 
511
  </div>
512
  </div>
513
 
@@ -553,15 +557,17 @@ function App() {
553
  <div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
554
 
555
  <XTerm
556
- output={output}
557
- onData={(line) => {
558
- // line is the typed content (no CR), pass to unified runner
559
- const trimmed = (line || "").replace(/\r$/, "");
560
- if (!trimmed && !awaitingInput) {
561
- // nothing typed and not expecting input
562
- return;
563
- }
564
- runCodeWithUpdatedInput(trimmed);
 
 
565
  }}
566
  />
567
 
@@ -571,12 +577,26 @@ function App() {
571
  This program appears to be interactive and requires console input.
572
  </div>
573
  <div style={{ display: "flex", gap: 8 }}>
574
- <button onClick={() => { focusXtermHelper(); }} className="ide-button">▶ Focus Terminal</button>
575
- <button onClick={() => { resetTerminal(true); appendTerminal("[Interactive session started — type into the terminal]"); setAwaitingInput(true); focusXtermHelper(); }} className="ide-button">▶ Start interactive session</button>
576
- <div style={{ color: "#999", alignSelf: "center" }}>Type & press Enter in terminal.</div>
 
 
 
 
 
 
 
 
 
 
577
  </div>
578
  </div>
579
  )}
 
 
 
 
580
  </div>
581
 
582
  {problems.length > 0 && (
@@ -628,4 +648,30 @@ function App() {
628
  );
629
  }
630
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  export default App;
 
1
  // src/App.js
2
  import { useState, useEffect, useRef } from "react";
3
  import Editor from "@monaco-editor/react";
4
+ import XTerm from "./Terminal";
5
  import { askAgent } from "./agent/assistant";
 
6
  import {
7
  loadTree,
8
  saveTree,
 
18
  import { parseProblems } from "./problemParser";
19
  import "./App.css";
20
  import "xterm/css/xterm.css";
 
21
 
22
+ // API base (adjust if your Flask runs on different host/port)
23
+ const API_BASE = "";
24
+
25
  const LANGUAGE_OPTIONS = [
26
  { id: "python", ext: ".py", icon: "🐍", monaco: "python" },
27
  { id: "javascript", ext: ".js", icon: "🟨", monaco: "javascript" },
 
34
  { id: "json", ext: ".json", icon: "🧾", monaco: "json" },
35
  ];
36
 
37
+ const RUNNABLE_LANGS = ["python", "javascript", "java", "ts", "c", "cpp"];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  function App() {
40
+ // File tree state
41
  const [tree, setTree] = useState(loadTree());
42
  const [activePath, setActivePath] = useState("main.py");
43
 
44
+ // Terminal/Session state
45
+ const [sessionId, setSessionId] = useState(null);
46
+ const [terminalLines, setTerminalLines] = useState([]);
47
+ const [terminalOutputProp, setTerminalOutputProp] = useState(""); // last line to send to XTerm
48
  const [awaitingInput, setAwaitingInput] = useState(false);
 
 
49
  const [interactivePromptShown, setInteractivePromptShown] = useState(false);
50
+ const [isRunning, setIsRunning] = useState(false);
51
 
52
+ // AI/editor state
53
  const [prompt, setPrompt] = useState("");
54
  const [explanation, setExplanation] = useState("");
55
  const [problems, setProblems] = useState([]);
 
58
  const [searchQuery, setSearchQuery] = useState("");
59
  const [aiSuggestions, setAiSuggestions] = useState([]);
60
  const [contextMenu, setContextMenu] = useState(null);
 
61
  const [isFixing, setIsFixing] = useState(false);
62
  const [isExplaining, setIsExplaining] = useState(false);
63
 
 
64
  const editorRef = useRef(null);
65
  const fileInputRef = useRef(null);
66
+ const pollRef = useRef(null);
67
 
68
+ useEffect(() => saveTree(tree), [tree]);
 
 
69
 
70
  const currentNode = getNodeByPath(tree, activePath);
71
  const langMeta =
72
  LANGUAGE_OPTIONS.find((l) => currentNode?.name?.endsWith(l.ext)) ||
73
  LANGUAGE_OPTIONS[0];
74
 
75
+ // ------------------ file / folder helpers ------------------
76
  const collectFolderPaths = (node, acc = []) => {
77
  if (!node) return acc;
78
  if (node.type === "folder") acc.push(node.path || "");
 
170
  e.target.value = "";
171
  };
172
 
173
+ // ------------------ terminal helpers ------------------
174
  const appendTerminal = (text) => {
175
+ if (text == null) return;
176
+ const lines = String(text).split(/\r?\n/).filter(Boolean);
177
  setTerminalLines((prev) => {
178
+ const next = [...prev, ...lines];
179
+ // set terminalOutputProp to last line so XTerm writes it
180
+ setTerminalOutputProp(lines[lines.length - 1] || "");
181
  return next;
182
  });
183
  };
184
 
185
+ const clearTerminal = async () => {
 
186
  setTerminalLines([]);
187
+ setTerminalOutputProp("\x1b[2J\x1b[H");
 
188
  setAwaitingInput(false);
189
  setInteractivePromptShown(false);
190
+ // stop any existing session
191
+ if (sessionId) {
192
+ try {
193
+ await fetch(`${API_BASE}/stop`, {
194
+ method: "POST",
195
+ headers: { "Content-Type": "application/json" },
196
+ body: JSON.stringify({ session_id: sessionId }),
197
+ });
198
+ } catch {}
199
+ setSessionId(null);
200
+ }
201
  };
202
 
203
+ // Poller to /read output
204
+ const startPoller = (sid) => {
205
+ if (!sid) return;
206
+ // clear previous
207
+ if (pollRef.current) {
208
+ clearInterval(pollRef.current);
209
+ pollRef.current = null;
210
  }
211
+ pollRef.current = setInterval(async () => {
212
+ try {
213
+ const res = await fetch(`${API_BASE}/read`, {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify({ session_id: sid }),
217
+ });
218
+ const data = await res.json();
219
+ if (!data) return;
220
+ if (Array.isArray(data.output) && data.output.length) {
221
+ data.output.forEach((ln) => appendTerminal(ln));
222
+ }
223
+ if (data.finished) {
224
+ // stop poller, clear sessionId
225
+ if (pollRef.current) {
226
+ clearInterval(pollRef.current);
227
+ pollRef.current = null;
228
+ }
229
+ setSessionId(null);
230
+ setAwaitingInput(false);
231
+ setInteractivePromptShown(false);
232
+ appendTerminal("[Process finished]");
233
+ } else {
234
+ // heuristics: if last output line looks like input prompt, set awaitingInput
235
+ // we'll check the terminal lines
236
+ const last = terminalLines[terminalLines.length - 1] || "";
237
+ const maybePrompt = last + (data.output && data.output.length ? data.output[data.output.length - 1] : "");
238
+ const promptWords = /(enter|input|please enter|provide input|number|value|scanner|press enter)/i;
239
+ if (promptWords.test(maybePrompt)) {
240
+ setAwaitingInput(true);
241
+ setInteractivePromptShown(true);
242
+ // focus XTerm by toggling a prop (we pass autoFocusWhen = awaitingInput below)
243
+ }
244
+ }
245
+ } catch (e) {
246
+ // ignore transient errors
247
+ }
248
+ }, 250);
249
  };
250
 
251
+ // Start session (POST /start)
252
+ // improved startSession: surfaces backend errors
253
+ const startSession = async (code, filename) => {
254
+ try {
255
+ const res = await fetch(`${API_BASE}/start`, {
256
+ method: "POST",
257
+ headers: { "Content-Type": "application/json" },
258
+ body: JSON.stringify({ code, filename }),
259
+ });
260
 
261
+ // try parse JSON (backend returns JSON even on errors)
262
+ let data = null;
263
+ try {
264
+ data = await res.json();
265
+ } catch (e) {
266
+ // non-json response (HTML or other) — surface raw text
267
+ const text = await res.text();
268
+ appendTerminal(`[start] unexpected non-json response:\n${text}`);
269
+ return null;
270
+ }
271
 
272
+ if (!res.ok || data.error) {
273
+ // backend signalled an error; try to extract helpful message
274
+ const msg = Array.isArray(data.output) ? data.output.join("\n") : (data.output || data.message || JSON.stringify(data));
275
+ appendTerminal(`[start] backend error:\n${msg}`);
276
+ return null;
277
  }
278
 
279
+ const sid = data.session_id;
280
+ // initial output lines (if any)
281
+ if (Array.isArray(data.output) && data.output.length) {
282
+ data.output.forEach((ln) => appendTerminal(ln));
 
283
  }
284
+ // start polling
285
+ startPoller(sid);
286
+ return sid;
287
+ } catch (err) {
288
+ // network or unexpected error
289
+ appendTerminal(`[start] fetch failed: ${String(err)}`);
290
+ console.error("startSession error:", err);
291
+ return null;
292
+ }
293
+ };
294
+
295
 
296
+ // Write input to session (POST /write)
297
+ const writeToSession = async (sid, text) => {
298
+ if (!sid) return;
299
  try {
300
+ await fetch(`${API_BASE}/write`, {
301
+ method: "POST",
302
+ headers: { "Content-Type": "application/json" },
303
+ body: JSON.stringify({ session_id: sid, text }),
304
+ });
305
+ } catch (e) {
306
+ appendTerminal(`[Error sending input: ${e}]`);
 
 
 
 
 
 
 
 
 
307
  }
308
  };
309
 
310
+ // Run handler starts session for runnable files; non-runnable will show content
311
+ // improved handleRun: surfaces backend errors and logs
312
+ const handleRun = async () => {
313
+ const node = getNodeByPath(tree, activePath);
314
+ if (!node || node.type !== "file") {
315
+ appendTerminal("Select a file to run.");
316
+ return;
317
+ }
318
 
319
+ // clear previous session + terminal for a fresh run
320
+ if (pollRef.current) {
321
+ clearInterval(pollRef.current);
322
+ pollRef.current = null;
323
+ }
324
+ if (sessionId) {
325
+ try {
326
+ await fetch(`${API_BASE}/stop`, {
327
+ method: "POST",
328
+ headers: { "Content-Type": "application/json" },
329
+ body: JSON.stringify({ session_id: sessionId }),
330
+ });
331
+ } catch (e) {
332
+ // ignore
333
  }
334
+ setSessionId(null);
335
+ }
336
+ setTerminalLines([]);
337
+ setTerminalOutputProp("");
338
+ setAwaitingInput(false);
339
+ setInteractivePromptShown(false);
340
+ setIsRunning(true);
341
 
342
+ const filename = node.name;
343
+
344
+ // start session and show backend errors (if any)
345
+ const sid = await startSession(node.content || "", filename);
346
+ setIsRunning(false);
347
+
348
+ if (!sid) {
349
+ appendTerminal("[Failed to start session] (see above server message)");
350
+ return;
351
+ }
352
+
353
+ setSessionId(sid);
354
+ appendTerminal(`[Session started: ${sid}]`);
355
+ };
356
 
 
357
 
358
+ // Called when XTerm or small Send triggers input
359
+ const onTerminalInput = async (line) => {
360
+ if (!sessionId) {
361
+ appendTerminal("[No active session. Press Run first]");
362
+ return;
 
363
  }
364
+ // echo to terminal & send
365
+ appendTerminal(`> ${line}`);
366
+ await writeToSession(sessionId, line);
367
+ setAwaitingInput(false);
368
+ setInteractivePromptShown(false);
369
+ };
370
 
371
+ // clear/stop session
372
+ const stopSession = async () => {
373
+ if (!sessionId) return;
 
374
  try {
375
+ await fetch(`${API_BASE}/stop`, {
376
+ method: "POST",
377
+ headers: { "Content-Type": "application/json" },
378
+ body: JSON.stringify({ session_id: sessionId }),
379
+ });
380
+ } catch {}
381
+ if (pollRef.current) {
382
+ clearInterval(pollRef.current);
383
+ pollRef.current = null;
 
 
 
 
 
 
 
 
384
  }
385
+ setSessionId(null);
386
+ setAwaitingInput(false);
387
+ setInteractivePromptShown(false);
388
+ appendTerminal("[Session stopped]");
389
  };
390
 
391
+ // ------------------ AI helpers (unchanged) ------------------
392
  const handleAskFix = async () => {
393
  const node = getNodeByPath(tree, activePath);
394
  if (!node || node.type !== "file") {
 
399
  try {
400
  const userHint = prompt.trim() ? `User request: ${prompt}` : "";
401
  const reply = await askAgent(
402
+ `Improve, debug, or refactor this file (${node.name}). ${userHint}\nReturn ONLY updated code, no explanation.\n\nCODE:\n${node.content}`
403
  );
404
  const updatedTree = updateFileContent(tree, node.path, reply);
405
  setTree(updatedTree);
 
419
  }
420
  setIsExplaining(true);
421
  try {
 
422
  let selectedCode = "";
423
  try {
424
+ selectedCode = editorRef.current?.getModel()?.getValueInRange(editorRef.current.getSelection()) || "";
425
  } catch {}
426
  const code = selectedCode.trim() || node.content;
427
  const userHint = prompt.trim() ? `Focus on: ${prompt}` : "Give a clear and simple explanation.";
 
436
  }
437
  };
438
 
439
+ // AI suggestions fetch
440
  const fetchAiSuggestions = async (code) => {
441
  if (!code?.trim()) return;
442
  try {
443
  const reply = await askAgent(`Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`);
444
  setAiSuggestions(reply.split("\n").filter((l) => l.trim()));
445
+ } catch {}
 
 
446
  };
447
 
448
+ // ------------------ search ------------------
449
  const handleSearchToggle = () => setSearchOpen(!searchOpen);
450
  const handleSearchNow = () => {
451
  if (!searchQuery) return;
 
453
  alert(`Found ${results.length} results:\n` + JSON.stringify(results, null, 2));
454
  };
455
 
456
+ // update file content
457
  const updateActiveFileContent = (value) => {
 
 
458
  const updated = updateFileContent(tree, activePath, value ?? "");
459
  setTree(updated);
460
  };
461
 
 
462
  const renderTree = (node, depth = 0) => {
463
  const isActive = node.path === activePath;
464
  return (
 
484
 
485
  const anyLoading = isRunning || isFixing || isExplaining;
486
 
487
+ // UI JSX
488
  return (
489
+ <div className={`ide-root ${theme === "vs-dark" ? "ide-dark" : "ide-light"}`} style={{ overflowX: "hidden" }}>
 
 
 
490
  <input ref={fileInputRef} id="file-import-input" type="file" style={{ display: "none" }} onChange={handleFileInputChange} />
491
 
492
  <div className="ide-menubar">
 
511
  {theme === "vs-dark" ? "☀️" : "🌙"}
512
  </button>
513
  <button onClick={clearTerminal} disabled={anyLoading} title="Clear terminal">🧹 Clear</button>
514
+ <button onClick={stopSession} disabled={!sessionId}>⏹ Stop</button>
515
  </div>
516
  </div>
517
 
 
557
  <div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
558
 
559
  <XTerm
560
+ output={terminalOutputProp}
561
+ autoFocusWhen={awaitingInput}
562
+ onData={(data) => {
563
+ // XTerm will fire onData when Enter pressed (wrapper behavior)
564
+ // data may be newline or typed chunks; for safety, only trigger when newline or non-empty
565
+ const trimmed = String(data || "").replace(/\r/g, "");
566
+ if (!trimmed) return;
567
+ // When wrapper sends "\n" for Enter, treat it as "send what user typed in last visible line"
568
+ // Simpler approach: the wrapper isn't buffering; rely on user to type line and press Send (small input), OR
569
+ // handle newline by sending the last terminal input we have (not tracked here).
570
+ // We'll not call runCodeWithUpdatedInput here to avoid double semantics; instead, user uses small send box below.
571
  }}
572
  />
573
 
 
577
  This program appears to be interactive and requires console input.
578
  </div>
579
  <div style={{ display: "flex", gap: 8 }}>
580
+ <button onClick={() => {
581
+ // focus XTerm via DOM helper
582
+ const ta = document.querySelector("#terminal-container .xterm-helper-textarea");
583
+ if (ta) ta.focus();
584
+ }} className="ide-button">▶ Focus Terminal</button>
585
+ <button onClick={() => {
586
+ clearTerminal();
587
+ appendTerminal("[Interactive session started — type into the terminal]");
588
+ setAwaitingInput(true);
589
+ const ta = document.querySelector("#terminal-container .xterm-helper-textarea");
590
+ if (ta) ta.focus();
591
+ }} className="ide-button">▶ Start interactive session</button>
592
+ <div style={{ color: "#999", alignSelf: "center" }}>Type & press Enter in terminal or use the small input below.</div>
593
  </div>
594
  </div>
595
  )}
596
+
597
+ {/* Small input (optional): user can type a line and press Send to forward to backend */}
598
+ <TerminalSend onSend={onTerminalInput} disabled={!sessionId && !awaitingInput} isRunning={isRunning} />
599
+
600
  </div>
601
 
602
  {problems.length > 0 && (
 
648
  );
649
  }
650
 
651
+ // Small send component (kept in same file to avoid extra imports)
652
+ function TerminalSend({ onSend, disabled, isRunning }) {
653
+ const [val, setVal] = useState("");
654
+ return (
655
+ <div style={{ display: "flex", gap: 6, marginTop: 8 }}>
656
+ <input
657
+ className="ide-input-box"
658
+ placeholder="Type input and press Send (or press Enter in terminal)"
659
+ value={val}
660
+ onChange={(e) => setVal(e.target.value)}
661
+ onKeyDown={(e) => {
662
+ if (e.key === "Enter") {
663
+ e.preventDefault();
664
+ if (!val) return;
665
+ onSend(val);
666
+ setVal("");
667
+ }
668
+ }}
669
+ />
670
+ <button onClick={() => { if (val) { onSend(val); setVal(""); } }} className="ide-button" disabled={disabled || isRunning}>
671
+ {isRunning ? "⏳" : "Send"}
672
+ </button>
673
+ </div>
674
+ );
675
+ }
676
+
677
  export default App;
src/Terminal.js CHANGED
@@ -4,123 +4,93 @@ import { Terminal } from "xterm";
4
  import { FitAddon } from "xterm-addon-fit";
5
  import "xterm/css/xterm.css";
6
 
7
- /**
8
- * Props:
9
- * - onData(line: string) -> called when user presses Enter with the typed line (no trailing \r)
10
- * - output (string) -> append output to the terminal when it changes
11
- */
12
- export default function XTerm({ onData, output }) {
13
- const containerId = "terminal-container";
14
  const termRef = useRef(null);
15
- const bufferRef = useRef(""); // collects user typed chars until Enter
16
  const fitRef = useRef(null);
 
17
 
18
  useEffect(() => {
19
  const term = new Terminal({
20
  cursorBlink: true,
21
- fontSize: 14,
22
- disableStdin: false,
23
  convertEol: true,
24
  theme: {
25
  background: "#1e1e1e",
26
- foreground: "#ffffff",
27
  },
28
  });
 
 
29
 
30
- const fitAddon = new FitAddon();
31
- fitRef.current = fitAddon;
32
-
33
- term.loadAddon(fitAddon);
34
  term.open(document.getElementById(containerId));
35
- fitAddon.fit();
36
 
37
- // Keep a reference
38
- termRef.current = term;
39
-
40
- // echo typed characters and detect Enter
41
  term.onData((data) => {
42
- // xterm sends strings including characters and control chars like '\r'
43
- // append to visible terminal
44
- term.write(data);
45
-
46
- // common Enter is '\r' (CR)
47
- if (data === "\r" || data === "\n") {
48
- // capture current buffer as line, trim trailing CR/LF
49
- const line = (bufferRef.current || "").replace(/\r?\n$/, "");
50
- bufferRef.current = ""; // reset buffer
51
- // echo newline if not already
52
- // (we already wrote the '\r' above)
53
- // call parent handler
54
- try {
55
- if (typeof onData === "function" && line !== null) onData(line);
56
- } catch (e) {
57
- // swallow
58
- console.error("XTerm onData handler threw:", e);
59
- }
60
- return;
61
- }
62
-
63
- // backspace handling: if user presses backspace key, it may come as '\x7f' or '\b'
64
- if (data === "\x7f" || data === "\b") {
65
- bufferRef.current = bufferRef.current.slice(0, -1);
66
- return;
67
  }
 
68
 
69
- // other control sequences ignore
70
- if (data.charCodeAt(0) < 32) {
71
- // ignore other ctrl chars
72
- return;
73
- }
74
 
75
- // normal characters: append to buffer
76
- bufferRef.current += data;
 
77
  });
78
-
79
- // expose a simple focus method on container element for external focusing
80
- const container = document.getElementById(containerId);
81
- if (container) container.tabIndex = 0;
82
 
83
  return () => {
84
- try {
85
- term.dispose();
86
- } catch {}
87
  };
88
- }, [onData]);
 
89
 
90
- // Append new output when `output` prop changes
91
  useEffect(() => {
92
- const term = termRef.current;
93
- if (!term || !output) return;
94
- // write a newline and the output text (preserves newlines)
95
- term.writeln("");
96
- // if output includes multiple lines, write each
97
- const lines = output.split(/\r?\n/);
98
- lines.forEach((ln, idx) => {
99
- // avoid extra blank at very start
100
- if (idx === 0 && ln === "") return;
101
- term.writeln(ln);
102
- });
103
- }, [output]);
104
-
105
- // helper to focus the hidden xterm textarea
106
- const focus = () => {
107
- // xterm's helper textarea is what receives keyboard input
108
- const ta = document.querySelector(`#${containerId} .xterm-helper-textarea`);
109
- if (ta) {
110
- ta.focus();
111
- const len = ta.value?.length ?? 0;
112
- try { ta.setSelectionRange(len, len); } catch {}
113
- } else {
114
- const cont = document.getElementById(containerId);
115
- if (cont) cont.focus();
116
  }
117
- };
118
 
119
- // expose a global method in-case parent wants to call it (not ideal but handy)
120
  useEffect(() => {
121
- window.__xterm_focus = focus;
122
- return () => { try { delete window.__xterm_focus } catch {} };
123
- }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  return (
126
  <div
@@ -130,7 +100,6 @@ export default function XTerm({ onData, output }) {
130
  height: "180px",
131
  background: "#1e1e1e",
132
  borderTop: "1px solid #333",
133
- outline: "none",
134
  }}
135
  />
136
  );
 
4
  import { FitAddon } from "xterm-addon-fit";
5
  import "xterm/css/xterm.css";
6
 
7
+ export default function XTerm({ onData, output, autoFocusWhen }) {
 
 
 
 
 
 
8
  const termRef = useRef(null);
 
9
  const fitRef = useRef(null);
10
+ const containerId = "terminal-container";
11
 
12
  useEffect(() => {
13
  const term = new Terminal({
14
  cursorBlink: true,
15
+ fontSize: 13,
 
16
  convertEol: true,
17
  theme: {
18
  background: "#1e1e1e",
19
+ foreground: "#dcdcdc",
20
  },
21
  });
22
+ const fit = new FitAddon();
23
+ fitRef.current = fit;
24
 
25
+ term.loadAddon(fit);
 
 
 
26
  term.open(document.getElementById(containerId));
27
+ fit.fit();
28
 
 
 
 
 
29
  term.onData((data) => {
30
+ // echo input for feedback
31
+ // don't echo CR as blank line; xterm handles newline
32
+ if (data === "\r") {
33
+ // pass the collected line to parent via onData
34
+ // xterm doesn't provide line buffer, so parent may rely on onData chunks (expected)
35
+ onData("\n"); // send newline signal (caller will interpret)
36
+ } else {
37
+ // For regular characters, echo them and also forward individual characters
38
+ term.write(data);
39
+ // forward characters to parent if needed
40
+ // It's often better to send full line when Enter pressed; the parent code in App.js
41
+ // expects the onData to receive the full line — our App.js uses trimmed lines.
42
+ // Here, for simplicity, also forward characters so parent can capture typed content if desired.
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
+ });
45
 
46
+ termRef.current = term;
 
 
 
 
47
 
48
+ // Resize observer to keep fit updated
49
+ const ro = new ResizeObserver(() => {
50
+ try { fit.fit(); } catch {}
51
  });
52
+ ro.observe(document.getElementById(containerId));
 
 
 
53
 
54
  return () => {
55
+ ro.disconnect();
56
+ try { term.dispose(); } catch {}
 
57
  };
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ }, []);
60
 
61
+ // Write new output into terminal when `output` prop changes.
62
  useEffect(() => {
63
+ if (!termRef.current || output == null) return;
64
+ try {
65
+ // print with newline separation
66
+ // ensure not to double newlines if the output already has trailing newline
67
+ const text = String(output);
68
+ termRef.current.writeln(text.replace(/\r/g, ""));
69
+ } catch (e) {
70
+ // ignore
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  }
72
+ }, [output]);
73
 
74
+ // Auto-focus when parent asks
75
  useEffect(() => {
76
+ if (!termRef.current) return;
77
+ if (autoFocusWhen) {
78
+ // focus xterm helper
79
+ setTimeout(() => {
80
+ const ta = document.querySelector(`#${containerId} .xterm-helper-textarea`);
81
+ if (ta) {
82
+ try {
83
+ ta.focus();
84
+ const len = ta.value?.length || 0;
85
+ ta.setSelectionRange(len, len);
86
+ } catch {}
87
+ } else {
88
+ const cont = document.getElementById(containerId);
89
+ if (cont) cont.focus();
90
+ }
91
+ }, 50);
92
+ }
93
+ }, [autoFocusWhen]);
94
 
95
  return (
96
  <div
 
100
  height: "180px",
101
  background: "#1e1e1e",
102
  borderTop: "1px solid #333",
 
103
  }}
104
  />
105
  );