neural-loom 0.2.59 → 0.2.61

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 (186) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +3 -0
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/cache/.previewinfo +1 -1
  5. package/.next/cache/.rscinfo +1 -1
  6. package/.next/cache/.tsbuildinfo +1 -1
  7. package/.next/diagnostics/route-bundle-stats.json +2 -2
  8. package/.next/fallback-build-manifest.json +3 -3
  9. package/.next/prerender-manifest.json +3 -3
  10. package/.next/routes-manifest.json +18 -0
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  22. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  23. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  24. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  25. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/server/app/api/configs/route/app-paths-manifest.json +3 -0
  27. package/.next/server/app/api/configs/route/build-manifest.json +9 -0
  28. package/.next/server/app/api/configs/route/server-reference-manifest.json +4 -0
  29. package/.next/server/app/api/configs/route.js +6 -0
  30. package/.next/server/app/api/configs/route.js.map +5 -0
  31. package/.next/server/app/api/configs/route.js.nft.json +1 -0
  32. package/.next/server/app/api/configs/route_client-reference-manifest.js +3 -0
  33. package/.next/server/app/api/context/aider/route.js.nft.json +1 -1
  34. package/.next/server/app/api/context/claude/route.js.nft.json +1 -1
  35. package/.next/server/app/api/context/route.js.nft.json +1 -1
  36. package/.next/server/app/api/context/ssh/route.js.nft.json +1 -1
  37. package/.next/server/app/api/recents/route.js.nft.json +1 -1
  38. package/.next/server/app/api/sessions/inject/route.js +2 -2
  39. package/.next/server/app/api/sessions/inject/route.js.nft.json +1 -1
  40. package/.next/server/app/api/sessions/input/route.js +2 -2
  41. package/.next/server/app/api/sessions/input/route.js.nft.json +1 -1
  42. package/.next/server/app/api/sessions/launch/route.js +2 -2
  43. package/.next/server/app/api/sessions/launch/route.js.nft.json +1 -1
  44. package/.next/server/app/api/sessions/route.js +2 -2
  45. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  46. package/.next/server/app/api/sessions/scope/route.js +2 -2
  47. package/.next/server/app/api/sessions/scope/route.js.nft.json +1 -1
  48. package/.next/server/app/api/sessions/start/route.js +2 -2
  49. package/.next/server/app/api/sessions/start/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/stats/route.js +2 -2
  51. package/.next/server/app/api/sessions/stats/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/stop/route.js +2 -2
  53. package/.next/server/app/api/sessions/stop/route.js.nft.json +1 -1
  54. package/.next/server/app/api/sessions/upload-image/route/app-paths-manifest.json +3 -0
  55. package/.next/server/app/api/sessions/upload-image/route/build-manifest.json +9 -0
  56. package/.next/server/app/api/sessions/upload-image/route/server-reference-manifest.json +4 -0
  57. package/.next/server/app/api/sessions/upload-image/route.js +7 -0
  58. package/.next/server/app/api/sessions/upload-image/route.js.map +5 -0
  59. package/.next/server/app/api/sessions/upload-image/route.js.nft.json +1 -0
  60. package/.next/server/app/api/sessions/upload-image/route_client-reference-manifest.js +3 -0
  61. package/.next/server/app/api/sql/connections/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sql/mcp/route.js.nft.json +1 -1
  63. package/.next/server/app/api/sql/query/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sql/schema/route.js.nft.json +1 -1
  65. package/.next/server/app/api/telemetry/route/app-paths-manifest.json +3 -0
  66. package/.next/server/app/api/telemetry/route/build-manifest.json +9 -0
  67. package/.next/server/app/api/telemetry/route/server-reference-manifest.json +4 -0
  68. package/.next/server/app/api/telemetry/route.js +6 -0
  69. package/.next/server/app/api/telemetry/route.js.map +5 -0
  70. package/.next/server/app/api/telemetry/route.js.nft.json +1 -0
  71. package/.next/server/app/api/telemetry/route_client-reference-manifest.js +3 -0
  72. package/.next/server/app/index.html +1 -1
  73. package/.next/server/app/index.rsc +3 -3
  74. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  75. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  76. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  77. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  78. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  79. package/.next/server/app/page/react-loadable-manifest.json +5 -5
  80. package/.next/server/app/page_client-reference-manifest.js +1 -1
  81. package/.next/server/app-paths-manifest.json +3 -0
  82. package/.next/server/chunks/[root-of-the-server]__0.ptwor._.js +1 -1
  83. package/.next/server/chunks/[root-of-the-server]__0.ptwor._.js.map +1 -1
  84. package/.next/server/chunks/[root-of-the-server]__00-sqou._.js +15 -0
  85. package/.next/server/chunks/{[root-of-the-server]__0thsozo._.js.map → [root-of-the-server]__00-sqou._.js.map} +1 -1
  86. package/.next/server/chunks/[root-of-the-server]__00l5btc._.js +1 -1
  87. package/.next/server/chunks/[root-of-the-server]__00l5btc._.js.map +1 -1
  88. package/.next/server/chunks/[root-of-the-server]__01621nz._.js +3 -0
  89. package/.next/server/chunks/[root-of-the-server]__01621nz._.js.map +1 -0
  90. package/.next/server/chunks/[root-of-the-server]__06men8g._.js +1 -1
  91. package/.next/server/chunks/[root-of-the-server]__06men8g._.js.map +1 -1
  92. package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js +3 -0
  93. package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js.map +1 -0
  94. package/.next/server/chunks/[root-of-the-server]__097vtn1._.js +15 -0
  95. package/.next/server/chunks/{[root-of-the-server]__0em6a~~._.js.map → [root-of-the-server]__097vtn1._.js.map} +1 -1
  96. package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js +15 -0
  97. package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js.map +1 -0
  98. package/.next/server/chunks/[root-of-the-server]__0co.g5k._.js +1 -1
  99. package/.next/server/chunks/[root-of-the-server]__0co.g5k._.js.map +1 -1
  100. package/.next/server/chunks/[root-of-the-server]__0e03ite._.js +15 -0
  101. package/.next/server/chunks/{[root-of-the-server]__10wwca8._.js.map → [root-of-the-server]__0e03ite._.js.map} +1 -1
  102. package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js +15 -0
  103. package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js.map +1 -0
  104. package/.next/server/chunks/[root-of-the-server]__0h.~hv6._.js +15 -0
  105. package/.next/server/chunks/{[root-of-the-server]__0s0is_9._.js.map → [root-of-the-server]__0h.~hv6._.js.map} +1 -1
  106. package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js +33 -0
  107. package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js.map +1 -0
  108. package/.next/server/chunks/[root-of-the-server]__0s1axek._.js +15 -0
  109. package/.next/server/chunks/{[root-of-the-server]__0i~01cv._.js.map → [root-of-the-server]__0s1axek._.js.map} +1 -1
  110. package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js +15 -0
  111. package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js.map +1 -0
  112. package/.next/server/chunks/[root-of-the-server]__0viil6b._.js +15 -0
  113. package/.next/server/chunks/{[root-of-the-server]__07zx04e._.js.map → [root-of-the-server]__0viil6b._.js.map} +1 -1
  114. package/.next/server/chunks/_next-internal_server_app_api_configs_route_actions_087k_4j.js +3 -0
  115. package/.next/server/chunks/_next-internal_server_app_api_configs_route_actions_087k_4j.js.map +1 -0
  116. package/.next/server/chunks/_next-internal_server_app_api_sessions_upload-image_route_actions_09tfn3a.js +3 -0
  117. package/.next/server/chunks/_next-internal_server_app_api_sessions_upload-image_route_actions_09tfn3a.js.map +1 -0
  118. package/.next/server/chunks/_next-internal_server_app_api_telemetry_route_actions_0.yo5qr.js +3 -0
  119. package/.next/server/chunks/_next-internal_server_app_api_telemetry_route_actions_0.yo5qr.js.map +1 -0
  120. package/.next/server/chunks/ssr/_0t7oqy6._.js +1 -1
  121. package/.next/server/chunks/ssr/_0t7oqy6._.js.map +1 -1
  122. package/.next/server/middleware-build-manifest.js +3 -3
  123. package/.next/server/pages/404.html +1 -1
  124. package/.next/server/pages/500.html +1 -1
  125. package/.next/server/server-reference-manifest.js +1 -1
  126. package/.next/server/server-reference-manifest.json +1 -1
  127. package/.next/static/chunks/07lj0adi0kt5p.css +1 -0
  128. package/.next/static/chunks/0xhaghhxllgzs.js +1 -0
  129. package/.next/static/chunks/128c-kly7grxg.js +4 -0
  130. package/.next/static/chunks/{0-vb7c~yrtpon.js → 15_baizhaiq8c.js} +16 -3
  131. package/.next/static/chunks/15sigqif_v.mu.js +1 -0
  132. package/.next/static/chunks/17ud2wmsyqknc.js +11 -0
  133. package/.next/trace +1 -1
  134. package/.next/trace-build +1 -1
  135. package/.next/types/routes.d.ts +4 -1
  136. package/.next/types/validator.ts +27 -0
  137. package/bin/cli.js +4 -4
  138. package/bin/fix-pwa-ubuntu.sh +50 -0
  139. package/package.json +2 -1
  140. package/src/app/api/configs/route.ts +40 -0
  141. package/src/app/api/sessions/launch/route.ts +3 -3
  142. package/src/app/api/sessions/route.ts +2 -2
  143. package/src/app/api/sessions/start/route.ts +2 -2
  144. package/src/app/api/sessions/upload-image/route.ts +57 -0
  145. package/src/app/api/sql/connections/route.ts +9 -6
  146. package/src/app/api/sql/mcp/route.ts +3 -2
  147. package/src/app/api/sql/query/route.ts +3 -2
  148. package/src/app/api/sql/schema/route.ts +3 -2
  149. package/src/app/api/telemetry/route.ts +18 -0
  150. package/src/app/components/AiderWizard.tsx +46 -2
  151. package/src/app/components/ClaudeWizard.tsx +30 -0
  152. package/src/app/components/IdeLayout.tsx +566 -70
  153. package/src/app/components/TerminalConsole.tsx +368 -11
  154. package/src/app/page.module.css +8 -2
  155. package/src/app/page.tsx +534 -401
  156. package/src/lib/agents/AgentRunner.ts +88 -5
  157. package/src/lib/agents/AiderRunner.ts +4 -18
  158. package/src/lib/agents/ClaudeRunner.ts +16 -26
  159. package/src/lib/agents/DockerRunner.ts +16 -26
  160. package/src/lib/agents/MockRunner.ts +4 -16
  161. package/src/lib/agents/SSHRunner.ts +16 -26
  162. package/src/lib/agents/SessionConfig.ts +79 -0
  163. package/src/lib/agents/SessionManager.ts +16 -16
  164. package/src/lib/agents/SessionPersister.ts +13 -9
  165. package/src/lib/agents/ShellRunner.ts +14 -21
  166. package/src/lib/agents/WebSocketServer.ts +2 -2
  167. package/.next/server/chunks/[root-of-the-server]__01.zh3b._.js +0 -15
  168. package/.next/server/chunks/[root-of-the-server]__01.zh3b._.js.map +0 -1
  169. package/.next/server/chunks/[root-of-the-server]__07zx04e._.js +0 -15
  170. package/.next/server/chunks/[root-of-the-server]__0_o7_jj._.js +0 -15
  171. package/.next/server/chunks/[root-of-the-server]__0_o7_jj._.js.map +0 -1
  172. package/.next/server/chunks/[root-of-the-server]__0d0pykb._.js +0 -33
  173. package/.next/server/chunks/[root-of-the-server]__0d0pykb._.js.map +0 -1
  174. package/.next/server/chunks/[root-of-the-server]__0em6a~~._.js +0 -15
  175. package/.next/server/chunks/[root-of-the-server]__0i~01cv._.js +0 -15
  176. package/.next/server/chunks/[root-of-the-server]__0s0is_9._.js +0 -15
  177. package/.next/server/chunks/[root-of-the-server]__0thsozo._.js +0 -15
  178. package/.next/server/chunks/[root-of-the-server]__10wwca8._.js +0 -15
  179. package/.next/static/chunks/076_b43_kienv.js +0 -1
  180. package/.next/static/chunks/0hoyc~vbuqe0t.css +0 -1
  181. package/.next/static/chunks/0k2co3uj4w81_.js +0 -1
  182. package/.next/static/chunks/0sqeixl35qoac.js +0 -1
  183. package/.next/static/chunks/10pqt4p7e3rk1.js +0 -11
  184. /package/.next/static/{cwv8IVp6ik4mwOjuM24B1 → b48zoI5QUg8m7v1asTiCM}/_buildManifest.js +0 -0
  185. /package/.next/static/{cwv8IVp6ik4mwOjuM24B1 → b48zoI5QUg8m7v1asTiCM}/_clientMiddlewareManifest.js +0 -0
  186. /package/.next/static/{cwv8IVp6ik4mwOjuM24B1 → b48zoI5QUg8m7v1asTiCM}/_ssgManifest.js +0 -0
