neural-loom 0.2.19 → 0.2.41

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 (226) hide show
  1. package/.next/build-manifest.json +3 -3
  2. package/.next/cache/.previewinfo +1 -1
  3. package/.next/cache/.rscinfo +1 -1
  4. package/.next/cache/.tsbuildinfo +1 -1
  5. package/.next/diagnostics/build-diagnostics.json +1 -1
  6. package/.next/fallback-build-manifest.json +3 -3
  7. package/.next/server/app/_global-error/page.js +6 -7
  8. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/server/app/_not-found/page.js +6 -7
  10. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/server/app/api/context/aider/route.js +2 -2
  12. package/.next/server/app/api/context/aider/route.js.nft.json +1 -1
  13. package/.next/server/app/api/context/claude/route.js +2 -2
  14. package/.next/server/app/api/context/claude/route.js.nft.json +1 -1
  15. package/.next/server/app/api/context/route.js +2 -2
  16. package/.next/server/app/api/context/route.js.nft.json +1 -1
  17. package/.next/server/app/api/context/ssh/route.js +2 -2
  18. package/.next/server/app/api/context/ssh/route.js.nft.json +1 -1
  19. package/.next/server/app/api/files/route.js +2 -2
  20. package/.next/server/app/api/files/route.js.nft.json +1 -1
  21. package/.next/server/app/api/git/route.js +2 -2
  22. package/.next/server/app/api/git/route.js.nft.json +1 -1
  23. package/.next/server/app/api/recents/route.js +2 -2
  24. package/.next/server/app/api/recents/route.js.nft.json +1 -1
  25. package/.next/server/app/api/sessions/inject/route.js +2 -2
  26. package/.next/server/app/api/sessions/inject/route.js.nft.json +1 -1
  27. package/.next/server/app/api/sessions/input/route.js +2 -2
  28. package/.next/server/app/api/sessions/input/route.js.nft.json +1 -1
  29. package/.next/server/app/api/sessions/launch/route.js +2 -2
  30. package/.next/server/app/api/sessions/launch/route.js.nft.json +1 -1
  31. package/.next/server/app/api/sessions/route.js +2 -2
  32. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  33. package/.next/server/app/api/sessions/scope/route.js +2 -2
  34. package/.next/server/app/api/sessions/scope/route.js.nft.json +1 -1
  35. package/.next/server/app/api/sessions/start/route.js +2 -2
  36. package/.next/server/app/api/sessions/start/route.js.nft.json +1 -1
  37. package/.next/server/app/api/sessions/stats/route.js +2 -2
  38. package/.next/server/app/api/sessions/stats/route.js.nft.json +1 -1
  39. package/.next/server/app/api/sessions/stop/route.js +2 -2
  40. package/.next/server/app/api/sessions/stop/route.js.nft.json +1 -1
  41. package/.next/server/app/page/react-loadable-manifest.json +4 -4
  42. package/.next/server/app/page.js +6 -7
  43. package/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/server/app-paths-manifest.json +0 -1
  46. package/.next/server/chunks/{[externals]_next_dist_0arv.vj._.js → [externals]__048y_op._.js} +2 -2
  47. package/.next/server/chunks/[root-of-the-server]__005xmeg._.js +15 -0
  48. package/.next/server/chunks/{[root-of-the-server]__0iqaxhx._.js.map → [root-of-the-server]__005xmeg._.js.map} +1 -1
  49. package/.next/server/chunks/{[root-of-the-server]__0k6w___._.js → [root-of-the-server]__0656ruo._.js} +3 -3
  50. package/.next/server/chunks/{[root-of-the-server]__0k6w___._.js.map → [root-of-the-server]__0656ruo._.js.map} +1 -1
  51. package/.next/server/chunks/[root-of-the-server]__0969ozy._.js +3 -0
  52. package/.next/server/chunks/{[root-of-the-server]__0l7c-gn._.js.map → [root-of-the-server]__0969ozy._.js.map} +1 -1
  53. package/.next/server/chunks/[root-of-the-server]__0_~zjnf._.js +15 -0
  54. package/.next/server/chunks/{[root-of-the-server]__0qz~w0~._.js.map → [root-of-the-server]__0_~zjnf._.js.map} +1 -1
  55. package/.next/server/chunks/[root-of-the-server]__0d0pykb._.js +9 -9
  56. package/.next/server/chunks/[root-of-the-server]__0d0pykb._.js.map +1 -1
  57. package/.next/server/chunks/[root-of-the-server]__0hl8jxx._.js +15 -0
  58. package/.next/server/chunks/{[root-of-the-server]__0bi3d5z._.js.map → [root-of-the-server]__0hl8jxx._.js.map} +1 -1
  59. package/.next/server/chunks/[root-of-the-server]__0ho7s6e._.js +15 -0
  60. package/.next/server/chunks/{[root-of-the-server]__0-lisav._.js.map → [root-of-the-server]__0ho7s6e._.js.map} +1 -1
  61. package/.next/server/chunks/{[root-of-the-server]__0j8-xkl._.js → [root-of-the-server]__0i83~95._.js} +2 -2
  62. package/.next/server/chunks/{[root-of-the-server]__0j8-xkl._.js.map → [root-of-the-server]__0i83~95._.js.map} +1 -1
  63. package/.next/server/chunks/[root-of-the-server]__0kn5tap._.js +4 -0
  64. package/.next/server/chunks/{[root-of-the-server]__0ntt3om._.js.map → [root-of-the-server]__0kn5tap._.js.map} +1 -1
  65. package/.next/server/chunks/[root-of-the-server]__0lh_jn-._.js +15 -0
  66. package/.next/server/chunks/{[root-of-the-server]__0__9rdk._.js.map → [root-of-the-server]__0lh_jn-._.js.map} +1 -1
  67. package/.next/server/chunks/[root-of-the-server]__0mq3k0u._.js +15 -0
  68. package/.next/server/chunks/{[root-of-the-server]__0_2_i_q._.js.map → [root-of-the-server]__0mq3k0u._.js.map} +1 -1
  69. package/.next/server/chunks/[root-of-the-server]__0n-zban._.js +3 -0
  70. package/.next/server/chunks/{[root-of-the-server]__0p~u94k._.js.map → [root-of-the-server]__0n-zban._.js.map} +1 -1
  71. package/.next/server/chunks/[root-of-the-server]__0rr36fm._.js +15 -0
  72. package/.next/server/chunks/{[root-of-the-server]__0m91xif._.js.map → [root-of-the-server]__0rr36fm._.js.map} +1 -1
  73. package/.next/server/chunks/[root-of-the-server]__0sa_gpm._.js +3 -0
  74. package/.next/server/chunks/[root-of-the-server]__0sa_gpm._.js.map +1 -0
  75. package/.next/server/chunks/[root-of-the-server]__0vp8-ka._.js +3 -0
  76. package/.next/server/chunks/{[root-of-the-server]__0qyrt6_._.js.map → [root-of-the-server]__0vp8-ka._.js.map} +1 -1
  77. package/.next/server/chunks/[root-of-the-server]__0xcrc-1._.js +15 -0
  78. package/.next/server/chunks/[root-of-the-server]__0xcrc-1._.js.map +1 -0
  79. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js +1 -1
  80. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js.map +1 -1
  81. package/.next/server/chunks/ssr/[root-of-the-server]__0cbmrds._.js +3 -0
  82. package/.next/server/chunks/ssr/[root-of-the-server]__0cbmrds._.js.map +1 -0
  83. package/.next/server/chunks/ssr/[root-of-the-server]__0ni-l5l._.js +3 -0
  84. package/.next/server/chunks/ssr/[root-of-the-server]__0ni-l5l._.js.map +1 -0
  85. package/.next/server/chunks/ssr/{[root-of-the-server]__089dc4i._.js → [root-of-the-server]__0tcyn68._.js} +2 -2
  86. package/.next/server/chunks/ssr/[root-of-the-server]__0tcyn68._.js.map +1 -0
  87. package/.next/server/chunks/ssr/[root-of-the-server]__0v73tbn._.js +1 -1
  88. package/.next/server/chunks/ssr/[root-of-the-server]__0v73tbn._.js.map +1 -1
  89. package/.next/server/chunks/ssr/[root-of-the-server]__0yj_dfr._.js +3 -0
  90. package/.next/server/chunks/ssr/[root-of-the-server]__0yj_dfr._.js.map +1 -0
  91. package/.next/server/chunks/ssr/_0t7oqy6._.js +1 -1
  92. package/.next/server/chunks/ssr/_0t7oqy6._.js.map +1 -1
  93. package/.next/server/chunks/ssr/{node_modules_next_dist_0ppctuh._.js → node_modules_next_dist_0-g.jog._.js} +2 -2
  94. package/.next/server/chunks/ssr/node_modules_next_dist_0-g.jog._.js.map +1 -0
  95. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0bj8dkg.js +4 -0
  96. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0bj8dkg.js.map +1 -0
  97. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0cvrgh7.js +4 -0
  98. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0cvrgh7.js.map +1 -0
  99. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0rc3ul_.js +4 -0
  100. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0rc3ul_.js.map +1 -0
  101. package/.next/server/middleware-build-manifest.js +3 -3
  102. package/.next/server/pages-manifest.json +1 -4
  103. package/.next/server/server-reference-manifest.js +1 -1
  104. package/.next/server/server-reference-manifest.json +1 -1
  105. package/.next/static/ZtSBLhTiGqKpNetcymWiC/_ssgManifest.js +1 -0
  106. package/.next/static/chunks/{0x__nd4avi0fg.js → 070hz362~_h8n.js} +1 -1
  107. package/.next/static/chunks/0bclk7hf35ac4.js +1 -0
  108. package/.next/static/chunks/0kkoif8x_kxrq.js +1 -0
  109. package/.next/static/chunks/{0pp.lp89pntpv.css → 0m-_sw1425nkp.css} +1 -1
  110. package/.next/static/chunks/17jifiroq0_q..js +1 -0
  111. package/package.json +1 -1
  112. package/public/logo.png +0 -0
  113. package/public/manifest.json +22 -0
  114. package/public/sw.js +12 -0
  115. package/src/app/api/context/aider/route.ts +6 -1
  116. package/src/app/api/files/route.ts +20 -8
  117. package/src/app/api/git/route.ts +117 -1
  118. package/src/app/api/sessions/route.ts +4 -1
  119. package/src/app/components/AiderWizard.tsx +19 -1
  120. package/src/app/components/IdeLayout.tsx +1215 -112
  121. package/src/app/components/MarkdownPreview.tsx +528 -0
  122. package/src/app/components/TerminalConsole.tsx +2 -2
  123. package/src/app/layout.tsx +5 -0
  124. package/src/app/page.module.css +2 -0
  125. package/src/app/page.tsx +42 -19
  126. package/src/lib/agents/AiderRunner.ts +83 -44
  127. package/src/lib/agents/ClaudeRunner.ts +43 -18
  128. package/src/lib/agents/DockerRunner.ts +1 -3
  129. package/src/lib/agents/SSHRunner.ts +1 -3
  130. package/src/lib/agents/SessionManager.ts +18 -9
  131. package/src/lib/agents/SessionPersister.ts +10 -0
  132. package/src/lib/agents/ShellRunner.ts +1 -3
  133. package/src/lib/agents/WebSocketServer.ts +20 -13
  134. package/.next/BUILD_ID +0 -1
  135. package/.next/app-path-routes-manifest.json +0 -21
  136. package/.next/diagnostics/route-bundle-stats.json +0 -25
  137. package/.next/export-marker.json +0 -6
  138. package/.next/images-manifest.json +0 -68
  139. package/.next/prerender-manifest.json +0 -114
  140. package/.next/required-server-files.js +0 -335
  141. package/.next/required-server-files.json +0 -335
  142. package/.next/routes-manifest.json +0 -159
  143. package/.next/server/app/_global-error.html +0 -1
  144. package/.next/server/app/_global-error.meta +0 -15
  145. package/.next/server/app/_global-error.rsc +0 -15
  146. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +0 -5
  147. package/.next/server/app/_global-error.segments/_full.segment.rsc +0 -15
  148. package/.next/server/app/_global-error.segments/_head.segment.rsc +0 -6
  149. package/.next/server/app/_global-error.segments/_index.segment.rsc +0 -5
  150. package/.next/server/app/_global-error.segments/_tree.segment.rsc +0 -1
  151. package/.next/server/app/_not-found.html +0 -1
  152. package/.next/server/app/_not-found.meta +0 -16
  153. package/.next/server/app/_not-found.rsc +0 -16
  154. package/.next/server/app/_not-found.segments/_full.segment.rsc +0 -16
  155. package/.next/server/app/_not-found.segments/_head.segment.rsc +0 -6
  156. package/.next/server/app/_not-found.segments/_index.segment.rsc +0 -5
  157. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +0 -5
  158. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +0 -5
  159. package/.next/server/app/_not-found.segments/_tree.segment.rsc +0 -2
  160. package/.next/server/app/favicon.ico/route/app-paths-manifest.json +0 -3
  161. package/.next/server/app/favicon.ico/route/build-manifest.json +0 -9
  162. package/.next/server/app/favicon.ico/route.js +0 -7
  163. package/.next/server/app/favicon.ico/route.js.map +0 -5
  164. package/.next/server/app/favicon.ico/route.js.nft.json +0 -1
  165. package/.next/server/app/favicon.ico.body +0 -0
  166. package/.next/server/app/favicon.ico.meta +0 -1
  167. package/.next/server/app/index.html +0 -1
  168. package/.next/server/app/index.meta +0 -14
  169. package/.next/server/app/index.rsc +0 -21
  170. package/.next/server/app/index.segments/__PAGE__.segment.rsc +0 -10
  171. package/.next/server/app/index.segments/_full.segment.rsc +0 -21
  172. package/.next/server/app/index.segments/_head.segment.rsc +0 -6
  173. package/.next/server/app/index.segments/_index.segment.rsc +0 -5
  174. package/.next/server/app/index.segments/_tree.segment.rsc +0 -5
  175. package/.next/server/chunks/[externals]__1334_78._.js +0 -3
  176. package/.next/server/chunks/[externals]_next_dist_0arv.vj._.js.map +0 -1
  177. package/.next/server/chunks/[root-of-the-server]__0-lisav._.js +0 -15
  178. package/.next/server/chunks/[root-of-the-server]__0_2_i_q._.js +0 -15
  179. package/.next/server/chunks/[root-of-the-server]__0__9rdk._.js +0 -15
  180. package/.next/server/chunks/[root-of-the-server]__0bi3d5z._.js +0 -15
  181. package/.next/server/chunks/[root-of-the-server]__0iqaxhx._.js +0 -15
  182. package/.next/server/chunks/[root-of-the-server]__0l7c-gn._.js +0 -3
  183. package/.next/server/chunks/[root-of-the-server]__0m91xif._.js +0 -15
  184. package/.next/server/chunks/[root-of-the-server]__0ntt3om._.js +0 -3
  185. package/.next/server/chunks/[root-of-the-server]__0p~u94k._.js +0 -3
  186. package/.next/server/chunks/[root-of-the-server]__0qyrt6_._.js +0 -3
  187. package/.next/server/chunks/[root-of-the-server]__0qz~w0~._.js +0 -15
  188. package/.next/server/chunks/[root-of-the-server]__0ty8hwz._.js +0 -15
  189. package/.next/server/chunks/[root-of-the-server]__0ty8hwz._.js.map +0 -1
  190. package/.next/server/chunks/[root-of-the-server]__0ylcu31._.js +0 -3
  191. package/.next/server/chunks/[root-of-the-server]__0ylcu31._.js.map +0 -1
  192. package/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_095lj93.js +0 -3
  193. package/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_095lj93.js.map +0 -1
  194. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0m.429v.js +0 -3
  195. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0m.429v.js.map +0 -1
  196. package/.next/server/chunks/ssr/[root-of-the-server]__089dc4i._.js.map +0 -1
  197. package/.next/server/chunks/ssr/[root-of-the-server]__0_7i5h0._.js +0 -3
  198. package/.next/server/chunks/ssr/[root-of-the-server]__0_7i5h0._.js.map +0 -1
  199. package/.next/server/chunks/ssr/[root-of-the-server]__0hw~y-4._.js +0 -3
  200. package/.next/server/chunks/ssr/[root-of-the-server]__0hw~y-4._.js.map +0 -1
  201. package/.next/server/chunks/ssr/[root-of-the-server]__0~y8ue.._.js +0 -3
  202. package/.next/server/chunks/ssr/[root-of-the-server]__0~y8ue.._.js.map +0 -1
  203. package/.next/server/chunks/ssr/node_modules_05l396c._.js +0 -3
  204. package/.next/server/chunks/ssr/node_modules_05l396c._.js.map +0 -1
  205. package/.next/server/chunks/ssr/node_modules_next_dist_0ppctuh._.js.map +0 -1
  206. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03-z2qq.js +0 -4
  207. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03-z2qq.js.map +0 -1
  208. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_07vh7rm.js +0 -4
  209. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_07vh7rm.js.map +0 -1
  210. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0qp4u6g.js +0 -4
  211. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0qp4u6g.js.map +0 -1
  212. package/.next/server/functions-config-manifest.json +0 -4
  213. package/.next/server/pages/404.html +0 -1
  214. package/.next/server/pages/500.html +0 -1
  215. package/.next/server/prefetch-hints.json +0 -1
  216. package/.next/static/W3UxhE_D02EcHxTzdXKHX/_ssgManifest.js +0 -1
  217. package/.next/static/chunks/01.1lqu6kmllv.js +0 -1
  218. package/.next/static/chunks/0ek0280331wie.js +0 -1
  219. package/.next/static/chunks/0jxar8nsw_py3.js +0 -1
  220. package/.next/static/media/favicon.0x3dzn~oxb6tn.ico +0 -0
  221. package/.next/trace +0 -1
  222. package/.next/trace-build +0 -1
  223. package/src/app/favicon.ico +0 -0
  224. /package/.next/server/chunks/{[externals]__1334_78._.js.map → [externals]__048y_op._.js.map} +0 -0
  225. /package/.next/static/{W3UxhE_D02EcHxTzdXKHX → ZtSBLhTiGqKpNetcymWiC}/_buildManifest.js +0 -0
  226. /package/.next/static/{W3UxhE_D02EcHxTzdXKHX → ZtSBLhTiGqKpNetcymWiC}/_clientMiddlewareManifest.js +0 -0
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useState, useCallback } from "react";
4
4
  import TerminalConsole from "./TerminalConsole";
