FrederickSundeep commited on
Commit
cc5a7ba
·
1 Parent(s): a35908e

commit initial 09-12-2025 46

Browse files
Files changed (2) hide show
  1. src/App.js +212 -238
  2. src/Terminal.js +91 -60
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 XTerm from "./Terminal";
5
  import { askAgent } from "./agent/assistant";
 
6
  import {
7
  loadTree,
8
  saveTree,
@@ -18,10 +18,9 @@ import { downloadProjectZip } from "./zipExport";
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,22 +33,92 @@ const LANGUAGE_OPTIONS = [
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,21 +127,24 @@ function App() {
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,127 +242,86 @@ function App() {
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
- const startSession = async (code, filename) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  try {
254
- const res = await fetch(`${API_BASE}/start`, {
255
- method: "POST",
256
- headers: { "Content-Type": "application/json" },
257
- body: JSON.stringify({ code, filename }),
258
- });
259
- const data = await res.json();
260
- if (!res.ok || data.error) {
261
- appendTerminal(data.output ? data.output.join("\n") : `[Failed starting session]`);
262
- return null;
 
263
  }
264
- const sid = data.session_id;
265
- setSessionId(sid);
266
- // initial output if any
267
- if (Array.isArray(data.output) && data.output.length) {
268
- data.output.forEach((ln) => appendTerminal(ln));
269
- }
270
- // poll for more output
271
- startPoller(sid);
272
- return sid;
273
  } catch (err) {
274
  appendTerminal(String(err));
275
- return null;
276
- }
277
- };
278
-
279
- // Write input to session (POST /write)
280
- const writeToSession = async (sid, text) => {
281
- if (!sid) return;
282
- try {
283
- await fetch(`${API_BASE}/write`, {
284
- method: "POST",
285
- headers: { "Content-Type": "application/json" },
286
- body: JSON.stringify({ session_id: sid, text }),
287
- });
288
- } catch (e) {
289
- appendTerminal(`[Error sending input: ${e}]`);
290
  }
291
  };
292
 
293
- // Run handler starts session for runnable files; non-runnable will show content
294
  const handleRun = async () => {
295
  const node = getNodeByPath(tree, activePath);
296
  if (!node || node.type !== "file") {
@@ -298,77 +329,54 @@ function App() {
298
  return;
299
  }
300
 
301
- // always clear previous session and terminal for fresh run
302
- if (pollRef.current) {
303
- clearInterval(pollRef.current);
304
- pollRef.current = null;
305
- }
306
- if (sessionId) {
307
- try {
308
- await fetch(`${API_BASE}/stop`, {
309
- method: "POST",
310
- headers: { "Content-Type": "application/json" },
311
- body: JSON.stringify({ session_id: sessionId }),
312
- });
313
- } catch {}
314
- setSessionId(null);
315
  }
316
- setTerminalLines([]);
317
- setTerminalOutputProp("");
 
 
318
  setAwaitingInput(false);
319
  setInteractivePromptShown(false);
320
- setIsRunning(true);
321
 
322
- // pick filename for extension; if path contains folders, use actual filename
323
- const filename = node.name;
324
 
325
- // try start session
326
- const sid = await startSession(node.content || "", filename);
327
- setIsRunning(false);
328
- if (!sid) {
329
- appendTerminal("[Failed to start session]");
330
- return;
331
  }
332
- // session started; the poller will pick up outputs
333
- setSessionId(sid);
334
- setAwaitingInput(false);
335
- setInteractivePromptShown(false);
336
- };
337
 
338
- // Called when XTerm or small Send triggers input
339
- const onTerminalInput = async (line) => {
340
- if (!sessionId) {
341
- appendTerminal("[No active session. Press Run first]");
342
- return;
343
- }
344
- // echo to terminal & send
345
- appendTerminal(`> ${line}`);
346
- await writeToSession(sessionId, line);
347
- setAwaitingInput(false);
348
- setInteractivePromptShown(false);
349
- };
350
-
351
- // clear/stop session
352
- const stopSession = async () => {
353
- if (!sessionId) return;
354
  try {
355
- await fetch(`${API_BASE}/stop`, {
356
- method: "POST",
357
- headers: { "Content-Type": "application/json" },
358
- body: JSON.stringify({ session_id: sessionId }),
359
- });
360
- } catch {}
361
- if (pollRef.current) {
362
- clearInterval(pollRef.current);
363
- pollRef.current = null;
 
 
 
 
 
 
 
 
364
  }
365
- setSessionId(null);
366
- setAwaitingInput(false);
367
- setInteractivePromptShown(false);
368
- appendTerminal("[Session stopped]");
369
  };
370
 
371
- // ------------------ AI helpers (unchanged) ------------------
372
  const handleAskFix = async () => {
373
  const node = getNodeByPath(tree, activePath);
374
  if (!node || node.type !== "file") {
@@ -379,7 +387,7 @@ function App() {
379
  try {
380
  const userHint = prompt.trim() ? `User request: ${prompt}` : "";
381
  const reply = await askAgent(
382
- `Improve, debug, or refactor this file (${node.name}). ${userHint}\nReturn ONLY updated code, no explanation.\n\nCODE:\n${node.content}`
383
  );
384
  const updatedTree = updateFileContent(tree, node.path, reply);
385
  setTree(updatedTree);
@@ -399,9 +407,10 @@ function App() {
399
  }
400
  setIsExplaining(true);
401
  try {
 
402
  let selectedCode = "";
403
  try {
404
- selectedCode = editorRef.current?.getModel()?.getValueInRange(editorRef.current.getSelection()) || "";
405
  } catch {}
406
  const code = selectedCode.trim() || node.content;
407
  const userHint = prompt.trim() ? `Focus on: ${prompt}` : "Give a clear and simple explanation.";
@@ -416,16 +425,18 @@ function App() {
416
  }
417
  };
418
 
419
- // AI suggestions fetch
420
  const fetchAiSuggestions = async (code) => {
421
  if (!code?.trim()) return;
422
  try {
423
  const reply = await askAgent(`Suggest possible next lines for continuation. Return 3 short snippets.\n${code}`);
424
  setAiSuggestions(reply.split("\n").filter((l) => l.trim()));
425
- } catch {}
 
 
426
  };
427
 
428
- // ------------------ search ------------------
429
  const handleSearchToggle = () => setSearchOpen(!searchOpen);
430
  const handleSearchNow = () => {
431
  if (!searchQuery) return;
@@ -433,12 +444,15 @@ function App() {
433
  alert(`Found ${results.length} results:\n` + JSON.stringify(results, null, 2));
434
  };
435
 
436
- // update file content
437
  const updateActiveFileContent = (value) => {
 
 
438
  const updated = updateFileContent(tree, activePath, value ?? "");
439
  setTree(updated);
440
  };
441
 
 
442
  const renderTree = (node, depth = 0) => {
443
  const isActive = node.path === activePath;
444
  return (
@@ -464,9 +478,12 @@ function App() {
464
 
465
  const anyLoading = isRunning || isFixing || isExplaining;
466
 
467
- // UI JSX
468
  return (
469
- <div className={`ide-root ${theme === "vs-dark" ? "ide-dark" : "ide-light"}`} style={{ overflowX: "hidden" }}>
 
 
 
470
  <input ref={fileInputRef} id="file-import-input" type="file" style={{ display: "none" }} onChange={handleFileInputChange} />
471
 
472
  <div className="ide-menubar">
@@ -491,7 +508,6 @@ function App() {
491
  {theme === "vs-dark" ? "☀️" : "🌙"}
492
  </button>
493
  <button onClick={clearTerminal} disabled={anyLoading} title="Clear terminal">🧹 Clear</button>
494
- <button onClick={stopSession} disabled={!sessionId}>⏹ Stop</button>
495
  </div>
496
  </div>
497
 
@@ -537,17 +553,15 @@ function App() {
537
  <div style={{ fontSize: 12, color: "#ccc", marginBottom: 6 }}>Terminal</div>
538
 
539
  <XTerm
540
- output={terminalOutputProp}
541
- autoFocusWhen={awaitingInput}
542
- onData={(data) => {
543
- // XTerm will fire onData when Enter pressed (wrapper behavior)
544
- // data may be newline or typed chunks; for safety, only trigger when newline or non-empty
545
- const trimmed = String(data || "").replace(/\r/g, "");
546
- if (!trimmed) return;
547
- // When wrapper sends "\n" for Enter, treat it as "send what user typed in last visible line"
548
- // Simpler approach: the wrapper isn't buffering; rely on user to type line and press Send (small input), OR
549
- // handle newline by sending the last terminal input we have (not tracked here).
550
- // We'll not call runCodeWithUpdatedInput here to avoid double semantics; instead, user uses small send box below.
551
  }}
552
  />
553
 
@@ -557,26 +571,12 @@ function App() {
557
  This program appears to be interactive and requires console input.
558
  </div>
559
  <div style={{ display: "flex", gap: 8 }}>
560
- <button onClick={() => {
561
- // focus XTerm via DOM helper
562
- const ta = document.querySelector("#terminal-container .xterm-helper-textarea");
563
- if (ta) ta.focus();
564
- }} className="ide-button">▶ Focus Terminal</button>
565
- <button onClick={() => {
566
- clearTerminal();
567
- appendTerminal("[Interactive session started — type into the terminal]");
568
- setAwaitingInput(true);
569
- const ta = document.querySelector("#terminal-container .xterm-helper-textarea");
570
- if (ta) ta.focus();
571
- }} className="ide-button">▶ Start interactive session</button>
572
- <div style={{ color: "#999", alignSelf: "center" }}>Type & press Enter in terminal or use the small input below.</div>
573
  </div>
574
  </div>
575
  )}
576
-
577
- {/* Small input (optional): user can type a line and press Send to forward to backend */}
578
- <TerminalSend onSend={onTerminalInput} disabled={!sessionId && !awaitingInput} isRunning={isRunning} />
579
-
580
  </div>
581
 
582
  {problems.length > 0 && (
@@ -628,30 +628,4 @@ function App() {
628
  );
629
  }
630
 
631
- // Small send component (kept in same file to avoid extra imports)
632
- function TerminalSend({ onSend, disabled, isRunning }) {
633
- const [val, setVal] = useState("");
634
- return (
635
- <div style={{ display: "flex", gap: 6, marginTop: 8 }}>
636
- <input
637
- className="ide-input-box"
638
- placeholder="Type input and press Send (or press Enter in terminal)"
639
- value={val}
640
- onChange={(e) => setVal(e.target.value)}
641
- onKeyDown={(e) => {
642
- if (e.key === "Enter") {
643
- e.preventDefault();
644
- if (!val) return;
645
- onSend(val);
646
- setVal("");
647
- }
648
- }}
649
- />
650
- <button onClick={() => { if (val) { onSend(val); setVal(""); } }} className="ide-button" disabled={disabled || isRunning}>
651
- {isRunning ? "⏳" : "Send"}
652
- </button>
653
- </div>
654
- );
655
- }
656
-
657
  export default App;
 
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
  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
  { 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
  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
  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") {
 
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
  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
  }
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
  }
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
  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
 
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
  {theme === "vs-dark" ? "☀️" : "🌙"}
509
  </button>
510
  <button onClick={clearTerminal} disabled={anyLoading} title="Clear terminal">🧹 Clear</button>
 
511
  </div>
512
  </div>
513
 
 
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
  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
  );
629
  }
630
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  export default App;
src/Terminal.js CHANGED
@@ -4,93 +4,123 @@ import { Terminal } from "xterm";
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,6 +130,7 @@ export default function XTerm({ onData, output, autoFocusWhen }) {
100
  height: "180px",
101
  background: "#1e1e1e",
102
  borderTop: "1px solid #333",
 
103
  }}
104
  />
105
  );
 
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
  height: "180px",
131
  background: "#1e1e1e",
132
  borderTop: "1px solid #333",
133
+ outline: "none",
134
  }}
135
  />
136
  );