@@ -1,12 +1,25 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState, useCallback } from "react";
3
+ import { useEffect, useState, useCallback, useMemo } from "react";
4
4
  import dynamic from "next/dynamic";
5
5
  import TerminalConsole from "./TerminalConsole";
6
6
  import DirectoryBrowserModal from "./DirectoryBrowserModal";
7
7
  import MarkdownPreview from "./MarkdownPreview";
8
8
  import DbExplorer from "./DbExplorer";
9
9
 
10
+ interface TokenTransaction {
11
+ timestamp: string;
12
+ sessionId: string;
13
+ sessionName: string;
14
+ type: string;
15
+ inputTokens: number;
16
+ outputTokens: number;
17
+ cost: number;
18
+ totalInput: number;
19
+ totalOutput: number;
20
+ totalCost: number;
21
+ }
22
+
10
23
  const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
11
24
 
12
25
  const getEditorLanguage = (filePath: string): string => {
@@ -42,6 +55,35 @@ const getEditorLanguage = (filePath: string): string => {
42
55
  }
43
56
  };
44
57
 
58
+ interface MergeConflictBlock {
59
+ index: number;
60
+ startIndex: number;
61
+ endIndex: number;
62
+ current: string;
63
+ incoming: string;
64
+ rawText: string;
65
+ }
66
+
67
+ const parseMergeConflicts = (text: string): MergeConflictBlock[] => {
68
+ if (!text) return [];
69
+ const regex = /<<<<<<<[^]*?\r?\n([^]*?)\r?\n=======\r?\n([^]*?)\r?\n>>>>>>>.*/g;
70
+ const conflicts: MergeConflictBlock[] = [];
71
+ let match;
72
+ let index = 0;
73
+ while ((match = regex.exec(text)) !== null) {
74
+ conflicts.push({
75
+ index: index++,
76
+ startIndex: match.index,
77
+ endIndex: match.index + match[0].length,
78
+ current: match[1],
79
+ incoming: match[2],
80
+ rawText: match[0]
81
+ });
82
+ }
83
+ return conflicts;
84
+ };
85
+
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
87
  const handleEditorWillMount = (monaco: any) => {
46
88
  monaco.editor.defineTheme("neural-loom-dark", {
47
89
  base: "vs-dark",
@@ -152,7 +194,7 @@ interface FlatFile {
152
194
  interface RunnerStatus {
153
195
  id: string;
154
196
  name: string;
155
- type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker';
197
+ type: 'mock' | 'claude' | 'claude-docker' | 'claude-ssh' | 'aider' | 'aider-docker' | 'shell';
156
198
  status: 'idle' | 'running' | 'stopped';
157
199
  createdAt: string;
158
200
  logsCount: number;
@@ -250,15 +292,104 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
250
292
  const [editorMode, setEditorMode] = useState<'edit' | 'split' | 'preview'>('edit');
251
293
  const [explorerVisible, setExplorerVisible] = useState(true);
252
294
  const [activeTab, setActiveTab] = useState<"explorer" | "git" | "analytics" | "auditor" | "browser" | "database">("explorer");
253
- const [browserUrlInput, setBrowserUrlInput] = useState("http://localhost:3000");
295
+ const [browserUrlInput, setBrowserUrlInput] = useState(typeof window !== "undefined" ? window.location.origin : "http://localhost:9600");
254
296
  const [isRefreshing, setIsRefreshing] = useState(false);
255
297
 
298
+ // Terminal tab states & handlers
299
+ const [consoleSessionId, setConsoleSessionId] = useState<string>(sessionId);
300
+
301
+ useEffect(() => {
302
+ // eslint-disable-next-line react-hooks/set-state-in-effect
303
+ setConsoleSessionId(sessionId);
304
+ }, [sessionId]);
305
+
306
+ const workspaceShells = sessions.filter(s => s.type === 'shell' && s.id.startsWith(`shell-${sessionId}-`));
307
+
308
+ const handleCreateShell = async () => {
309
+ try {
310
+ const res = await fetch("/api/sessions/launch", {
311
+ method: "POST",
312
+ headers: { "Content-Type": "application/json" },
313
+ body: JSON.stringify({
314
+ type: "shell",
315
+ name: `Shell ${workspaceShells.length + 1}`,
316
+ workspaceRoot: workspaceRoot,
317
+ parentSessionId: sessionId
318
+ })
319
+ });
320
+ if (res.ok) {
321
+ const data = await res.json();
322
+ if (data.success && data.session) {
323
+ if (onRefreshSessions) {
324
+ onRefreshSessions();
325
+ }
326
+ setConsoleSessionId(data.session.id);
327
+ }
328
+ }
329
+ } catch (err) {
330
+ console.error("Failed to launch shell terminal:", err);
331
+ }
332
+ };
333
+
334
+ const handleCloseShell = async (shellId: string, e: React.MouseEvent) => {
335
+ e.stopPropagation();
336
+ if (consoleSessionId === shellId) {
337
+ setConsoleSessionId(sessionId);
338
+ }
339
+ try {
340
+ await fetch("/api/sessions/stop", {
341
+ method: "POST",
342
+ headers: { "Content-Type": "application/json" },
343
+ body: JSON.stringify({ id: shellId, delete: true })
344
+ });
345
+ if (onRefreshSessions) {
346
+ onRefreshSessions();
347
+ }
348
+ } catch (err) {
349
+ console.error("Failed to delete shell:", err);
350
+ }
351
+ };
352
+
256
353
  // Layout sizing states
257
354
  const [sidebarWidth, setSidebarWidth] = useState(300);
258
355
  const [isResizingSidebar, setIsResizingSidebar] = useState(false);
259
356
  const [terminalHeight, setTerminalHeight] = useState(380);
260
357
  const [isResizingTerminal, setIsResizingTerminal] = useState(false);
261
358
 
359
+ // Load layout sizes from localStorage on mount
360
+ useEffect(() => {
361
+ if (typeof window !== "undefined") {
362
+ const savedWidth = localStorage.getItem("neural-loom:sidebar-width");
363
+ if (savedWidth) {
364
+ const w = parseInt(savedWidth, 10);
365
+ if (!isNaN(w) && w > 150 && w < 600) {
366
+ // eslint-disable-next-line react-hooks/set-state-in-effect
367
+ setSidebarWidth(w);
368
+ }
369
+ }
370
+ const savedHeight = localStorage.getItem("neural-loom:terminal-height");
371
+ if (savedHeight) {
372
+ const h = parseInt(savedHeight, 10);
373
+ if (!isNaN(h) && h > 100 && h < window.innerHeight - 150) {
374
+ setTerminalHeight(h);
375
+ }
376
+ }
377
+ }
378
+ }, []);
379
+
380
+ // Save layout sizes when changed
381
+ useEffect(() => {
382
+ if (typeof window !== "undefined" && sidebarWidth !== 300) {
383
+ localStorage.setItem("neural-loom:sidebar-width", sidebarWidth.toString());
384
+ }
385
+ }, [sidebarWidth]);
386
+
387
+ useEffect(() => {
388
+ if (typeof window !== "undefined" && terminalHeight !== 380) {
389
+ localStorage.setItem("neural-loom:terminal-height", terminalHeight.toString());
390
+ }
391
+ }, [terminalHeight]);
392
+
262
393
  const startResizingSidebar = useCallback((e: React.MouseEvent) => {
263
394
  e.preventDefault();
264
395
  setIsResizingSidebar(true);
@@ -334,6 +465,30 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
334
465
  // Log Search & Auditor
335
466
  const [logSearchQuery, setLogSearchQuery] = useState("");
336
467
 
468
+ // Telemetry history
469
+ const [telemetryHistory, setTelemetryHistory] = useState<TokenTransaction[]>([]);
470
+
471
+ const fetchTelemetryHistory = async () => {
472
+ try {
473
+ const res = await fetch("/api/telemetry");
474
+ if (res.ok) {
475
+ const data = await res.json();
476
+ if (data.success && data.history) {
477
+ setTelemetryHistory(data.history);
478
+ }
479
+ }
480
+ } catch (err) {
481
+ console.error("Failed to fetch telemetry history:", err);
482
+ }
483
+ };
484
+
485
+ useEffect(() => {
486
+ if (activeTab === "analytics") {
487
+ // eslint-disable-next-line react-hooks/set-state-in-effect
488
+ fetchTelemetryHistory();
489
+ }
490
+ }, [activeTab]);
491
+
337
492
  // Custom Commands
338
493
  const [customCommands, setCustomCommands] = useState<{ name: string; command: string }[]>([]);
339
494
  const [newCmdName, setNewCmdName] = useState("");
@@ -807,6 +962,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
807
962
  }, [workspaceRoot, scopedDirsStr]);
808
963
 
809
964
  useEffect(() => {
965
+ // eslint-disable-next-line react-hooks/set-state-in-effect
810
966
  loadFiles();
811
967
  const interval = setInterval(() => {
812
968
  loadFiles();
@@ -1738,7 +1894,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1738
1894
  type="text"
1739
1895
  value={browserUrlInput}
1740
1896
  onChange={(e) => setBrowserUrlInput(e.target.value)}
1741
- placeholder="http://localhost:3000"
1897
+ placeholder={typeof window !== "undefined" ? window.location.origin : "http://localhost:9600"}
1742
1898
  style={{
1743
1899
  flex: 1,
1744
1900
  backgroundColor: "#05070a",
@@ -1779,8 +1935,8 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1779
1935
  </label>
1780
1936
  <div style={{ display: "flex", flexDirection: "column", gap: "0.35rem" }}>
1781
1937
  {[
1782
- { label: "Next.js App (3000)", url: "http://localhost:3000" },
1783
- { label: "WebSocket Gateway (3001)", url: "http://localhost:3001" },
1938
+ { label: `Next.js App (${typeof window !== "undefined" ? window.location.port : "9600"})`, url: typeof window !== "undefined" ? window.location.origin : "http://localhost:9600" },
1939
+ { label: `WebSocket Gateway (${wsPort})`, url: `http://localhost:${wsPort}` },
1784
1940
  { label: "Vite Server (5173)", url: "http://localhost:5173" }
1785
1941
  ].map((item, idx) => (
1786
1942
  <div
@@ -1827,10 +1983,11 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1827
1983
  )}
1828
1984
 
1829
1985
  {activeTab === "analytics" && (
1830
- <div style={{ padding: "0.5rem 1rem", display: "flex", flexDirection: "column", gap: "1.25rem" }}>
1986
+ <div style={{ padding: "0.5rem 1rem", display: "flex", flexDirection: "column", gap: "1.25rem", maxHeight: "100%", overflowY: "auto" }}>
1987
+ {/* Session Token Usage */}
1831
1988
  <div>
1832
1989
  <div style={{ fontSize: "0.75rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)", marginBottom: "0.5rem" }}>
1833
- Token Usage
1990
+ Active Session Token Usage
1834
1991
  </div>
1835
1992
  <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.5rem" }}>
1836
1993
  <div style={analyticsCardStyle}>
@@ -1850,6 +2007,137 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1850
2007
  </div>
1851
2008
  </div>
1852
2009
 
2010
+ {/* All-time Accumulations */}
2011
+ <div>
2012
+ <div style={{ fontSize: "0.75rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)", marginBottom: "0.5rem" }}>
2013
+ All-time Cumulative Totals
2014
+ </div>
2015
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.5rem" }}>
2016
+ <div style={analyticsCardStyle}>
2017
+ <div style={analyticsCardLabelStyle}>Total Input</div>
2018
+ <div style={analyticsCardValueStyle}>
2019
+ {telemetryHistory.reduce((sum, item) => sum + (item.inputTokens || 0), 0).toLocaleString()}
2020
+ </div>
2021
+ </div>
2022
+ <div style={analyticsCardStyle}>
2023
+ <div style={analyticsCardLabelStyle}>Total Output</div>
2024
+ <div style={analyticsCardValueStyle}>
2025
+ {telemetryHistory.reduce((sum, item) => sum + (item.outputTokens || 0), 0).toLocaleString()}
2026
+ </div>
2027
+ </div>
2028
+ <div style={{ ...analyticsCardStyle, gridColumn: "span 2" }}>
2029
+ <div style={analyticsCardLabelStyle}>Total Cost</div>
2030
+ <div style={{ ...analyticsCardValueStyle, color: "#10b981", fontSize: "1.25rem" }}>
2031
+ ${telemetryHistory.reduce((sum, item) => sum + (item.cost || 0), 0).toFixed(4)}
2032
+ </div>
2033
+ </div>
2034
+ </div>
2035
+ </div>
2036
+
2037
+ {/* Historical Token Analytics Chart */}
2038
+ {telemetryHistory.length > 0 && (
2039
+ <div>
2040
+ <div style={{ fontSize: "0.75rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)", marginBottom: "0.5rem" }}>
2041
+ Cost & Token History Trend (Last 10 Actions)
2042
+ </div>
2043
+ <div className="card" style={{ padding: "1rem", backgroundColor: "#05070a", border: "1px solid var(--border)", display: "flex", flexDirection: "column", alignItems: "center" }}>
2044
+ <svg width="240" height="110" style={{ overflow: "visible" }}>
2045
+ {(() => {
2046
+ const chartData = telemetryHistory.slice(-10);
2047
+ const maxVal = Math.max(...chartData.map(d => (d.inputTokens || 0) + (d.outputTokens || 0)), 100);
2048
+ const barWidth = 14;
2049
+ const gap = 7;
2050
+ const svgHeight = 90;
2051
+
2052
+ return chartData.map((d, i) => {
2053
+ const inp = d.inputTokens || 0;
2054
+ const out = d.outputTokens || 0;
2055
+
2056
+ const inpHeight = (inp / maxVal) * (svgHeight - 10);
2057
+ const outHeight = (out / maxVal) * (svgHeight - 10);
2058
+
2059
+ const x = i * (barWidth + gap) + 15;
2060
+ const inpY = svgHeight - inpHeight;
2061
+ const outY = inpY - outHeight;
2062
+
2063
+ return (
2064
+ <g key={i}>
2065
+ {/* Input portion (purple) */}
2066
+ <rect
2067
+ x={x}
2068
+ y={inpY}
2069
+ width={barWidth}
2070
+ height={inpHeight}
2071
+ fill="#a855f7"
2072
+ opacity="0.85"
2073
+ rx="1.5"
2074
+ />
2075
+ {/* Output portion (yellow) */}
2076
+ <rect
2077
+ x={x}
2078
+ y={outY}
2079
+ width={barWidth}
2080
+ height={outHeight}
2081
+ fill="#eab308"
2082
+ opacity="0.9"
2083
+ rx="1.5"
2084
+ />
2085
+ <title>{`${d.sessionName || d.type}\nCost: $${(d.cost || 0).toFixed(4)}\nInput: ${inp.toLocaleString()}\nOutput: ${out.toLocaleString()}`}</title>
2086
+ </g>
2087
+ );
2088
+ });
2089
+ })()}
2090
+ <line x1="10" y1="90" x2="230" y2="90" stroke="var(--border)" strokeWidth="1" />
2091
+ </svg>
2092
+ {/* Legend */}
2093
+ <div style={{ display: "flex", gap: "1rem", marginTop: "0.5rem", fontSize: "0.65rem" }}>
2094
+ <span style={{ display: "flex", alignItems: "center", gap: "0.25rem" }}>
2095
+ <span style={{ width: "8px", height: "8px", backgroundColor: "#a855f7", borderRadius: "2px" }}></span> Input
2096
+ </span>
2097
+ <span style={{ display: "flex", alignItems: "center", gap: "0.25rem" }}>
2098
+ <span style={{ width: "8px", height: "8px", backgroundColor: "#eab308", borderRadius: "2px" }}></span> Output
2099
+ </span>
2100
+ </div>
2101
+ </div>
2102
+ </div>
2103
+ )}
2104
+
2105
+ {/* Tabular Invocations Log */}
2106
+ {telemetryHistory.length > 0 && (
2107
+ <div>
2108
+ <div style={{ fontSize: "0.75rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)", marginBottom: "0.5rem" }}>
2109
+ Cumulative Log
2110
+ </div>
2111
+ <div style={{ overflowX: "auto", border: "1px solid var(--border)", borderRadius: "6px", backgroundColor: "#05070a" }}>
2112
+ <table style={{ width: "100%", borderCollapse: "collapse", fontSize: "0.7rem", textAlign: "left" }}>
2113
+ <thead>
2114
+ <tr style={{ borderBottom: "1px solid var(--border)", backgroundColor: "rgba(255,255,255,0.02)" }}>
2115
+ <th style={{ padding: "0.4rem 0.5rem", color: "var(--muted)" }}>Agent</th>
2116
+ <th style={{ padding: "0.4rem 0.5rem", color: "var(--muted)" }}>Tokens</th>
2117
+ <th style={{ padding: "0.4rem 0.5rem", color: "var(--muted)" }}>Cost</th>
2118
+ <th style={{ padding: "0.4rem 0.5rem", color: "var(--muted)" }}>Time</th>
2119
+ </tr>
2120
+ </thead>
2121
+ <tbody>
2122
+ {telemetryHistory.slice(-8).reverse().map((item, index) => (
2123
+ <tr key={index} style={{ borderBottom: "1px solid rgba(255,255,255,0.03)" }}>
2124
+ <td style={{ padding: "0.4rem 0.5rem", fontWeight: 600 }}>{item.sessionName?.substring(0, 12) || item.type}</td>
2125
+ <td style={{ padding: "0.4rem 0.5rem" }}>
2126
+ {((item.inputTokens || 0) + (item.outputTokens || 0)).toLocaleString()}
2127
+ </td>
2128
+ <td style={{ padding: "0.4rem 0.5rem", color: "#10b981" }}>${(item.cost || 0).toFixed(4)}</td>
2129
+ <td style={{ padding: "0.4rem 0.5rem", color: "var(--muted)" }}>
2130
+ {new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
2131
+ </td>
2132
+ </tr>
2133
+ ))}
2134
+ </tbody>
2135
+ </table>
2136
+ </div>
2137
+ </div>
2138
+ )}
2139
+
2140
+ {/* Resource Telemetry */}
1853
2141
  <div>
1854
2142
  <div style={{ fontSize: "0.75rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)", marginBottom: "0.5rem" }}>
1855
2143
  Resource Telemetry
@@ -2090,7 +2378,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
2090
2378
  <span style={{ fontSize: "0.6rem", color: "var(--muted)", backgroundColor: "rgba(255,255,255,0.05)", padding: "1px 4px", borderRadius: "3px" }}>
2091
2379
  {s.type.replace("claude-", "").replace("aider-", "")}
2092
2380
  </span>
2093
- {s.status === 'running' && s.agentState && (
2381
+ {s.status === 'running' && s.agentState && s.type !== 'shell' && (
2094
2382
  <span
2095
2383
  style={{
2096
2384
  fontSize: "0.6rem",
@@ -2215,19 +2503,136 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
2215
2503
 
2216
2504
  {/* Editor contents */}
2217
2505
  <div style={editorWorkspaceStyle}>
2218
- {activeFile ? (
2219
- activeFile.path.startsWith("browser:") ? (
2220
- <BrowserView url={activeFile.content} />
2221
- ) : activeFile.path.startsWith("diff:") ? (
2222
- <DiffView content={activeFile.content} filePath={activeFile.path.substring(5)} />
2223
- ) : isMarkdown ? (
2224
- currentMode === 'preview' ? (
2225
- <div style={{ height: "100%", overflow: "hidden" }}>
2226
- <MarkdownPreview content={activeFile.content} />
2506
+ <div style={{ display: "flex", flexDirection: "column", height: "100%", width: "100%" }}>
2507
+ {activeFile && parseMergeConflicts(activeFile.content).length > 0 && (
2508
+ <div style={{
2509
+ padding: "0.5rem 1rem",
2510
+ backgroundColor: "rgba(220, 38, 38, 0.15)",
2511
+ borderBottom: "1px solid var(--danger)",
2512
+ color: "var(--foreground)",
2513
+ fontSize: "0.8rem",
2514
+ display: "flex",
2515
+ flexDirection: "column",
2516
+ gap: "0.5rem",
2517
+ flexShrink: 0,
2518
+ zIndex: 20
2519
+ }}>
2520
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
2521
+ <span style={{ fontWeight: 600, color: "#f87171" }}>
2522
+ ⚠️ {parseMergeConflicts(activeFile.content).length} Git Merge Conflict(s) Detected in this file!
2523
+ </span>
2524
+ </div>
2525
+ <div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
2526
+ {parseMergeConflicts(activeFile.content).slice(0, 3).map((conflict, idx) => (
2527
+ <div
2528
+ key={idx}
2529
+ style={{
2530
+ backgroundColor: "#05070a",
2531
+ border: "1px solid var(--border)",
2532
+ borderRadius: "4px",
2533
+ padding: "0.4rem 0.6rem",
2534
+ display: "flex",
2535
+ flexDirection: "column",
2536
+ gap: "0.35rem",
2537
+ flex: "1 1 200px",
2538
+ maxWidth: "350px"
2539
+ }}
2540
+ >
2541
+ <div style={{ fontSize: "0.7rem", color: "var(--muted)", fontWeight: 600 }}>Conflict Block #{idx + 1}</div>
2542
+ <div style={{ display: "flex", gap: "0.25rem" }}>
2543
+ <button
2544
+ onClick={() => {
2545
+ const newContent = activeFile.content.replace(conflict.rawText, conflict.current);
2546
+ handleContentChange(newContent);
2547
+ }}
2548
+ style={{
2549
+ padding: "0.2rem 0.4rem",
2550
+ borderRadius: "3px",
2551
+ backgroundColor: "var(--primary)",
2552
+ color: "#fff",
2553
+ border: "none",
2554
+ fontSize: "0.7rem",
2555
+ cursor: "pointer"
2556
+ }}
2557
+ >
2558
+ Accept Current
2559
+ </button>
2560
+ <button
2561
+ onClick={() => {
2562
+ const newContent = activeFile.content.replace(conflict.rawText, conflict.incoming);
2563
+ handleContentChange(newContent);
2564
+ }}
2565
+ style={{
2566
+ padding: "0.2rem 0.4rem",
2567
+ borderRadius: "3px",
2568
+ backgroundColor: "#10b981",
2569
+ color: "#fff",
2570
+ border: "none",
2571
+ fontSize: "0.7rem",
2572
+ cursor: "pointer"
2573
+ }}
2574
+ >
2575
+ Accept Incoming
2576
+ </button>
2577
+ <button
2578
+ onClick={() => {
2579
+ const newContent = activeFile.content.replace(conflict.rawText, conflict.current + "\n" + conflict.incoming);
2580
+ handleContentChange(newContent);
2581
+ }}
2582
+ style={{
2583
+ padding: "0.2rem 0.4rem",
2584
+ borderRadius: "3px",
2585
+ backgroundColor: "transparent",
2586
+ color: "var(--foreground)",
2587
+ border: "1px solid var(--border)",
2588
+ fontSize: "0.7rem",
2589
+ cursor: "pointer"
2590
+ }}
2591
+ >
2592
+ Accept Both
2593
+ </button>
2594
+ </div>
2595
+ </div>
2596
+ ))}
2597
+ {parseMergeConflicts(activeFile.content).length > 3 && (
2598
+ <div style={{ display: "flex", alignItems: "center", fontSize: "0.75rem", color: "var(--muted)" }}>
2599
+ + {parseMergeConflicts(activeFile.content).length - 3} more conflict blocks...
2600
+ </div>
2601
+ )}
2227
2602
  </div>
2228
- ) : currentMode === 'split' ? (
2229
- <div style={{ display: "flex", width: "100%", height: "100%", overflow: "hidden" }}>
2230
- <div style={{ flex: 1, height: "100%" }}>
2603
+ </div>
2604
+ )}
2605
+
2606
+ <div style={{ flex: 1, minHeight: 0, position: "relative", width: "100%", height: "100%" }}>
2607
+ {activeFile ? (
2608
+ activeFile.path.startsWith("browser:") ? (
2609
+ <BrowserView url={activeFile.content} />
2610
+ ) : activeFile.path.startsWith("diff:") ? (
2611
+ <DiffView content={activeFile.content} filePath={activeFile.path.substring(5)} />
2612
+ ) : isMarkdown ? (
2613
+ currentMode === 'preview' ? (
2614
+ <div style={{ height: "100%", overflow: "hidden" }}>
2615
+ <MarkdownPreview content={activeFile.content} />
2616
+ </div>
2617
+ ) : currentMode === 'split' ? (
2618
+ <div style={{ display: "flex", width: "100%", height: "100%", overflow: "hidden" }}>
2619
+ <div style={{ flex: 1, height: "100%" }}>
2620
+ <MonacoEditor
2621
+ height="100%"
2622
+ language="markdown"
2623
+ theme="neural-loom-dark"
2624
+ value={activeFile.content}
2625
+ onChange={(value) => handleContentChange(value || "")}
2626
+ beforeMount={handleEditorWillMount}
2627
+ options={editorOptions}
2628
+ loading={<div style={{ padding: "1rem", color: "var(--muted)" }}>Loading Editor...</div>}
2629
+ />
2630
+ </div>
2631
+ <div style={{ flex: 1, height: "100%", overflow: "hidden" }}>
2632
+ <MarkdownPreview content={activeFile.content} />
2633
+ </div>
2634
+ </div>
2635
+ ) : (
2231
2636
  <MonacoEditor
2232
2637
  height="100%"
2233
2638
  language="markdown"
@@ -2238,47 +2643,33 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
2238
2643
  options={editorOptions}
2239
2644
  loading={<div style={{ padding: "1rem", color: "var(--muted)" }}>Loading Editor...</div>}
2240
2645
  />
2241
- </div>
2242
- <div style={{ flex: 1, height: "100%", overflow: "hidden" }}>
2243
- <MarkdownPreview content={activeFile.content} />
2244
- </div>
2245
- </div>
2646
+ )
2647
+ ) : (
2648
+ <MonacoEditor
2649
+ height="100%"
2650
+ language={getEditorLanguage(activeFile.path)}
2651
+ theme="neural-loom-dark"
2652
+ value={activeFile.content}
2653
+ onChange={(value) => handleContentChange(value || "")}
2654
+ beforeMount={handleEditorWillMount}
2655
+ options={editorOptions}
2656
+ loading={<div style={{ padding: "1rem", color: "var(--muted)" }}>Loading Editor...</div>}
2657
+ />
2658
+ )
2246
2659
  ) : (
2247
- <MonacoEditor
2248
- height="100%"
2249
- language="markdown"
2250
- theme="neural-loom-dark"
2251
- value={activeFile.content}
2252
- onChange={(value) => handleContentChange(value || "")}
2253
- beforeMount={handleEditorWillMount}
2254
- options={editorOptions}
2255
- loading={<div style={{ padding: "1rem", color: "var(--muted)" }}>Loading Editor...</div>}
2256
- />
2257
- )
2258
- ) : (
2259
- <MonacoEditor
2260
- height="100%"
2261
- language={getEditorLanguage(activeFile.path)}
2262
- theme="neural-loom-dark"
2263
- value={activeFile.content}
2264
- onChange={(value) => handleContentChange(value || "")}
2265
- beforeMount={handleEditorWillMount}
2266
- options={editorOptions}
2267
- loading={<div style={{ padding: "1rem", color: "var(--muted)" }}>Loading Editor...</div>}
2268
- />
2269
- )
2270
- ) : (
2271
- <div style={emptyEditorStateStyle}>
2272
- <div style={{ fontSize: "2rem", marginBottom: "1rem" }}>💻</div>
2273
- <h4 style={{ margin: 0, fontWeight: 600 }}>NeuralLoom Workspace Editor</h4>
2274
- <p style={{ margin: "0.5rem 0 0 0", color: "var(--muted)", fontSize: "0.8rem" }}>
2275
- Select files from the left explorer panel to view and edit. Use Ctrl+S to save changes.
2276
- </p>
2277
- <p style={{ margin: "0.5rem 0 0 0", color: "var(--muted)", fontSize: "0.75rem", fontStyle: "italic" }}>
2278
- Press <kbd style={{ background: "rgba(255,255,255,0.1)", padding: "2px 4px", borderRadius: "3px" }}>Ctrl+P</kbd> to search files.
2279
- </p>
2660
+ <div style={emptyEditorStateStyle}>
2661
+ <div style={{ fontSize: "2rem", marginBottom: "1rem" }}>💻</div>
2662
+ <h4 style={{ margin: 0, fontWeight: 600 }}>NeuralLoom Workspace Editor</h4>
2663
+ <p style={{ margin: "0.5rem 0 0 0", color: "var(--muted)", fontSize: "0.8rem" }}>
2664
+ Select files from the left explorer panel to view and edit. Use Ctrl+S to save changes.
2665
+ </p>
2666
+ <p style={{ margin: "0.5rem 0 0 0", color: "var(--muted)", fontSize: "0.75rem", fontStyle: "italic" }}>
2667
+ Press <kbd style={{ background: "rgba(255,255,255,0.1)", padding: "2px 4px", borderRadius: "3px" }}>Ctrl+P</kbd> to search files.
2668
+ </p>
2669
+ </div>
2670
+ )}
2280
2671
  </div>
2281
- )}
2672
+ </div>
2282
2673
  </div>
2283
2674
  </div>
2284
2675
 
@@ -2306,9 +2697,94 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
2306
2697
  <div style={{ ...terminalPanelStyle, flex: "none", height: `${terminalHeight}px` }}>
2307
2698
  {/* Dashboard control headers injected above xterm */}
2308
2699
  <div style={terminalHeaderStyle}>
2309
- <span style={{ display: "flex", alignItems: "center", gap: "0.5rem", fontWeight: 600, fontSize: "0.8rem" }}>
2310
- 🖥️ Session Console Bridge {splitSessionId && `(Split View Active)`}
2311
- </span>
2700
+ <div style={{ display: "flex", alignItems: "flex-end", gap: "0.25rem", height: "100%", overflowX: "auto" }}>
2701
+ <div
2702
+ style={{
2703
+ padding: "0.25rem 0.75rem",
2704
+ backgroundColor: consoleSessionId === sessionId ? "#0f131a" : "transparent",
2705
+ border: "1px solid " + (consoleSessionId === sessionId ? "var(--border-focus)" : "transparent"),
2706
+ borderBottom: consoleSessionId === sessionId ? "1px solid #0f131a" : "none",
2707
+ borderRadius: "4px 4px 0 0",
2708
+ color: consoleSessionId === sessionId ? "var(--foreground)" : "var(--muted)",
2709
+ fontSize: "0.75rem",
2710
+ fontWeight: consoleSessionId === sessionId ? 600 : 500,
2711
+ cursor: "pointer",
2712
+ display: "flex",
2713
+ alignItems: "center",
2714
+ gap: "0.5rem",
2715
+ height: "100%",
2716
+ position: "relative",
2717
+ top: "1px"
2718
+ }}
2719
+ onClick={() => setConsoleSessionId(sessionId)}
2720
+ >
2721
+ 🤖 {activeSession?.name || "Agent PTY"}
2722
+ </div>
2723
+ {workspaceShells.map((shell) => (
2724
+ <div
2725
+ key={shell.id}
2726
+ style={{
2727
+ padding: "0.25rem 0.75rem",
2728
+ backgroundColor: consoleSessionId === shell.id ? "#0f131a" : "transparent",
2729
+ border: "1px solid " + (consoleSessionId === shell.id ? "var(--border-focus)" : "transparent"),
2730
+ borderBottom: consoleSessionId === shell.id ? "1px solid #0f131a" : "none",
2731
+ borderRadius: "4px 4px 0 0",
2732
+ color: consoleSessionId === shell.id ? "var(--foreground)" : "var(--muted)",
2733
+ fontSize: "0.75rem",
2734
+ fontWeight: consoleSessionId === shell.id ? 600 : 500,
2735
+ cursor: "pointer",
2736
+ display: "flex",
2737
+ alignItems: "center",
2738
+ gap: "0.5rem",
2739
+ height: "100%",
2740
+ position: "relative",
2741
+ top: "1px"
2742
+ }}
2743
+ onClick={() => setConsoleSessionId(shell.id)}
2744
+ >
2745
+ 🐚 {shell.name}
2746
+ <button
2747
+ onClick={(e) => handleCloseShell(shell.id, e)}
2748
+ style={{
2749
+ background: "none",
2750
+ border: "none",
2751
+ color: "var(--danger)",
2752
+ fontSize: "0.7rem",
2753
+ cursor: "pointer",
2754
+ padding: "0 0.15rem",
2755
+ display: "flex",
2756
+ alignItems: "center",
2757
+ justifyContent: "center",
2758
+ borderRadius: "3px"
2759
+ }}
2760
+ title="Close terminal tab"
2761
+ >
2762
+
2763
+ </button>
2764
+ </div>
2765
+ ))}
2766
+ <button
2767
+ onClick={handleCreateShell}
2768
+ style={{
2769
+ background: "rgba(255,255,255,0.03)",
2770
+ border: "1px solid var(--border)",
2771
+ borderRadius: "4px",
2772
+ color: "var(--muted)",
2773
+ fontSize: "0.8rem",
2774
+ padding: "0.1rem 0.4rem",
2775
+ cursor: "pointer",
2776
+ display: "flex",
2777
+ alignItems: "center",
2778
+ justifyContent: "center",
2779
+ marginLeft: "0.5rem",
2780
+ height: "20px",
2781
+ alignSelf: "center"
2782
+ }}
2783
+ title="Open new terminal tab"
2784
+ >
2785
+ +
2786
+ </button>
2787
+ </div>
2312
2788
 
2313
2789
  <div style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}>
2314
2790
  {activeSession?.status === "stopped" && (
@@ -2442,7 +2918,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
2442
2918
 
2443
2919
  <div style={{ ...terminalConsoleWrapperStyle, display: "flex", width: "100%" }}>
2444
2920
  <div style={{ flex: 1, height: "100%", borderRight: splitSessionId ? "1px solid var(--border)" : "none", overflow: "hidden" }}>
2445
- <TerminalConsole sessionId={sessionId} height="100%" wsPort={wsPort} />
2921
+ <TerminalConsole sessionId={consoleSessionId} height="100%" wsPort={wsPort} />
2446
2922
  </div>
2447
2923
  {splitSessionId && (
2448
2924
  <div style={{ flex: 1, height: "100%", overflow: "hidden", display: "flex", flexDirection: "column" }}>
@@ -3393,6 +3869,7 @@ function BrowserView({ url }: BrowserViewProps) {
3393
3869
  const [refreshKey, setRefreshKey] = useState(0);
3394
3870
 
3395
3871
  useEffect(() => {
3872
+ // eslint-disable-next-line react-hooks/set-state-in-effect
3396
3873
  setCurrentUrl(url);
3397
3874
  setIframeUrl(url);
3398
3875
  }, [url]);
@@ -3527,17 +4004,21 @@ function BrowserView({ url }: BrowserViewProps) {
3527
4004
  );
3528
4005
  }
3529
4006
 
3530
- interface DiffViewProps {
4007
+ interface ParsedDiffLine {
4008
+ type: 'meta' | 'header' | 'hunk' | 'addition' | 'deletion' | 'normal';
3531
4009
  content: string;
3532
- filePath: string;
4010
+ oldNum: number | null;
4011
+ newNum: number | null;
4012
+ raw: string;
3533
4013
  }
3534
4014
 
3535
- function DiffView({ content, filePath }: DiffViewProps) {
3536
- const lines = content.split(/\r?\n/);
3537
-
4015
+ function parseDiffLines(lines: string[]): {
4016
+ parsedLines: ParsedDiffLine[];
4017
+ additions: number;
4018
+ deletions: number;
4019
+ } {
3538
4020
  let oldLineNum = 0;
3539
4021
  let newLineNum = 0;
3540
-
3541
4022
  let additions = 0;
3542
4023
  let deletions = 0;
3543
4024
 
@@ -3587,6 +4068,21 @@ function DiffView({ content, filePath }: DiffViewProps) {
3587
4068
  raw: line
3588
4069
  };
3589
4070
  });
4071
+
4072
+ return { parsedLines, additions, deletions };
4073
+ }
4074
+
4075
+ interface DiffViewProps {
4076
+ content: string;
4077
+ filePath: string;
4078
+ }
4079
+
4080
+ function DiffView({ content, filePath }: DiffViewProps) {
4081
+ const lines = content.split(/\r?\n/);
4082
+
4083
+ const { parsedLines, additions, deletions } = useMemo(() => {
4084
+ return parseDiffLines(lines);
4085
+ }, [lines]);
3590
4086
 
3591
4087
  return (
3592
4088
  <div style={{ display: "flex", flexDirection: "column", height: "100%", backgroundColor: "#0b101b" }}>