5
5
  import DirectoryBrowserModal from "./DirectoryBrowserModal";
6
+ import MarkdownPreview from "./MarkdownPreview";
6
7
 
7
8
  interface FileNode {
8
9
  name: string;
@@ -28,6 +29,8 @@ interface GitFile {
28
29
  relativePath: string;
29
30
  absolutePath: string;
30
31
  name: string;
32
+ indexStatus?: string;
33
+ worktreeStatus?: string;
31
34
  }
32
35
 
33
36
  interface SessionStatus {
@@ -151,14 +154,79 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
151
154
  setSessionActiveFiles(prev => ({ ...prev, [sessionId]: path }));
152
155
  }, [sessionId]);
153
156
 
157
+ const [editorMode, setEditorMode] = useState<'edit' | 'split' | 'preview'>('edit');
154
158
  const [explorerVisible, setExplorerVisible] = useState(true);
155
- const [activeTab, setActiveTab] = useState<"explorer" | "git" | "analytics" | "auditor">("explorer");
159
+ const [activeTab, setActiveTab] = useState<"explorer" | "git" | "analytics" | "auditor" | "browser">("explorer");
160
+ const [browserUrlInput, setBrowserUrlInput] = useState("http://localhost:3000");
156
161
  const [isRefreshing, setIsRefreshing] = useState(false);
157
162
 
163
+ // Layout sizing states
164
+ const [sidebarWidth, setSidebarWidth] = useState(300);
165
+ const [isResizingSidebar, setIsResizingSidebar] = useState(false);
166
+ const [terminalHeight, setTerminalHeight] = useState(380);
167
+ const [isResizingTerminal, setIsResizingTerminal] = useState(false);
168
+
169
+ const startResizingSidebar = useCallback((e: React.MouseEvent) => {
170
+ e.preventDefault();
171
+ setIsResizingSidebar(true);
172
+ }, []);
173
+
174
+ const startResizingTerminal = useCallback((e: React.MouseEvent) => {
175
+ e.preventDefault();
176
+ setIsResizingTerminal(true);
177
+ }, []);
178
+
179
+ useEffect(() => {
180
+ const handleMouseMove = (e: MouseEvent) => {
181
+ if (isResizingSidebar) {
182
+ const container = document.getElementById("loom-ide-container");
183
+ if (container) {
184
+ const rect = container.getBoundingClientRect();
185
+ const newWidth = e.clientX - rect.left - 50; // 50px is the activity bar width
186
+ if (newWidth > 150 && newWidth < 600) {
187
+ setSidebarWidth(newWidth);
188
+ }
189
+ }
190
+ }
191
+ if (isResizingTerminal) {
192
+ const container = document.getElementById("loom-ide-container");
193
+ if (container) {
194
+ const rect = container.getBoundingClientRect();
195
+ const newHeight = rect.bottom - e.clientY;
196
+ if (newHeight > 100 && newHeight < rect.height - 150) {
197
+ setTerminalHeight(newHeight);
198
+ }
199
+ }
200
+ }
201
+ };
202
+
203
+ const handleMouseUp = () => {
204
+ setIsResizingSidebar(false);
205
+ setIsResizingTerminal(false);
206
+ };
207
+
208
+ if (isResizingSidebar || isResizingTerminal) {
209
+ window.addEventListener("mousemove", handleMouseMove);
210
+ window.addEventListener("mouseup", handleMouseUp);
211
+ document.body.style.cursor = isResizingSidebar ? "col-resize" : "row-resize";
212
+ document.body.style.userSelect = "none";
213
+ }
214
+
215
+ return () => {
216
+ window.removeEventListener("mousemove", handleMouseMove);
217
+ window.removeEventListener("mouseup", handleMouseUp);
218
+ document.body.style.cursor = "";
219
+ document.body.style.userSelect = "";
220
+ };
221
+ }, [isResizingSidebar, isResizingTerminal]);
222
+
158
223
  // Git Diff Panel state
159
224
  const [gitFiles, setGitFiles] = useState<GitFile[]>([]);
160
225
  const [isGitRepo, setIsGitRepo] = useState(false);
161
226
  const [gitLoading, setGitLoading] = useState(false);
227
+ const [gitBranch, setGitBranch] = useState("");
228
+ const [gitSync, setGitSync] = useState({ ahead: 0, behind: 0 });
229
+ const [commitMessage, setCommitMessage] = useState("");
162
230
 
163
231
  // Status & Telemetry
164
232
  const [sessionStatus, setSessionStatus] = useState<SessionStatus | null>(null);
@@ -187,6 +255,8 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
187
255
  if (data.success) {
188
256
  setIsGitRepo(data.isGit);
189
257
  setGitFiles(data.files || []);
258
+ setGitBranch(data.branch || "");
259
+ setGitSync({ ahead: data.ahead || 0, behind: data.behind || 0 });
190
260
  }
191
261
  }
192
262
  } catch (e) {
@@ -204,6 +274,185 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
204
274
  return () => clearInterval(id);
205
275
  }, [loadGitStatus]);
206
276
 
277
+ const handleGitStage = async (file: string) => {
278
+ try {
279
+ const res = await fetch("/api/git", {
280
+ method: "POST",
281
+ headers: { "Content-Type": "application/json" },
282
+ body: JSON.stringify({ action: "stage", file, rootPath: workspaceRoot })
283
+ });
284
+ if (res.ok) {
285
+ await loadGitStatus();
286
+ }
287
+ } catch (e) {
288
+ console.error(e);
289
+ }
290
+ };
291
+
292
+ const handleGitIgnore = async (file: string) => {
293
+ try {
294
+ const res = await fetch("/api/git", {
295
+ method: "POST",
296
+ headers: { "Content-Type": "application/json" },
297
+ body: JSON.stringify({ action: "ignore", file, rootPath: workspaceRoot })
298
+ });
299
+ if (res.ok) {
300
+ await loadGitStatus();
301
+ }
302
+ } catch (e) {
303
+ console.error(e);
304
+ }
305
+ };
306
+
307
+ const handleGitDiscard = async (file: string) => {
308
+ if (!confirm(`Are you sure you want to discard changes in "${file}"? This action cannot be undone.`)) {
309
+ return;
310
+ }
311
+ try {
312
+ const res = await fetch("/api/git", {
313
+ method: "POST",
314
+ headers: { "Content-Type": "application/json" },
315
+ body: JSON.stringify({ action: "discard", file, rootPath: workspaceRoot })
316
+ });
317
+ if (res.ok) {
318
+ await loadGitStatus();
319
+ }
320
+ } catch (e) {
321
+ console.error(e);
322
+ }
323
+ };
324
+
325
+ const handleGitStageAll = async () => {
326
+ try {
327
+ const res = await fetch("/api/git", {
328
+ method: "POST",
329
+ headers: { "Content-Type": "application/json" },
330
+ body: JSON.stringify({ action: "stage", rootPath: workspaceRoot })
331
+ });
332
+ if (res.ok) {
333
+ await loadGitStatus();
334
+ }
335
+ } catch (e) {
336
+ console.error(e);
337
+ }
338
+ };
339
+
340
+ const handleGitUnstage = async (file: string) => {
341
+ try {
342
+ const res = await fetch("/api/git", {
343
+ method: "POST",
344
+ headers: { "Content-Type": "application/json" },
345
+ body: JSON.stringify({ action: "unstage", file, rootPath: workspaceRoot })
346
+ });
347
+ if (res.ok) {
348
+ await loadGitStatus();
349
+ }
350
+ } catch (e) {
351
+ console.error(e);
352
+ }
353
+ };
354
+
355
+ const handleGitUnstageAll = async () => {
356
+ try {
357
+ const res = await fetch("/api/git", {
358
+ method: "POST",
359
+ headers: { "Content-Type": "application/json" },
360
+ body: JSON.stringify({ action: "unstage", rootPath: workspaceRoot })
361
+ });
362
+ if (res.ok) {
363
+ await loadGitStatus();
364
+ }
365
+ } catch (e) {
366
+ console.error(e);
367
+ }
368
+ };
369
+
370
+ const handleGitCommit = async () => {
371
+ if (!commitMessage.trim()) return;
372
+ try {
373
+ const res = await fetch("/api/git", {
374
+ method: "POST",
375
+ headers: { "Content-Type": "application/json" },
376
+ body: JSON.stringify({ action: "commit", message: commitMessage, rootPath: workspaceRoot })
377
+ });
378
+ if (res.ok) {
379
+ setCommitMessage("");
380
+ await loadGitStatus();
381
+ } else {
382
+ const data = await res.json();
383
+ alert(data.error || "Git commit failed");
384
+ }
385
+ } catch (e) {
386
+ console.error(e);
387
+ }
388
+ };
389
+
390
+ const handleGitPull = async () => {
391
+ try {
392
+ const res = await fetch("/api/git", {
393
+ method: "POST",
394
+ headers: { "Content-Type": "application/json" },
395
+ body: JSON.stringify({ action: "pull", rootPath: workspaceRoot })
396
+ });
397
+ if (res.ok) {
398
+ const data = await res.json();
399
+ alert(data.output || "Git pull completed successfully.");
400
+ await loadGitStatus();
401
+ } else {
402
+ const data = await res.json();
403
+ alert(data.error || "Git pull failed");
404
+ }
405
+ } catch (e) {
406
+ console.error(e);
407
+ }
408
+ };
409
+
410
+ const handleGitPush = async () => {
411
+ try {
412
+ const res = await fetch("/api/git", {
413
+ method: "POST",
414
+ headers: { "Content-Type": "application/json" },
415
+ body: JSON.stringify({ action: "push", rootPath: workspaceRoot })
416
+ });
417
+ if (res.ok) {
418
+ const data = await res.json();
419
+ alert(data.output || "Git push completed successfully.");
420
+ await loadGitStatus();
421
+ } else {
422
+ const data = await res.json();
423
+ alert(data.error || "Git push failed");
424
+ }
425
+ } catch (e) {
426
+ console.error(e);
427
+ }
428
+ };
429
+
430
+ const handleSelectGitFile = async (file: GitFile) => {
431
+ try {
432
+ const res = await fetch(`/api/git?action=diff&file=${encodeURIComponent(file.relativePath)}&root=${encodeURIComponent(workspaceRoot)}`);
433
+ if (res.ok) {
434
+ const data = await res.json();
435
+ if (data.success) {
436
+ const diffPath = `diff:${file.relativePath}`;
437
+ if (!openFiles.some(f => f.path === diffPath)) {
438
+ setOpenFiles(prev => [
439
+ ...prev,
440
+ {
441
+ name: `Diff: ${file.name}`,
442
+ path: diffPath,
443
+ content: data.diff,
444
+ isModified: false
445
+ }
446
+ ]);
447
+ }
448
+ setActiveFilePath(diffPath);
449
+ }
450
+ }
451
+ } catch (e) {
452
+ console.error(e);
453
+ }
454
+ };
455
+
207
456
  // Poll Session Status (logs, tokens, type)
208
457
  useEffect(() => {
209
458
  const fetchStatus = async () => {
@@ -335,9 +584,11 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
335
584
  }, [workspaceRoot, scopedDirsStr]);
336
585
 
337
586
  useEffect(() => {
338
- setTimeout(() => {
587
+ loadFiles();
588
+ const interval = setInterval(() => {
339
589
  loadFiles();
340
- }, 0);
590
+ }, 3000);
591
+ return () => clearInterval(interval);
341
592
  }, [loadFiles]);
342
593
 
343
594
  // Trigger creation dialog
@@ -556,7 +807,31 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
556
807
  }
557
808
  };
558
809
 
810
+ const handleOpenBrowserTab = (url: string) => {
811
+ const formattedUrl = url.trim();
812
+ if (!formattedUrl) return;
813
+ const browserPath = `browser:${formattedUrl}`;
814
+ const alreadyOpen = openFiles.find(f => f.path === browserPath);
815
+ if (!alreadyOpen) {
816
+ setOpenFiles(prev => [
817
+ ...prev,
818
+ {
819
+ name: `Browser: ${formattedUrl.replace(/^https?:\/\//, "")}`,
820
+ path: browserPath,
821
+ content: formattedUrl,
822
+ isModified: false,
823
+ }
824
+ ]);
825
+ }
826
+ setActiveFilePath(browserPath);
827
+ };
828
+
559
829
  const activeFile = openFiles.find((f) => f.path === activeFilePath);
830
+ const isMarkdown = activeFile ? activeFile.path.toLowerCase().endsWith(".md") : false;
831
+ const currentMode = isMarkdown ? editorMode : "edit";
832
+
833
+ const stagedFiles = gitFiles.filter(f => f.indexStatus && f.indexStatus !== " " && f.indexStatus !== "?");
834
+ const unstagedFiles = gitFiles.filter(f => (f.worktreeStatus && f.worktreeStatus !== " ") || f.status === "??");
560
835
 
561
836
  const handleContentChange = (newVal: string) => {
562
837
  if (!activeFilePath) return;
@@ -566,7 +841,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
566
841
  };
567
842
 
568
843
  const handleSaveFile = useCallback(async () => {
569
- if (!activeFile) return;
844
+ if (!activeFile || activeFile.path.startsWith("diff:") || activeFile.path.startsWith("browser:")) return;
570
845
  try {
571
846
  const res = await fetch("/api/files", {
572
847
  method: "POST",
@@ -630,7 +905,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
630
905
  };
631
906
 
632
907
  return (
633
- <div style={ideContainerStyle}>
908
+ <div id="loom-ide-container" style={ideContainerStyle}>
634
909
  {/* Activity Bar (Sidebar leftmost panel icons) */}
635
910
  <div style={activityBarStyle}>
636
911
  <div style={topIconsStyle}>
@@ -713,6 +988,26 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
713
988
  >
714
989
  🔍
715
990
  </div>
991
+
992
+ {/* Web Browser Tab */}
993
+ <div
994
+ onClick={() => {
995
+ if (activeTab === "browser") {
996
+ setExplorerVisible(!explorerVisible);
997
+ } else {
998
+ setActiveTab("browser");
999
+ setExplorerVisible(true);
1000
+ }
1001
+ }}
1002
+ style={{
1003
+ ...sidebarIconStyle,
1004
+ backgroundColor: activeTab === "browser" && explorerVisible ? "rgba(255,255,255,0.08)" : "transparent",
1005
+ borderLeft: activeTab === "browser" && explorerVisible ? "2px solid var(--primary)" : "2px solid transparent",
1006
+ }}
1007
+ title="Local Web Browser Test Preview"
1008
+ >
1009
+ 🌐
1010
+ </div>
716
1011
  </div>
717
1012
 
718
1013
  <div
@@ -726,13 +1021,15 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
726
1021
 
727
1022
  {/* Primary Sidebar (File Tree Panel) */}
728
1023
  {explorerVisible && (
729
- <div style={sidebarPanelStyle}>
1024
+ <>
1025
+ <div style={{ ...sidebarPanelStyle, width: `${sidebarWidth}px`, borderRight: "none" }}>
730
1026
  <div style={sidebarHeaderStyle}>
731
1027
  <span style={{ fontWeight: 700, fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.05em" }}>
732
1028
  {activeTab === "explorer" && "Workspace Explorer"}
733
1029
  {activeTab === "git" && "Source Control"}
734
1030
  {activeTab === "analytics" && "Telemetry & Cost"}
735
1031
  {activeTab === "auditor" && "Session Auditor"}
1032
+ {activeTab === "browser" && "Browser Preview"}
736
1033
  </span>
737
1034
  {activeTab === "explorer" && (
738
1035
  <div style={{ display: "flex", gap: "0.25rem" }}>
@@ -855,81 +1152,248 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
855
1152
  )}
856
1153
 
857
1154
  {activeTab === "git" && (
858
- <div style={{ padding: "0.5rem 1rem" }}>
859
- <div style={{ fontSize: "0.75rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)", marginBottom: "0.5rem" }}>
860
- Changes
861
- </div>
862
- {!isGitRepo ? (
863
- <div style={{ color: "var(--muted)", fontSize: "0.75rem", fontStyle: "italic" }}>
864
- Not a git repository.
865
- </div>
866
- ) : gitFiles.length === 0 ? (
867
- <div style={{ color: "var(--success)", fontSize: "0.75rem", padding: "0.5rem 0" }}>
868
- ✔ Workspace clean.
1155
+ <div style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
1156
+ {/* Branch and Sync header */}
1157
+ <div style={{ padding: "0.5rem 1rem", borderBottom: "1px solid var(--border)", display: "flex", alignItems: "center", justifyContent: "space-between", backgroundColor: "rgba(255,255,255,0.01)", flexShrink: 0 }}>
1158
+ <div style={{ display: "flex", alignItems: "center", gap: "0.4rem", fontSize: "0.75rem", fontWeight: 600, overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis", flex: 1 }}>
1159
+ <span style={{ color: "var(--primary)", flexShrink: 0 }}>🌿</span>
1160
+ <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={gitBranch}>
1161
+ {isGitRepo ? (gitBranch || "HEAD detached") : "No Repository"}
1162
+ </span>
869
1163
  </div>
870
- ) : (
871
- <div style={{ display: "flex", flexDirection: "column", gap: "0.25rem" }}>
872
- {gitFiles.map((file) => {
873
- let badgeColor = "#eab308";
874
- if (file.status === "??") badgeColor = "#10b981";
875
- if (file.status === "A") badgeColor = "#10b981";
876
- if (file.status === "D") badgeColor = "#ef4444";
877
- return (
878
- <div
879
- key={file.relativePath}
880
- onClick={async () => {
881
- try {
882
- const res = await fetch(`/api/git?action=diff&file=${encodeURIComponent(file.relativePath)}&root=${encodeURIComponent(workspaceRoot)}`);
883
- if (res.ok) {
884
- const data = await res.json();
885
- if (data.success) {
886
- const diffPath = `diff:${file.relativePath}`;
887
- if (!openFiles.some(f => f.path === diffPath)) {
888
- setOpenFiles(prev => [
889
- ...prev,
890
- {
891
- name: `Diff: ${file.name}`,
892
- path: diffPath,
893
- content: data.diff,
894
- isModified: false
895
- }
896
- ]);
897
- }
898
- setActiveFilePath(diffPath);
899
- }
900
- }
901
- } catch (e) {
902
- console.error(e);
903
- }
904
- }}
905
- style={{
906
- display: "flex",
907
- alignItems: "center",
908
- justifyContent: "space-between",
909
- padding: "0.35rem 0.5rem",
910
- borderRadius: "4px",
911
- backgroundColor: "rgba(255,255,255,0.02)",
912
- cursor: "pointer",
913
- fontSize: "0.75rem",
914
- fontFamily: "var(--font-mono)",
915
- }}
916
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.06)"}
917
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.02)"}
918
- >
919
- <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 }} title={file.relativePath}>
920
- {file.name}
921
- </span>
922
- <span style={{ fontSize: "0.6rem", padding: "0.1rem 0.3rem", borderRadius: "3px", backgroundColor: badgeColor + "22", color: badgeColor, fontWeight: 700 }}>
923
- {file.status}
1164
+ {isGitRepo && (
1165
+ <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", flexShrink: 0 }}>
1166
+ {gitSync.ahead > 0 && <span style={{ fontSize: "0.65rem", color: "#10b981", fontWeight: 700 }} title="Commits ahead of remote">↑{gitSync.ahead}</span>}
1167
+ {gitSync.behind > 0 && <span style={{ fontSize: "0.65rem", color: "#ef4444", fontWeight: 700 }} title="Commits behind remote">↓{gitSync.behind}</span>}
1168
+ <button
1169
+ onClick={handleGitPull}
1170
+ title="Pull changes"
1171
+ style={gitIconButtonStyle}
1172
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.08)"}
1173
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.02)"}
1174
+ >
1175
+ 📥
1176
+ </button>
1177
+ <button
1178
+ onClick={handleGitPush}
1179
+ title="Push commits"
1180
+ style={gitIconButtonStyle}
1181
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.08)"}
1182
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.02)"}
1183
+ >
1184
+ 📤
1185
+ </button>
1186
+ </div>
1187
+ )}
1188
+ </div>
1189
+
1190
+ {isGitRepo ? (
1191
+ <div style={{ flex: 1, overflowY: "auto", padding: "0.5rem 1rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
1192
+
1193
+ {/* Commit Box */}
1194
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.4rem" }}>
1195
+ <textarea
1196
+ value={commitMessage}
1197
+ onChange={(e) => setCommitMessage(e.target.value)}
1198
+ placeholder="Commit message (Ctrl+Enter to commit)..."
1199
+ style={commitTextareaStyle}
1200
+ onKeyDown={(e) => {
1201
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
1202
+ e.preventDefault();
1203
+ handleGitCommit();
1204
+ }
1205
+ }}
1206
+ />
1207
+ <button
1208
+ onClick={handleGitCommit}
1209
+ disabled={gitFiles.length === 0}
1210
+ style={{
1211
+ ...commitButtonStyle,
1212
+ opacity: gitFiles.length > 0 ? 1 : 0.5,
1213
+ cursor: gitFiles.length > 0 ? "pointer" : "not-allowed"
1214
+ }}
1215
+ >
1216
+ Commit
1217
+ </button>
1218
+ </div>
1219
+
1220
+ {/* Staged Changes Section */}
1221
+ {stagedFiles.length > 0 && (
1222
+ <div>
1223
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "0.35rem" }}>
1224
+ <span style={{ fontSize: "0.7rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)" }}>
1225
+ Staged Changes ({stagedFiles.length})
924
1226
  </span>
1227
+ <button
1228
+ onClick={() => handleGitUnstageAll()}
1229
+ title="Unstage All"
1230
+ style={actionHeaderBtnStyle}
1231
+ >
1232
+
1233
+ </button>
925
1234
  </div>
926
- );
927
- })}
1235
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.25rem" }}>
1236
+ {stagedFiles.map((file) => (
1237
+ <GitFileRow
1238
+ key={file.relativePath}
1239
+ file={file}
1240
+ isStaged={true}
1241
+ onAction={handleGitUnstage}
1242
+ onSelect={handleSelectGitFile}
1243
+ />
1244
+ ))}
1245
+ </div>
1246
+ </div>
1247
+ )}
1248
+
1249
+ {/* Changes (Unstaged) Section */}
1250
+ <div>
1251
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "0.35rem" }}>
1252
+ <span style={{ fontSize: "0.7rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)" }}>
1253
+ Changes ({unstagedFiles.length})
1254
+ </span>
1255
+ {unstagedFiles.length > 0 && (
1256
+ <button
1257
+ onClick={() => handleGitStageAll()}
1258
+ title="Stage All"
1259
+ style={actionHeaderBtnStyle}
1260
+ >
1261
+
1262
+ </button>
1263
+ )}
1264
+ </div>
1265
+ {unstagedFiles.length === 0 && stagedFiles.length === 0 ? (
1266
+ <div style={{ color: "var(--success)", fontSize: "0.75rem", padding: "0.5rem 0" }}>
1267
+ ✔ Workspace clean.
1268
+ </div>
1269
+ ) : unstagedFiles.length === 0 ? (
1270
+ <div style={{ color: "var(--muted)", fontSize: "0.7rem", fontStyle: "italic", padding: "0.25rem 0" }}>
1271
+ No unstaged changes.
1272
+ </div>
1273
+ ) : (
1274
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.25rem" }}>
1275
+ {unstagedFiles.map((file) => (
1276
+ <GitFileRow
1277
+ key={file.relativePath}
1278
+ file={file}
1279
+ isStaged={false}
1280
+ onAction={handleGitStage}
1281
+ onSelect={handleSelectGitFile}
1282
+ onIgnore={handleGitIgnore}
1283
+ onDiscard={handleGitDiscard}
1284
+ />
1285
+ ))}
1286
+ </div>
1287
+ )}
1288
+ </div>
1289
+ </div>
1290
+ ) : (
1291
+ <div style={{ padding: "1rem", color: "var(--muted)", fontSize: "0.75rem", fontStyle: "italic" }}>
1292
+ Not a git repository.
928
1293
  </div>
929
1294
  )}
930
1295
  </div>
931
1296
  )}
932
1297
 
1298
+ {activeTab === "browser" && (
1299
+ <div style={{ padding: "1rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
1300
+ <div>
1301
+ <label style={{ fontSize: "0.7rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)", display: "block", marginBottom: "0.4rem" }}>
1302
+ Url Address
1303
+ </label>
1304
+ <div style={{ display: "flex", gap: "0.25rem" }}>
1305
+ <input
1306
+ type="text"
1307
+ value={browserUrlInput}
1308
+ onChange={(e) => setBrowserUrlInput(e.target.value)}
1309
+ placeholder="http://localhost:3000"
1310
+ style={{
1311
+ flex: 1,
1312
+ backgroundColor: "#05070a",
1313
+ border: "1px solid var(--border)",
1314
+ borderRadius: "4px",
1315
+ padding: "0.35rem 0.5rem",
1316
+ color: "#ffffff",
1317
+ fontSize: "0.75rem",
1318
+ outline: "none"
1319
+ }}
1320
+ onKeyDown={(e) => {
1321
+ if (e.key === "Enter") {
1322
+ handleOpenBrowserTab(browserUrlInput);
1323
+ }
1324
+ }}
1325
+ />
1326
+ <button
1327
+ onClick={() => handleOpenBrowserTab(browserUrlInput)}
1328
+ style={{
1329
+ backgroundColor: "var(--primary)",
1330
+ border: "none",
1331
+ color: "#ffffff",
1332
+ padding: "0.35rem 0.75rem",
1333
+ borderRadius: "4px",
1334
+ fontSize: "0.75rem",
1335
+ fontWeight: 600,
1336
+ cursor: "pointer"
1337
+ }}
1338
+ >
1339
+ Go
1340
+ </button>
1341
+ </div>
1342
+ </div>
1343
+
1344
+ <div>
1345
+ <label style={{ fontSize: "0.7rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)", display: "block", marginBottom: "0.5rem" }}>
1346
+ Quick Launch Ports
1347
+ </label>
1348
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.35rem" }}>
1349
+ {[
1350
+ { label: "Next.js App (3000)", url: "http://localhost:3000" },
1351
+ { label: "WebSocket Gateway (3001)", url: "http://localhost:3001" },
1352
+ { label: "Vite Server (5173)", url: "http://localhost:5173" }
1353
+ ].map((item, idx) => (
1354
+ <div
1355
+ key={idx}
1356
+ onClick={() => {
1357
+ setBrowserUrlInput(item.url);
1358
+ handleOpenBrowserTab(item.url);
1359
+ }}
1360
+ style={{
1361
+ display: "flex",
1362
+ alignItems: "center",
1363
+ justifyContent: "space-between",
1364
+ padding: "0.5rem 0.75rem",
1365
+ borderRadius: "4px",
1366
+ backgroundColor: "rgba(255,255,255,0.02)",
1367
+ border: "1px solid var(--border)",
1368
+ cursor: "pointer",
1369
+ fontSize: "0.75rem",
1370
+ transition: "all 0.15s ease"
1371
+ }}
1372
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.06)"}
1373
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.02)"}
1374
+ >
1375
+ <span style={{ color: "var(--foreground)" }}>{item.label}</span>
1376
+ <span style={{ color: "var(--primary)", fontSize: "0.8rem" }}>🌐➔</span>
1377
+ </div>
1378
+ ))}
1379
+ </div>
1380
+ </div>
1381
+
1382
+ <div style={{
1383
+ marginTop: "auto",
1384
+ padding: "0.5rem",
1385
+ borderRadius: "6px",
1386
+ backgroundColor: "rgba(234, 179, 8, 0.05)",
1387
+ border: "1px solid rgba(234, 179, 8, 0.15)",
1388
+ fontSize: "0.65rem",
1389
+ color: "var(--muted)",
1390
+ lineHeight: "1.4"
1391
+ }}>
1392
+ <span style={{ color: "#eab308", fontWeight: 700 }}>⚠️ Iframe CSP Note</span>: Some public websites (e.g. Google, GitHub) block loading inside frames via security headers. Use this tab primarily to preview and debug local development webservers.
1393
+ </div>
1394
+ </div>
1395
+ )}
1396
+
933
1397
  {activeTab === "analytics" && (
934
1398
  <div style={{ padding: "0.5rem 1rem", display: "flex", flexDirection: "column", gap: "1.25rem" }}>
935
1399
  <div>
@@ -1129,7 +1593,26 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1129
1593
  )}
1130
1594
  </div>
1131
1595
  </div>
1132
- )}
1596
+ <div
1597
+ onMouseDown={startResizingSidebar}
1598
+ style={{
1599
+ width: "4px",
1600
+ cursor: "col-resize",
1601
+ backgroundColor: isResizingSidebar ? "var(--primary)" : "var(--border)",
1602
+ transition: "background-color 0.15s ease",
1603
+ zIndex: 10,
1604
+ alignSelf: "stretch",
1605
+ }}
1606
+ onMouseEnter={(e) => {
1607
+ if (!isResizingSidebar) e.currentTarget.style.backgroundColor = "var(--border-focus)";
1608
+ }}
1609
+ onMouseLeave={(e) => {
1610
+ if (!isResizingSidebar) e.currentTarget.style.backgroundColor = "var(--border)";
1611
+ }}
1612
+ title="Drag to resize sidebar"
1613
+ />
1614
+ </>
1615
+ )}
1133
1616
 
1134
1617
  {/* Editor & Console Split View */}
1135
1618
  <div style={mainContentAreaStyle}>
@@ -1178,7 +1661,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1178
1661
  </div>
1179
1662
 
1180
1663
  {/* Editor Panel (top half) */}
1181
- <div style={editorPanelStyle}>
1664
+ <div style={{ ...editorPanelStyle, borderBottom: "none" }}>
1182
1665
  {/* Tabs header */}
1183
1666
  <div style={tabHeaderStyle}>
1184
1667
  <div style={tabsWrapperStyle}>
@@ -1209,46 +1692,97 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1209
1692
  ))}
1210
1693
  </div>
1211
1694
 
1212
- {activeFile && !activeFile.path.startsWith("diff:") && (
1213
- <button
1214
- onClick={handleSaveFile}
1215
- disabled={!activeFile.isModified}
1216
- style={{
1217
- ...saveButtonStyle,
1218
- opacity: activeFile.isModified ? 1 : 0.5,
1219
- cursor: activeFile.isModified ? "pointer" : "not-allowed",
1220
- }}
1221
- >
1222
- 💾 Save (Ctrl+S)
1223
- </button>
1695
+ {activeFile && !activeFile.path.startsWith("diff:") && !activeFile.path.startsWith("browser:") && (
1696
+ <div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
1697
+ {isMarkdown && (
1698
+ <div style={modeToggleContainerStyle}>
1699
+ <button
1700
+ onClick={() => setEditorMode('edit')}
1701
+ style={{
1702
+ ...modeToggleItemStyle,
1703
+ backgroundColor: currentMode === 'edit' ? 'rgba(255,255,255,0.08)' : 'transparent',
1704
+ color: currentMode === 'edit' ? 'var(--foreground)' : 'var(--muted)',
1705
+ borderColor: currentMode === 'edit' ? 'var(--border-focus)' : 'transparent',
1706
+ }}
1707
+ title="Edit file"
1708
+ >
1709
+ ✏️ Edit
1710
+ </button>
1711
+ <button
1712
+ onClick={() => setEditorMode('split')}
1713
+ style={{
1714
+ ...modeToggleItemStyle,
1715
+ backgroundColor: currentMode === 'split' ? 'rgba(255,255,255,0.08)' : 'transparent',
1716
+ color: currentMode === 'split' ? 'var(--foreground)' : 'var(--muted)',
1717
+ borderColor: currentMode === 'split' ? 'var(--border-focus)' : 'transparent',
1718
+ }}
1719
+ title="Split editor & preview"
1720
+ >
1721
+ 🥞 Split
1722
+ </button>
1723
+ <button
1724
+ onClick={() => setEditorMode('preview')}
1725
+ style={{
1726
+ ...modeToggleItemStyle,
1727
+ backgroundColor: currentMode === 'preview' ? 'rgba(255,255,255,0.08)' : 'transparent',
1728
+ color: currentMode === 'preview' ? 'var(--foreground)' : 'var(--muted)',
1729
+ borderColor: currentMode === 'preview' ? 'var(--border-focus)' : 'transparent',
1730
+ }}
1731
+ title="Preview rendered document"
1732
+ >
1733
+ 👁️ Preview
1734
+ </button>
1735
+ </div>
1736
+ )}
1737
+ <button
1738
+ onClick={handleSaveFile}
1739
+ disabled={!activeFile.isModified}
1740
+ style={{
1741
+ ...saveButtonStyle,
1742
+ opacity: activeFile.isModified ? 1 : 0.5,
1743
+ cursor: activeFile.isModified ? "pointer" : "not-allowed",
1744
+ }}
1745
+ >
1746
+ 💾 Save (Ctrl+S)
1747
+ </button>
1748
+ </div>
1224
1749
  )}
1225
1750
  </div>
1226
1751
 
1227
1752
  {/* Editor contents */}
1228
1753
  <div style={editorWorkspaceStyle}>
1229
1754
  {activeFile ? (
1230
- activeFile.path.startsWith("diff:") ? (
1231
- <div style={{ fontFamily: "var(--font-mono)", fontSize: "0.8rem", padding: "1rem", overflow: "auto", height: "100%", backgroundColor: "#0b101b", lineHeight: 1.5 }}>
1232
- {activeFile.content.split("\n").map((line, i) => {
1233
- let color = "#e2e8f0";
1234
- let bg = "transparent";
1235
- if (line.startsWith("+")) {
1236
- color = "#4ade80";
1237
- bg = "rgba(74, 222, 128, 0.08)";
1238
- } else if (line.startsWith("-")) {
1239
- color = "#f87171";
1240
- bg = "rgba(248, 113, 113, 0.08)";
1241
- } else if (line.startsWith("@@")) {
1242
- color = "#60a5fa";
1243
- bg = "rgba(96, 165, 250, 0.05)";
1244
- }
1245
- return (
1246
- <div key={i} style={{ color, backgroundColor: bg, whiteSpace: "pre-wrap", padding: "0.1rem 0.5rem" }}>
1247
- {line}
1248
- </div>
1249
- );
1250
- })}
1251
- </div>
1755
+ activeFile.path.startsWith("browser:") ? (
1756
+ <BrowserView url={activeFile.content} />
1757
+ ) : activeFile.path.startsWith("diff:") ? (
1758
+ <DiffView content={activeFile.content} filePath={activeFile.path.substring(5)} />
1759
+ ) : isMarkdown ? (
1760
+ currentMode === 'preview' ? (
1761
+ <div style={{ height: "100%", overflow: "hidden" }}>
1762
+ <MarkdownPreview content={activeFile.content} />
1763
+ </div>
1764
+ ) : currentMode === 'split' ? (
1765
+ <div style={{ display: "flex", width: "100%", height: "100%", overflow: "hidden" }}>
1766
+ <div style={{ flex: 1, height: "100%" }}>
1767
+ <textarea
1768
+ value={activeFile.content}
1769
+ onChange={(e) => handleContentChange(e.target.value)}
1770
+ style={textareaStyle}
1771
+ spellCheck={false}
1772
+ />
1773
+ </div>
1774
+ <div style={{ flex: 1, height: "100%", overflow: "hidden" }}>
1775
+ <MarkdownPreview content={activeFile.content} />
1776
+ </div>
1777
+ </div>
1778
+ ) : (
1779
+ <textarea
1780
+ value={activeFile.content}
1781
+ onChange={(e) => handleContentChange(e.target.value)}
1782
+ style={textareaStyle}
1783
+ spellCheck={false}
1784
+ />
1785
+ )
1252
1786
  ) : (
1253
1787
  <textarea
1254
1788
  value={activeFile.content}
@@ -1272,8 +1806,28 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1272
1806
  </div>
1273
1807
  </div>
1274
1808
 
1809
+ {/* Vertical divider */}
1810
+ <div
1811
+ onMouseDown={startResizingTerminal}
1812
+ style={{
1813
+ height: "4px",
1814
+ cursor: "row-resize",
1815
+ backgroundColor: isResizingTerminal ? "var(--primary)" : "var(--border)",
1816
+ transition: "background-color 0.15s ease",
1817
+ zIndex: 10,
1818
+ width: "100%",
1819
+ }}
1820
+ onMouseEnter={(e) => {
1821
+ if (!isResizingTerminal) e.currentTarget.style.backgroundColor = "var(--border-focus)";
1822
+ }}
1823
+ onMouseLeave={(e) => {
1824
+ if (!isResizingTerminal) e.currentTarget.style.backgroundColor = "var(--border)";
1825
+ }}
1826
+ title="Drag to resize console"
1827
+ />
1828
+
1275
1829
  {/* Console / PTY Bridge Panel (bottom half) */}
1276
- <div style={terminalPanelStyle}>
1830
+ <div style={{ ...terminalPanelStyle, flex: "none", height: `${terminalHeight}px` }}>
1277
1831
  {/* Dashboard control headers injected above xterm */}
1278
1832
  <div style={terminalHeaderStyle}>
1279
1833
  <span style={{ display: "flex", alignItems: "center", gap: "0.5rem", fontWeight: 600, fontSize: "0.8rem" }}>
@@ -1410,7 +1964,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1410
1964
  </div>
1411
1965
  </div>
1412
1966
 
1413
- <div style={{ ...terminalConsoleWrapperStyle, display: "flex", width: "100%", height: "100%" }}>
1967
+ <div style={{ ...terminalConsoleWrapperStyle, display: "flex", width: "100%" }}>
1414
1968
  <div style={{ flex: 1, height: "100%", borderRight: splitSessionId ? "1px solid var(--border)" : "none", overflow: "hidden" }}>
1415
1969
  <TerminalConsole sessionId={sessionId} height="100%" wsPort={wsPort} />
1416
1970
  </div>
@@ -1617,7 +2171,7 @@ const sidebarIconStyle: React.CSSProperties = {
1617
2171
  };
1618
2172
 
1619
2173
  const sidebarPanelStyle: React.CSSProperties = {
1620
- width: "260px",
2174
+ width: "300px",
1621
2175
  backgroundColor: "#080c14",
1622
2176
  borderRight: "1px solid var(--border)",
1623
2177
  display: "flex",
@@ -2209,3 +2763,552 @@ const sessionTabItemStyle: React.CSSProperties = {
2209
2763
  cursor: "pointer",
2210
2764
  transition: "all 0.15s ease",
2211
2765
  };
2766
+
2767
+ const modeToggleContainerStyle: React.CSSProperties = {
2768
+ display: "flex",
2769
+ alignItems: "center",
2770
+ backgroundColor: "rgba(0, 0, 0, 0.3)",
2771
+ border: "1px solid var(--border)",
2772
+ borderRadius: "6px",
2773
+ padding: "2px",
2774
+ marginRight: "0.5rem",
2775
+ };
2776
+
2777
+ const modeToggleItemStyle: React.CSSProperties = {
2778
+ padding: "0.2rem 0.5rem",
2779
+ borderRadius: "4px",
2780
+ border: "1px solid transparent",
2781
+ fontSize: "0.65rem",
2782
+ fontWeight: 600,
2783
+ cursor: "pointer",
2784
+ transition: "all 0.15s ease",
2785
+ display: "flex",
2786
+ alignItems: "center",
2787
+ gap: "3px",
2788
+ };
2789
+
2790
+ interface GitFileRowProps {
2791
+ file: GitFile;
2792
+ isStaged: boolean;
2793
+ onAction: (file: string) => void;
2794
+ onSelect: (file: GitFile) => void;
2795
+ onIgnore?: (file: string) => void;
2796
+ onDiscard?: (file: string) => void;
2797
+ }
2798
+
2799
+ function GitFileRow({ file, isStaged, onAction, onSelect, onIgnore, onDiscard }: GitFileRowProps) {
2800
+ const [hovered, setHovered] = useState(false);
2801
+ let badgeColor = "#eab308";
2802
+ const displayStatus = isStaged ? (file.indexStatus || "M") : (file.worktreeStatus || "M");
2803
+
2804
+ if (displayStatus === "??" || displayStatus === "?") badgeColor = "#10b981";
2805
+ if (displayStatus === "A") badgeColor = "#10b981";
2806
+ if (displayStatus === "D") badgeColor = "#ef4444";
2807
+
2808
+ return (
2809
+ <div
2810
+ onClick={() => onSelect(file)}
2811
+ onMouseEnter={() => setHovered(true)}
2812
+ onMouseLeave={() => setHovered(false)}
2813
+ style={{
2814
+ display: "flex",
2815
+ alignItems: "center",
2816
+ justifyContent: "space-between",
2817
+ padding: "0.35rem 0.5rem",
2818
+ borderRadius: "4px",
2819
+ backgroundColor: "rgba(255,255,255,0.02)",
2820
+ cursor: "pointer",
2821
+ fontSize: "0.75rem",
2822
+ fontFamily: "var(--font-mono)",
2823
+ position: "relative"
2824
+ }}
2825
+ >
2826
+ <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1, marginRight: "0.5rem" }} title={file.relativePath}>
2827
+ {file.name}
2828
+ </span>
2829
+
2830
+ <div style={{ display: "flex", alignItems: "center", gap: "0.35rem" }}>
2831
+ {hovered && (
2832
+ <div style={{ display: "flex", gap: "0.25rem" }}>
2833
+ {!isStaged && onIgnore && (
2834
+ <button
2835
+ onClick={(e) => {
2836
+ e.stopPropagation();
2837
+ onIgnore(file.relativePath);
2838
+ }}
2839
+ title="Add to .gitignore"
2840
+ style={{
2841
+ background: "none",
2842
+ border: "none",
2843
+ cursor: "pointer",
2844
+ fontSize: "0.65rem",
2845
+ color: "var(--muted)",
2846
+ padding: "1px 4px",
2847
+ borderRadius: "3px",
2848
+ backgroundColor: "rgba(255,255,255,0.05)"
2849
+ }}
2850
+ onMouseEnter={(e) => e.currentTarget.style.color = "var(--danger)"}
2851
+ onMouseLeave={(e) => e.currentTarget.style.color = "var(--muted)"}
2852
+ >
2853
+ 🚫
2854
+ </button>
2855
+ )}
2856
+ {!isStaged && onDiscard && (
2857
+ <button
2858
+ onClick={(e) => {
2859
+ e.stopPropagation();
2860
+ onDiscard(file.relativePath);
2861
+ }}
2862
+ title="Discard changes"
2863
+ style={{
2864
+ background: "none",
2865
+ border: "none",
2866
+ cursor: "pointer",
2867
+ fontSize: "0.65rem",
2868
+ color: "var(--muted)",
2869
+ padding: "1px 4px",
2870
+ borderRadius: "3px",
2871
+ backgroundColor: "rgba(255,255,255,0.05)"
2872
+ }}
2873
+ onMouseEnter={(e) => e.currentTarget.style.color = "var(--danger)"}
2874
+ onMouseLeave={(e) => e.currentTarget.style.color = "var(--muted)"}
2875
+ >
2876
+ 🗑️
2877
+ </button>
2878
+ )}
2879
+ <button
2880
+ onClick={(e) => {
2881
+ e.stopPropagation();
2882
+ onAction(file.relativePath);
2883
+ }}
2884
+ title={isStaged ? "Unstage Change" : "Stage Change"}
2885
+ style={{
2886
+ background: "none",
2887
+ border: "none",
2888
+ cursor: "pointer",
2889
+ fontSize: "0.65rem",
2890
+ color: "var(--muted)",
2891
+ padding: "1px 4px",
2892
+ borderRadius: "3px",
2893
+ backgroundColor: "rgba(255,255,255,0.05)"
2894
+ }}
2895
+ onMouseEnter={(e) => e.currentTarget.style.color = "var(--foreground)"}
2896
+ onMouseLeave={(e) => e.currentTarget.style.color = "var(--muted)"}
2897
+ >
2898
+ {isStaged ? "➖" : "➕"}
2899
+ </button>
2900
+ </div>
2901
+ )}
2902
+ <span style={{ fontSize: "0.6rem", padding: "0.1rem 0.3rem", borderRadius: "3px", backgroundColor: badgeColor + "22", color: badgeColor, fontWeight: 700, width: "1.2rem", display: "inline-block", textAlign: "center" }}>
2903
+ {displayStatus.substring(0, 1)}
2904
+ </span>
2905
+ </div>
2906
+ </div>
2907
+ );
2908
+ }
2909
+
2910
+ interface BrowserViewProps {
2911
+ url: string;
2912
+ }
2913
+
2914
+ function BrowserView({ url }: BrowserViewProps) {
2915
+ const [currentUrl, setCurrentUrl] = useState(url);
2916
+ const [iframeUrl, setIframeUrl] = useState(url);
2917
+ const [refreshKey, setRefreshKey] = useState(0);
2918
+
2919
+ useEffect(() => {
2920
+ setCurrentUrl(url);
2921
+ setIframeUrl(url);
2922
+ }, [url]);
2923
+
2924
+ const handleGo = () => {
2925
+ let targetUrl = currentUrl.trim();
2926
+ if (targetUrl && !/^https?:\/\//i.test(targetUrl)) {
2927
+ targetUrl = "http://" + targetUrl;
2928
+ }
2929
+ setCurrentUrl(targetUrl);
2930
+ setIframeUrl(targetUrl);
2931
+ };
2932
+
2933
+ const handleReload = () => {
2934
+ setRefreshKey(prev => prev + 1);
2935
+ };
2936
+
2937
+ const handleOpenExternal = () => {
2938
+ window.open(iframeUrl, "_blank");
2939
+ };
2940
+
2941
+ return (
2942
+ <div style={{ display: "flex", flexDirection: "column", width: "100%", height: "100%", backgroundColor: "#0b101b" }}>
2943
+ {/* Navigation Toolbar */}
2944
+ <div style={{
2945
+ display: "flex",
2946
+ alignItems: "center",
2947
+ gap: "0.5rem",
2948
+ padding: "0.5rem",
2949
+ backgroundColor: "#05070a",
2950
+ borderBottom: "1px solid var(--border)",
2951
+ }}>
2952
+ <button
2953
+ onClick={handleReload}
2954
+ title="Hard Reload Frame"
2955
+ style={{
2956
+ background: "none",
2957
+ border: "1px solid var(--border)",
2958
+ borderRadius: "4px",
2959
+ color: "var(--muted)",
2960
+ cursor: "pointer",
2961
+ fontSize: "0.85rem",
2962
+ padding: "0.25rem 0.5rem",
2963
+ display: "flex",
2964
+ alignItems: "center",
2965
+ backgroundColor: "rgba(255,255,255,0.02)"
2966
+ }}
2967
+ onMouseEnter={(e) => e.currentTarget.style.color = "var(--foreground)"}
2968
+ onMouseLeave={(e) => e.currentTarget.style.color = "var(--muted)"}
2969
+ >
2970
+ 🔄
2971
+ </button>
2972
+
2973
+ <input
2974
+ type="text"
2975
+ value={currentUrl}
2976
+ onChange={(e) => setCurrentUrl(e.target.value)}
2977
+ onKeyDown={(e) => {
2978
+ if (e.key === "Enter") {
2979
+ handleGo();
2980
+ }
2981
+ }}
2982
+ style={{
2983
+ flex: 1,
2984
+ backgroundColor: "#080c14",
2985
+ border: "1px solid var(--border)",
2986
+ borderRadius: "4px",
2987
+ padding: "0.25rem 0.75rem",
2988
+ color: "#ffffff",
2989
+ fontSize: "0.8rem",
2990
+ outline: "none",
2991
+ fontFamily: "var(--font-mono), monospace"
2992
+ }}
2993
+ />
2994
+
2995
+ <button
2996
+ onClick={handleGo}
2997
+ title="Go to URL"
2998
+ style={{
2999
+ background: "none",
3000
+ border: "1px solid var(--border)",
3001
+ borderRadius: "4px",
3002
+ color: "var(--primary)",
3003
+ cursor: "pointer",
3004
+ fontSize: "0.8rem",
3005
+ padding: "0.25rem 0.5rem",
3006
+ display: "flex",
3007
+ alignItems: "center",
3008
+ backgroundColor: "rgba(255,255,255,0.02)"
3009
+ }}
3010
+ >
3011
+
3012
+ </button>
3013
+
3014
+ <button
3015
+ onClick={handleOpenExternal}
3016
+ title="Open in External Browser"
3017
+ style={{
3018
+ background: "none",
3019
+ border: "1px solid var(--border)",
3020
+ borderRadius: "4px",
3021
+ color: "var(--muted)",
3022
+ cursor: "pointer",
3023
+ fontSize: "0.85rem",
3024
+ padding: "0.25rem 0.5rem",
3025
+ display: "flex",
3026
+ alignItems: "center",
3027
+ backgroundColor: "rgba(255,255,255,0.02)"
3028
+ }}
3029
+ onMouseEnter={(e) => e.currentTarget.style.color = "var(--foreground)"}
3030
+ onMouseLeave={(e) => e.currentTarget.style.color = "var(--muted)"}
3031
+ >
3032
+ 🌐
3033
+ </button>
3034
+ </div>
3035
+
3036
+ {/* Frame Wrapper */}
3037
+ <div style={{ flex: 1, position: "relative", width: "100%", height: "100%", backgroundColor: "#ffffff" }}>
3038
+ <iframe
3039
+ key={refreshKey}
3040
+ src={iframeUrl}
3041
+ style={{
3042
+ width: "100%",
3043
+ height: "100%",
3044
+ border: "none",
3045
+ backgroundColor: "#ffffff",
3046
+ }}
3047
+ sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
3048
+ />
3049
+ </div>
3050
+ </div>
3051
+ );
3052
+ }
3053
+
3054
+ interface DiffViewProps {
3055
+ content: string;
3056
+ filePath: string;
3057
+ }
3058
+
3059
+ function DiffView({ content, filePath }: DiffViewProps) {
3060
+ const lines = content.split(/\r?\n/);
3061
+
3062
+ let oldLineNum = 0;
3063
+ let newLineNum = 0;
3064
+
3065
+ let additions = 0;
3066
+ let deletions = 0;
3067
+
3068
+ const parsedLines = lines.map((line) => {
3069
+ let type: 'meta' | 'header' | 'hunk' | 'addition' | 'deletion' | 'normal' = 'normal';
3070
+ let lineContent = line;
3071
+ let oldNum: number | null = null;
3072
+ let newNum: number | null = null;
3073
+
3074
+ if (line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ ")) {
3075
+ type = 'header';
3076
+ } else if (line.startsWith("@@")) {
3077
+ type = 'hunk';
3078
+ const match = line.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
3079
+ if (match) {
3080
+ oldLineNum = parseInt(match[1], 10);
3081
+ newLineNum = parseInt(match[2], 10);
3082
+ }
3083
+ } else if (line.startsWith("+")) {
3084
+ type = 'addition';
3085
+ lineContent = line.substring(1);
3086
+ newNum = newLineNum++;
3087
+ additions++;
3088
+ } else if (line.startsWith("-")) {
3089
+ type = 'deletion';
3090
+ lineContent = line.substring(1);
3091
+ oldNum = oldLineNum++;
3092
+ deletions++;
3093
+ } else {
3094
+ type = 'normal';
3095
+ if (line.startsWith(" ")) {
3096
+ lineContent = line.substring(1);
3097
+ }
3098
+ if (oldLineNum > 0) {
3099
+ oldNum = oldLineNum++;
3100
+ }
3101
+ if (newLineNum > 0) {
3102
+ newNum = newLineNum++;
3103
+ }
3104
+ }
3105
+
3106
+ return {
3107
+ type,
3108
+ content: lineContent,
3109
+ oldNum,
3110
+ newNum,
3111
+ raw: line
3112
+ };
3113
+ });
3114
+
3115
+ return (
3116
+ <div style={{ display: "flex", flexDirection: "column", height: "100%", backgroundColor: "#0b101b" }}>
3117
+ <div style={{
3118
+ display: "flex",
3119
+ alignItems: "center",
3120
+ justifyContent: "space-between",
3121
+ padding: "0.5rem 1rem",
3122
+ backgroundColor: "#05070a",
3123
+ borderBottom: "1px solid var(--border)",
3124
+ }}>
3125
+ <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
3126
+ <span style={{ fontSize: "1rem" }}>📄</span>
3127
+ <span style={{ fontWeight: 600, fontSize: "0.85rem", color: "var(--foreground)", fontFamily: "var(--font-mono)" }}>
3128
+ {filePath}
3129
+ </span>
3130
+ </div>
3131
+ <div style={{ display: "flex", alignItems: "center", gap: "0.75rem", fontSize: "0.75rem" }}>
3132
+ {additions > 0 && (
3133
+ <span style={{ color: "#4ade80", backgroundColor: "rgba(74, 222, 128, 0.1)", padding: "0.15rem 0.4rem", borderRadius: "4px", fontWeight: 600 }}>
3134
+ +{additions} lines
3135
+ </span>
3136
+ )}
3137
+ {deletions > 0 && (
3138
+ <span style={{ color: "#f87171", backgroundColor: "rgba(248, 113, 113, 0.1)", padding: "0.15rem 0.4rem", borderRadius: "4px", fontWeight: 600 }}>
3139
+ -{deletions} lines
3140
+ </span>
3141
+ )}
3142
+ <span style={{ color: "var(--muted)" }}>
3143
+ {parsedLines.length} lines in diff
3144
+ </span>
3145
+ </div>
3146
+ </div>
3147
+
3148
+ <div style={{ flex: 1, overflow: "auto", padding: "0.5rem 0" }}>
3149
+ <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "var(--font-mono), monospace", fontSize: "0.8rem", lineHeight: 1.5 }}>
3150
+ <tbody>
3151
+ {parsedLines.map((lineObj, idx) => {
3152
+ if (lineObj.type === 'header') {
3153
+ return (
3154
+ <tr key={idx} style={{ backgroundColor: "rgba(255,255,255,0.02)" }}>
3155
+ <td colSpan={4} style={{ color: "var(--muted)", padding: "0.25rem 1rem", fontSize: "0.75rem", borderBottom: "1px solid rgba(255,255,255,0.03)" }}>
3156
+ {lineObj.raw}
3157
+ </td>
3158
+ </tr>
3159
+ );
3160
+ }
3161
+ if (lineObj.type === 'hunk') {
3162
+ return (
3163
+ <tr key={idx} style={{ backgroundColor: "rgba(96, 165, 250, 0.05)" }}>
3164
+ <td colSpan={2} style={{
3165
+ width: "80px",
3166
+ textAlign: "center",
3167
+ color: "rgba(96, 165, 250, 0.6)",
3168
+ borderRight: "1px solid rgba(96, 165, 250, 0.15)",
3169
+ userSelect: "none",
3170
+ padding: "0.25rem 0",
3171
+ fontSize: "0.7rem",
3172
+ }}>
3173
+ @@
3174
+ </td>
3175
+ <td style={{
3176
+ width: "25px",
3177
+ borderRight: "1px solid rgba(255, 255, 255, 0.05)",
3178
+ }}></td>
3179
+ <td style={{ color: "#60a5fa", padding: "0.25rem 1rem", fontStyle: "italic", fontSize: "0.75rem" }}>
3180
+ {lineObj.raw}
3181
+ </td>
3182
+ </tr>
3183
+ );
3184
+ }
3185
+
3186
+ let rowBg = "transparent";
3187
+ let codeColor = "var(--foreground)";
3188
+ let sign = " ";
3189
+ let signColor = "transparent";
3190
+
3191
+ if (lineObj.type === 'addition') {
3192
+ rowBg = "rgba(74, 222, 128, 0.08)";
3193
+ codeColor = "#4ade80";
3194
+ sign = "+";
3195
+ signColor = "rgba(74, 222, 128, 0.4)";
3196
+ } else if (lineObj.type === 'deletion') {
3197
+ rowBg = "rgba(248, 113, 113, 0.08)";
3198
+ codeColor = "#f87171";
3199
+ sign = "-";
3200
+ signColor = "rgba(248, 113, 113, 0.4)";
3201
+ }
3202
+
3203
+ return (
3204
+ <tr key={idx} style={{ backgroundColor: rowBg }}>
3205
+ {/* Old line number */}
3206
+ <td style={{
3207
+ width: "40px",
3208
+ textAlign: "right",
3209
+ paddingRight: "0.75rem",
3210
+ color: "var(--muted)",
3211
+ opacity: lineObj.oldNum ? 0.6 : 0.2,
3212
+ userSelect: "none",
3213
+ borderRight: "1px solid rgba(255, 255, 255, 0.05)",
3214
+ fontSize: "0.75rem"
3215
+ }}>
3216
+ {lineObj.oldNum || ""}
3217
+ </td>
3218
+ {/* New line number */}
3219
+ <td style={{
3220
+ width: "40px",
3221
+ textAlign: "right",
3222
+ paddingRight: "0.75rem",
3223
+ color: "var(--muted)",
3224
+ opacity: lineObj.newNum ? 0.6 : 0.2,
3225
+ userSelect: "none",
3226
+ borderRight: "1px solid rgba(255, 255, 255, 0.05)",
3227
+ fontSize: "0.75rem"
3228
+ }}>
3229
+ {lineObj.newNum || ""}
3230
+ </td>
3231
+ {/* Sign (+ / -) */}
3232
+ <td style={{
3233
+ width: "25px",
3234
+ textAlign: "center",
3235
+ color: signColor,
3236
+ userSelect: "none",
3237
+ fontWeight: "bold",
3238
+ fontSize: "0.85rem",
3239
+ borderRight: "1px solid rgba(255, 255, 255, 0.05)",
3240
+ }}>
3241
+ {sign}
3242
+ </td>
3243
+ {/* Code line content */}
3244
+ <td style={{
3245
+ paddingLeft: "0.75rem",
3246
+ color: codeColor,
3247
+ whiteSpace: "pre-wrap",
3248
+ wordBreak: "break-all"
3249
+ }}>
3250
+ {lineObj.content}
3251
+ </td>
3252
+ </tr>
3253
+ );
3254
+ })}
3255
+ </tbody>
3256
+ </table>
3257
+ </div>
3258
+ </div>
3259
+ );
3260
+ }
3261
+
3262
+
3263
+ const commitTextareaStyle: React.CSSProperties = {
3264
+ width: "100%",
3265
+ minHeight: "60px",
3266
+ backgroundColor: "#05070a",
3267
+ border: "1px solid var(--border)",
3268
+ borderRadius: "6px",
3269
+ padding: "0.5rem",
3270
+ color: "#e2e8f0",
3271
+ fontSize: "0.75rem",
3272
+ resize: "vertical",
3273
+ outline: "none",
3274
+ fontFamily: "var(--font-sans), sans-serif",
3275
+ };
3276
+
3277
+ const commitButtonStyle: React.CSSProperties = {
3278
+ width: "100%",
3279
+ padding: "0.35rem",
3280
+ backgroundColor: "var(--primary)",
3281
+ border: "none",
3282
+ color: "#ffffff",
3283
+ borderRadius: "6px",
3284
+ fontSize: "0.75rem",
3285
+ fontWeight: 600,
3286
+ cursor: "pointer",
3287
+ transition: "opacity 0.1s ease",
3288
+ };
3289
+
3290
+ const gitIconButtonStyle: React.CSSProperties = {
3291
+ background: "none",
3292
+ border: "1px solid var(--border)",
3293
+ borderRadius: "4px",
3294
+ padding: "2px 5px",
3295
+ cursor: "pointer",
3296
+ fontSize: "0.7rem",
3297
+ display: "flex",
3298
+ alignItems: "center",
3299
+ justifyContent: "center",
3300
+ backgroundColor: "rgba(255,255,255,0.02)",
3301
+ };
3302
+
3303
+ const actionHeaderBtnStyle: React.CSSProperties = {
3304
+ background: "none",
3305
+ border: "none",
3306
+ cursor: "pointer",
3307
+ fontSize: "0.65rem",
3308
+ color: "var(--muted)",
3309
+ padding: "2px 4px",
3310
+ borderRadius: "3px",
3311
+ display: "flex",
3312
+ alignItems: "center",
3313
+ justifyContent: "center"
3314
+ };