neural-loom 0.2.19 → 0.2.40

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 (201) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +0 -1
  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 -32
  10. package/.next/routes-manifest.json +0 -6
  11. package/.next/server/app/_global-error/page.js +6 -7
  12. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  13. package/.next/server/app/_global-error.html +1 -1
  14. package/.next/server/app/_global-error.rsc +2 -3
  15. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_full.segment.rsc +2 -3
  17. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -2
  18. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/_not-found/page.js +6 -7
  21. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/server/app/_not-found.html +1 -1
  23. package/.next/server/app/_not-found.rsc +2 -2
  24. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  25. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  26. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  27. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  28. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  29. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  30. package/.next/server/app/api/context/aider/route.js +2 -2
  31. package/.next/server/app/api/context/aider/route.js.nft.json +1 -1
  32. package/.next/server/app/api/context/claude/route.js +2 -2
  33. package/.next/server/app/api/context/claude/route.js.nft.json +1 -1
  34. package/.next/server/app/api/context/route.js +2 -2
  35. package/.next/server/app/api/context/route.js.nft.json +1 -1
  36. package/.next/server/app/api/context/ssh/route.js +2 -2
  37. package/.next/server/app/api/context/ssh/route.js.nft.json +1 -1
  38. package/.next/server/app/api/files/route.js +2 -2
  39. package/.next/server/app/api/files/route.js.nft.json +1 -1
  40. package/.next/server/app/api/git/route.js +2 -2
  41. package/.next/server/app/api/git/route.js.nft.json +1 -1
  42. package/.next/server/app/api/recents/route.js +2 -2
  43. package/.next/server/app/api/recents/route.js.nft.json +1 -1
  44. package/.next/server/app/api/sessions/inject/route.js +2 -2
  45. package/.next/server/app/api/sessions/inject/route.js.nft.json +1 -1
  46. package/.next/server/app/api/sessions/input/route.js +2 -2
  47. package/.next/server/app/api/sessions/input/route.js.nft.json +1 -1
  48. package/.next/server/app/api/sessions/launch/route.js +2 -2
  49. package/.next/server/app/api/sessions/launch/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/route.js +2 -2
  51. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/scope/route.js +2 -2
  53. package/.next/server/app/api/sessions/scope/route.js.nft.json +1 -1
  54. package/.next/server/app/api/sessions/start/route.js +2 -2
  55. package/.next/server/app/api/sessions/start/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/stats/route.js +2 -2
  57. package/.next/server/app/api/sessions/stats/route.js.nft.json +1 -1
  58. package/.next/server/app/api/sessions/stop/route.js +2 -2
  59. package/.next/server/app/api/sessions/stop/route.js.nft.json +1 -1
  60. package/.next/server/app/index.html +1 -1
  61. package/.next/server/app/index.rsc +4 -4
  62. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  63. package/.next/server/app/index.segments/_full.segment.rsc +4 -4
  64. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  65. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  66. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  67. package/.next/server/app/page/react-loadable-manifest.json +2 -2
  68. package/.next/server/app/page.js +6 -7
  69. package/.next/server/app/page.js.nft.json +1 -1
  70. package/.next/server/app/page_client-reference-manifest.js +1 -1
  71. package/.next/server/app-paths-manifest.json +0 -1
  72. package/.next/server/chunks/{[externals]_next_dist_0arv.vj._.js → [externals]__048y_op._.js} +2 -2
  73. package/.next/server/chunks/{[root-of-the-server]__0-lisav._.js → [root-of-the-server]__0-vx6-s._.js} +3 -3
  74. package/.next/server/chunks/{[root-of-the-server]__0-lisav._.js.map → [root-of-the-server]__0-vx6-s._.js.map} +1 -1
  75. package/.next/server/chunks/{[root-of-the-server]__0k6w___._.js → [root-of-the-server]__0656ruo._.js} +3 -3
  76. package/.next/server/chunks/{[root-of-the-server]__0k6w___._.js.map → [root-of-the-server]__0656ruo._.js.map} +1 -1
  77. package/.next/server/chunks/[root-of-the-server]__0969ozy._.js +3 -0
  78. package/.next/server/chunks/{[root-of-the-server]__0l7c-gn._.js.map → [root-of-the-server]__0969ozy._.js.map} +1 -1
  79. package/.next/server/chunks/[root-of-the-server]__0d0pykb._.js +4 -4
  80. package/.next/server/chunks/[root-of-the-server]__0d0pykb._.js.map +1 -1
  81. package/.next/server/chunks/{[root-of-the-server]__0iqaxhx._.js → [root-of-the-server]__0d9_-7g._.js} +3 -3
  82. package/.next/server/chunks/{[root-of-the-server]__0iqaxhx._.js.map → [root-of-the-server]__0d9_-7g._.js.map} +1 -1
  83. package/.next/server/chunks/{[root-of-the-server]__0qz~w0~._.js → [root-of-the-server]__0fhv7.1._.js} +3 -3
  84. package/.next/server/chunks/{[root-of-the-server]__0qz~w0~._.js.map → [root-of-the-server]__0fhv7.1._.js.map} +1 -1
  85. package/.next/server/chunks/{[root-of-the-server]__0j8-xkl._.js → [root-of-the-server]__0i83~95._.js} +2 -2
  86. package/.next/server/chunks/{[root-of-the-server]__0j8-xkl._.js.map → [root-of-the-server]__0i83~95._.js.map} +1 -1
  87. package/.next/server/chunks/[root-of-the-server]__0kn5tap._.js +4 -0
  88. package/.next/server/chunks/{[root-of-the-server]__0ntt3om._.js.map → [root-of-the-server]__0kn5tap._.js.map} +1 -1
  89. package/.next/server/chunks/[root-of-the-server]__0n-zban._.js +3 -0
  90. package/.next/server/chunks/{[root-of-the-server]__0p~u94k._.js.map → [root-of-the-server]__0n-zban._.js.map} +1 -1
  91. package/.next/server/chunks/[root-of-the-server]__0qqnqov._.js +15 -0
  92. package/.next/server/chunks/{[root-of-the-server]__0m91xif._.js.map → [root-of-the-server]__0qqnqov._.js.map} +1 -1
  93. package/.next/server/chunks/[root-of-the-server]__0sa_gpm._.js +3 -0
  94. package/.next/server/chunks/[root-of-the-server]__0sa_gpm._.js.map +1 -0
  95. package/.next/server/chunks/[root-of-the-server]__0t_~on0._.js +15 -0
  96. package/.next/server/chunks/[root-of-the-server]__0t_~on0._.js.map +1 -0
  97. package/.next/server/chunks/[root-of-the-server]__0vp8-ka._.js +3 -0
  98. package/.next/server/chunks/{[root-of-the-server]__0qyrt6_._.js.map → [root-of-the-server]__0vp8-ka._.js.map} +1 -1
  99. package/.next/server/chunks/{[root-of-the-server]__0__9rdk._.js → [root-of-the-server]__0xr~y1f._.js} +3 -3
  100. package/.next/server/chunks/{[root-of-the-server]__0__9rdk._.js.map → [root-of-the-server]__0xr~y1f._.js.map} +1 -1
  101. package/.next/server/chunks/{[root-of-the-server]__0_2_i_q._.js → [root-of-the-server]__0zkcq9q._.js} +3 -3
  102. package/.next/server/chunks/{[root-of-the-server]__0_2_i_q._.js.map → [root-of-the-server]__0zkcq9q._.js.map} +1 -1
  103. package/.next/server/chunks/{[root-of-the-server]__0bi3d5z._.js → [root-of-the-server]__0~zauvq._.js} +3 -3
  104. package/.next/server/chunks/{[root-of-the-server]__0bi3d5z._.js.map → [root-of-the-server]__0~zauvq._.js.map} +1 -1
  105. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js +1 -1
  106. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js.map +1 -1
  107. package/.next/server/chunks/ssr/[root-of-the-server]__0cbmrds._.js +3 -0
  108. package/.next/server/chunks/ssr/[root-of-the-server]__0cbmrds._.js.map +1 -0
  109. package/.next/server/chunks/ssr/[root-of-the-server]__0ni-l5l._.js +3 -0
  110. package/.next/server/chunks/ssr/[root-of-the-server]__0ni-l5l._.js.map +1 -0
  111. package/.next/server/chunks/ssr/{[root-of-the-server]__089dc4i._.js → [root-of-the-server]__0tcyn68._.js} +2 -2
  112. package/.next/server/chunks/ssr/[root-of-the-server]__0tcyn68._.js.map +1 -0
  113. package/.next/server/chunks/ssr/[root-of-the-server]__0v73tbn._.js +1 -1
  114. package/.next/server/chunks/ssr/[root-of-the-server]__0v73tbn._.js.map +1 -1
  115. package/.next/server/chunks/ssr/[root-of-the-server]__0yj_dfr._.js +3 -0
  116. package/.next/server/chunks/ssr/[root-of-the-server]__0yj_dfr._.js.map +1 -0
  117. package/.next/server/chunks/ssr/_0t7oqy6._.js +1 -1
  118. package/.next/server/chunks/ssr/_0t7oqy6._.js.map +1 -1
  119. package/.next/server/chunks/ssr/{node_modules_next_dist_0ppctuh._.js → node_modules_next_dist_0-g.jog._.js} +2 -2
  120. package/.next/server/chunks/ssr/node_modules_next_dist_0-g.jog._.js.map +1 -0
  121. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0bj8dkg.js +4 -0
  122. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0bj8dkg.js.map +1 -0
  123. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0cvrgh7.js +4 -0
  124. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0cvrgh7.js.map +1 -0
  125. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0rc3ul_.js +4 -0
  126. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0rc3ul_.js.map +1 -0
  127. package/.next/server/middleware-build-manifest.js +3 -3
  128. package/.next/server/pages/404.html +1 -1
  129. package/.next/server/pages/500.html +1 -1
  130. package/.next/server/server-reference-manifest.js +1 -1
  131. package/.next/server/server-reference-manifest.json +1 -1
  132. package/.next/static/chunks/07-tub5vqy.fk.js +1 -0
  133. package/.next/static/chunks/0kkoif8x_kxrq.js +1 -0
  134. package/.next/static/chunks/{0pp.lp89pntpv.css → 0m-_sw1425nkp.css} +1 -1
  135. package/.next/static/chunks/0wq3_34inbzj6.js +1 -0
  136. package/.next/trace +1 -1
  137. package/.next/trace-build +1 -1
  138. package/package.json +1 -1
  139. package/public/logo.png +0 -0
  140. package/public/manifest.json +22 -0
  141. package/public/sw.js +12 -0
  142. package/src/app/api/context/aider/route.ts +6 -1
  143. package/src/app/api/files/route.ts +20 -8
  144. package/src/app/api/git/route.ts +117 -1
  145. package/src/app/api/sessions/route.ts +4 -1
  146. package/src/app/components/AiderWizard.tsx +19 -1
  147. package/src/app/components/IdeLayout.tsx +715 -87
  148. package/src/app/components/MarkdownPreview.tsx +528 -0
  149. package/src/app/layout.tsx +6 -0
  150. package/src/app/page.module.css +2 -0
  151. package/src/app/page.tsx +34 -19
  152. package/src/lib/agents/AiderRunner.ts +75 -36
  153. package/src/lib/agents/SessionManager.ts +18 -9
  154. package/src/lib/agents/SessionPersister.ts +10 -0
  155. package/.next/server/app/favicon.ico/route/app-paths-manifest.json +0 -3
  156. package/.next/server/app/favicon.ico/route/build-manifest.json +0 -9
  157. package/.next/server/app/favicon.ico/route.js +0 -7
  158. package/.next/server/app/favicon.ico/route.js.map +0 -5
  159. package/.next/server/app/favicon.ico/route.js.nft.json +0 -1
  160. package/.next/server/app/favicon.ico.body +0 -0
  161. package/.next/server/app/favicon.ico.meta +0 -1
  162. package/.next/server/chunks/[externals]__1334_78._.js +0 -3
  163. package/.next/server/chunks/[externals]_next_dist_0arv.vj._.js.map +0 -1
  164. package/.next/server/chunks/[root-of-the-server]__0l7c-gn._.js +0 -3
  165. package/.next/server/chunks/[root-of-the-server]__0m91xif._.js +0 -15
  166. package/.next/server/chunks/[root-of-the-server]__0ntt3om._.js +0 -3
  167. package/.next/server/chunks/[root-of-the-server]__0p~u94k._.js +0 -3
  168. package/.next/server/chunks/[root-of-the-server]__0qyrt6_._.js +0 -3
  169. package/.next/server/chunks/[root-of-the-server]__0ty8hwz._.js +0 -15
  170. package/.next/server/chunks/[root-of-the-server]__0ty8hwz._.js.map +0 -1
  171. package/.next/server/chunks/[root-of-the-server]__0ylcu31._.js +0 -3
  172. package/.next/server/chunks/[root-of-the-server]__0ylcu31._.js.map +0 -1
  173. package/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_095lj93.js +0 -3
  174. package/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_095lj93.js.map +0 -1
  175. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0m.429v.js +0 -3
  176. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0m.429v.js.map +0 -1
  177. package/.next/server/chunks/ssr/[root-of-the-server]__089dc4i._.js.map +0 -1
  178. package/.next/server/chunks/ssr/[root-of-the-server]__0_7i5h0._.js +0 -3
  179. package/.next/server/chunks/ssr/[root-of-the-server]__0_7i5h0._.js.map +0 -1
  180. package/.next/server/chunks/ssr/[root-of-the-server]__0hw~y-4._.js +0 -3
  181. package/.next/server/chunks/ssr/[root-of-the-server]__0hw~y-4._.js.map +0 -1
  182. package/.next/server/chunks/ssr/[root-of-the-server]__0~y8ue.._.js +0 -3
  183. package/.next/server/chunks/ssr/[root-of-the-server]__0~y8ue.._.js.map +0 -1
  184. package/.next/server/chunks/ssr/node_modules_05l396c._.js +0 -3
  185. package/.next/server/chunks/ssr/node_modules_05l396c._.js.map +0 -1
  186. package/.next/server/chunks/ssr/node_modules_next_dist_0ppctuh._.js.map +0 -1
  187. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03-z2qq.js +0 -4
  188. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03-z2qq.js.map +0 -1
  189. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_07vh7rm.js +0 -4
  190. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_07vh7rm.js.map +0 -1
  191. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0qp4u6g.js +0 -4
  192. package/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0qp4u6g.js.map +0 -1
  193. package/.next/static/chunks/01.1lqu6kmllv.js +0 -1
  194. package/.next/static/chunks/0ek0280331wie.js +0 -1
  195. package/.next/static/chunks/0jxar8nsw_py3.js +0 -1
  196. package/.next/static/media/favicon.0x3dzn~oxb6tn.ico +0 -0
  197. package/src/app/favicon.ico +0 -0
  198. /package/.next/server/chunks/{[externals]__1334_78._.js.map → [externals]__048y_op._.js.map} +0 -0
  199. /package/.next/static/{W3UxhE_D02EcHxTzdXKHX → 5uGIuH8oXybqivkeHIrqb}/_buildManifest.js +0 -0
  200. /package/.next/static/{W3UxhE_D02EcHxTzdXKHX → 5uGIuH8oXybqivkeHIrqb}/_clientMiddlewareManifest.js +0 -0
  201. /package/.next/static/{W3UxhE_D02EcHxTzdXKHX → 5uGIuH8oXybqivkeHIrqb}/_ssgManifest.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,78 @@ 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
159
  const [activeTab, setActiveTab] = useState<"explorer" | "git" | "analytics" | "auditor">("explorer");
156
160
  const [isRefreshing, setIsRefreshing] = useState(false);
157
161
 
162
+ // Layout sizing states
163
+ const [sidebarWidth, setSidebarWidth] = useState(300);
164
+ const [isResizingSidebar, setIsResizingSidebar] = useState(false);
165
+ const [terminalHeight, setTerminalHeight] = useState(380);
166
+ const [isResizingTerminal, setIsResizingTerminal] = useState(false);
167
+
168
+ const startResizingSidebar = useCallback((e: React.MouseEvent) => {
169
+ e.preventDefault();
170
+ setIsResizingSidebar(true);
171
+ }, []);
172
+
173
+ const startResizingTerminal = useCallback((e: React.MouseEvent) => {
174
+ e.preventDefault();
175
+ setIsResizingTerminal(true);
176
+ }, []);
177
+
178
+ useEffect(() => {
179
+ const handleMouseMove = (e: MouseEvent) => {
180
+ if (isResizingSidebar) {
181
+ const container = document.getElementById("loom-ide-container");
182
+ if (container) {
183
+ const rect = container.getBoundingClientRect();
184
+ const newWidth = e.clientX - rect.left - 50; // 50px is the activity bar width
185
+ if (newWidth > 150 && newWidth < 600) {
186
+ setSidebarWidth(newWidth);
187
+ }
188
+ }
189
+ }
190
+ if (isResizingTerminal) {
191
+ const container = document.getElementById("loom-ide-container");
192
+ if (container) {
193
+ const rect = container.getBoundingClientRect();
194
+ const newHeight = rect.bottom - e.clientY;
195
+ if (newHeight > 100 && newHeight < rect.height - 150) {
196
+ setTerminalHeight(newHeight);
197
+ }
198
+ }
199
+ }
200
+ };
201
+
202
+ const handleMouseUp = () => {
203
+ setIsResizingSidebar(false);
204
+ setIsResizingTerminal(false);
205
+ };
206
+
207
+ if (isResizingSidebar || isResizingTerminal) {
208
+ window.addEventListener("mousemove", handleMouseMove);
209
+ window.addEventListener("mouseup", handleMouseUp);
210
+ document.body.style.cursor = isResizingSidebar ? "col-resize" : "row-resize";
211
+ document.body.style.userSelect = "none";
212
+ }
213
+
214
+ return () => {
215
+ window.removeEventListener("mousemove", handleMouseMove);
216
+ window.removeEventListener("mouseup", handleMouseUp);
217
+ document.body.style.cursor = "";
218
+ document.body.style.userSelect = "";
219
+ };
220
+ }, [isResizingSidebar, isResizingTerminal]);
221
+
158
222
  // Git Diff Panel state
159
223
  const [gitFiles, setGitFiles] = useState<GitFile[]>([]);
160
224
  const [isGitRepo, setIsGitRepo] = useState(false);
161
225
  const [gitLoading, setGitLoading] = useState(false);
226
+ const [gitBranch, setGitBranch] = useState("");
227
+ const [gitSync, setGitSync] = useState({ ahead: 0, behind: 0 });
228
+ const [commitMessage, setCommitMessage] = useState("");
162
229
 
163
230
  // Status & Telemetry
164
231
  const [sessionStatus, setSessionStatus] = useState<SessionStatus | null>(null);
@@ -187,6 +254,8 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
187
254
  if (data.success) {
188
255
  setIsGitRepo(data.isGit);
189
256
  setGitFiles(data.files || []);
257
+ setGitBranch(data.branch || "");
258
+ setGitSync({ ahead: data.ahead || 0, behind: data.behind || 0 });
190
259
  }
191
260
  }
192
261
  } catch (e) {
@@ -204,6 +273,185 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
204
273
  return () => clearInterval(id);
205
274
  }, [loadGitStatus]);
206
275
 
276
+ const handleGitStage = async (file: string) => {
277
+ try {
278
+ const res = await fetch("/api/git", {
279
+ method: "POST",
280
+ headers: { "Content-Type": "application/json" },
281
+ body: JSON.stringify({ action: "stage", file, rootPath: workspaceRoot })
282
+ });
283
+ if (res.ok) {
284
+ await loadGitStatus();
285
+ }
286
+ } catch (e) {
287
+ console.error(e);
288
+ }
289
+ };
290
+
291
+ const handleGitIgnore = async (file: string) => {
292
+ try {
293
+ const res = await fetch("/api/git", {
294
+ method: "POST",
295
+ headers: { "Content-Type": "application/json" },
296
+ body: JSON.stringify({ action: "ignore", file, rootPath: workspaceRoot })
297
+ });
298
+ if (res.ok) {
299
+ await loadGitStatus();
300
+ }
301
+ } catch (e) {
302
+ console.error(e);
303
+ }
304
+ };
305
+
306
+ const handleGitDiscard = async (file: string) => {
307
+ if (!confirm(`Are you sure you want to discard changes in "${file}"? This action cannot be undone.`)) {
308
+ return;
309
+ }
310
+ try {
311
+ const res = await fetch("/api/git", {
312
+ method: "POST",
313
+ headers: { "Content-Type": "application/json" },
314
+ body: JSON.stringify({ action: "discard", file, rootPath: workspaceRoot })
315
+ });
316
+ if (res.ok) {
317
+ await loadGitStatus();
318
+ }
319
+ } catch (e) {
320
+ console.error(e);
321
+ }
322
+ };
323
+
324
+ const handleGitStageAll = async () => {
325
+ try {
326
+ const res = await fetch("/api/git", {
327
+ method: "POST",
328
+ headers: { "Content-Type": "application/json" },
329
+ body: JSON.stringify({ action: "stage", rootPath: workspaceRoot })
330
+ });
331
+ if (res.ok) {
332
+ await loadGitStatus();
333
+ }
334
+ } catch (e) {
335
+ console.error(e);
336
+ }
337
+ };
338
+
339
+ const handleGitUnstage = async (file: string) => {
340
+ try {
341
+ const res = await fetch("/api/git", {
342
+ method: "POST",
343
+ headers: { "Content-Type": "application/json" },
344
+ body: JSON.stringify({ action: "unstage", file, rootPath: workspaceRoot })
345
+ });
346
+ if (res.ok) {
347
+ await loadGitStatus();
348
+ }
349
+ } catch (e) {
350
+ console.error(e);
351
+ }
352
+ };
353
+
354
+ const handleGitUnstageAll = async () => {
355
+ try {
356
+ const res = await fetch("/api/git", {
357
+ method: "POST",
358
+ headers: { "Content-Type": "application/json" },
359
+ body: JSON.stringify({ action: "unstage", rootPath: workspaceRoot })
360
+ });
361
+ if (res.ok) {
362
+ await loadGitStatus();
363
+ }
364
+ } catch (e) {
365
+ console.error(e);
366
+ }
367
+ };
368
+
369
+ const handleGitCommit = async () => {
370
+ if (!commitMessage.trim()) return;
371
+ try {
372
+ const res = await fetch("/api/git", {
373
+ method: "POST",
374
+ headers: { "Content-Type": "application/json" },
375
+ body: JSON.stringify({ action: "commit", message: commitMessage, rootPath: workspaceRoot })
376
+ });
377
+ if (res.ok) {
378
+ setCommitMessage("");
379
+ await loadGitStatus();
380
+ } else {
381
+ const data = await res.json();
382
+ alert(data.error || "Git commit failed");
383
+ }
384
+ } catch (e) {
385
+ console.error(e);
386
+ }
387
+ };
388
+
389
+ const handleGitPull = async () => {
390
+ try {
391
+ const res = await fetch("/api/git", {
392
+ method: "POST",
393
+ headers: { "Content-Type": "application/json" },
394
+ body: JSON.stringify({ action: "pull", rootPath: workspaceRoot })
395
+ });
396
+ if (res.ok) {
397
+ const data = await res.json();
398
+ alert(data.output || "Git pull completed successfully.");
399
+ await loadGitStatus();
400
+ } else {
401
+ const data = await res.json();
402
+ alert(data.error || "Git pull failed");
403
+ }
404
+ } catch (e) {
405
+ console.error(e);
406
+ }
407
+ };
408
+
409
+ const handleGitPush = async () => {
410
+ try {
411
+ const res = await fetch("/api/git", {
412
+ method: "POST",
413
+ headers: { "Content-Type": "application/json" },
414
+ body: JSON.stringify({ action: "push", rootPath: workspaceRoot })
415
+ });
416
+ if (res.ok) {
417
+ const data = await res.json();
418
+ alert(data.output || "Git push completed successfully.");
419
+ await loadGitStatus();
420
+ } else {
421
+ const data = await res.json();
422
+ alert(data.error || "Git push failed");
423
+ }
424
+ } catch (e) {
425
+ console.error(e);
426
+ }
427
+ };
428
+
429
+ const handleSelectGitFile = async (file: GitFile) => {
430
+ try {
431
+ const res = await fetch(`/api/git?action=diff&file=${encodeURIComponent(file.relativePath)}&root=${encodeURIComponent(workspaceRoot)}`);
432
+ if (res.ok) {
433
+ const data = await res.json();
434
+ if (data.success) {
435
+ const diffPath = `diff:${file.relativePath}`;
436
+ if (!openFiles.some(f => f.path === diffPath)) {
437
+ setOpenFiles(prev => [
438
+ ...prev,
439
+ {
440
+ name: `Diff: ${file.name}`,
441
+ path: diffPath,
442
+ content: data.diff,
443
+ isModified: false
444
+ }
445
+ ]);
446
+ }
447
+ setActiveFilePath(diffPath);
448
+ }
449
+ }
450
+ } catch (e) {
451
+ console.error(e);
452
+ }
453
+ };
454
+
207
455
  // Poll Session Status (logs, tokens, type)
208
456
  useEffect(() => {
209
457
  const fetchStatus = async () => {
@@ -335,9 +583,11 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
335
583
  }, [workspaceRoot, scopedDirsStr]);
336
584
 
337
585
  useEffect(() => {
338
- setTimeout(() => {
586
+ loadFiles();
587
+ const interval = setInterval(() => {
339
588
  loadFiles();
340
- }, 0);
589
+ }, 3000);
590
+ return () => clearInterval(interval);
341
591
  }, [loadFiles]);
342
592
 
343
593
  // Trigger creation dialog
@@ -557,6 +807,11 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
557
807
  };
558
808
 
559
809
  const activeFile = openFiles.find((f) => f.path === activeFilePath);
810
+ const isMarkdown = activeFile ? activeFile.path.toLowerCase().endsWith(".md") : false;
811
+ const currentMode = isMarkdown ? editorMode : "edit";
812
+
813
+ const stagedFiles = gitFiles.filter(f => f.indexStatus && f.indexStatus !== " " && f.indexStatus !== "?");
814
+ const unstagedFiles = gitFiles.filter(f => (f.worktreeStatus && f.worktreeStatus !== " ") || f.status === "??");
560
815
 
561
816
  const handleContentChange = (newVal: string) => {
562
817
  if (!activeFilePath) return;
@@ -630,7 +885,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
630
885
  };
631
886
 
632
887
  return (
633
- <div style={ideContainerStyle}>
888
+ <div id="loom-ide-container" style={ideContainerStyle}>
634
889
  {/* Activity Bar (Sidebar leftmost panel icons) */}
635
890
  <div style={activityBarStyle}>
636
891
  <div style={topIconsStyle}>
@@ -726,7 +981,8 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
726
981
 
727
982
  {/* Primary Sidebar (File Tree Panel) */}
728
983
  {explorerVisible && (
729
- <div style={sidebarPanelStyle}>
984
+ <>
985
+ <div style={{ ...sidebarPanelStyle, width: `${sidebarWidth}px`, borderRight: "none" }}>
730
986
  <div style={sidebarHeaderStyle}>
731
987
  <span style={{ fontWeight: 700, fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.05em" }}>
732
988
  {activeTab === "explorer" && "Workspace Explorer"}
@@ -855,76 +1111,144 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
855
1111
  )}
856
1112
 
857
1113
  {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.
1114
+ <div style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
1115
+ {/* Branch and Sync header */}
1116
+ <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 }}>
1117
+ <div style={{ display: "flex", alignItems: "center", gap: "0.4rem", fontSize: "0.75rem", fontWeight: 600, overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis", flex: 1 }}>
1118
+ <span style={{ color: "var(--primary)", flexShrink: 0 }}>🌿</span>
1119
+ <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={gitBranch}>
1120
+ {isGitRepo ? (gitBranch || "HEAD detached") : "No Repository"}
1121
+ </span>
869
1122
  </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}
1123
+ {isGitRepo && (
1124
+ <div style={{ display: "flex", alignItems: "center", gap: "0.35rem", flexShrink: 0 }}>
1125
+ {gitSync.ahead > 0 && <span style={{ fontSize: "0.65rem", color: "#10b981", fontWeight: 700 }} title="Commits ahead of remote">↑{gitSync.ahead}</span>}
1126
+ {gitSync.behind > 0 && <span style={{ fontSize: "0.65rem", color: "#ef4444", fontWeight: 700 }} title="Commits behind remote">↓{gitSync.behind}</span>}
1127
+ <button
1128
+ onClick={handleGitPull}
1129
+ title="Pull changes"
1130
+ style={gitIconButtonStyle}
1131
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.08)"}
1132
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.02)"}
1133
+ >
1134
+ 📥
1135
+ </button>
1136
+ <button
1137
+ onClick={handleGitPush}
1138
+ title="Push commits"
1139
+ style={gitIconButtonStyle}
1140
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.08)"}
1141
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = "rgba(255,255,255,0.02)"}
1142
+ >
1143
+ 📤
1144
+ </button>
1145
+ </div>
1146
+ )}
1147
+ </div>
1148
+
1149
+ {isGitRepo ? (
1150
+ <div style={{ flex: 1, overflowY: "auto", padding: "0.5rem 1rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
1151
+
1152
+ {/* Commit Box */}
1153
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.4rem" }}>
1154
+ <textarea
1155
+ value={commitMessage}
1156
+ onChange={(e) => setCommitMessage(e.target.value)}
1157
+ placeholder="Commit message (Ctrl+Enter to commit)..."
1158
+ style={commitTextareaStyle}
1159
+ onKeyDown={(e) => {
1160
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
1161
+ e.preventDefault();
1162
+ handleGitCommit();
1163
+ }
1164
+ }}
1165
+ />
1166
+ <button
1167
+ onClick={handleGitCommit}
1168
+ disabled={gitFiles.length === 0}
1169
+ style={{
1170
+ ...commitButtonStyle,
1171
+ opacity: gitFiles.length > 0 ? 1 : 0.5,
1172
+ cursor: gitFiles.length > 0 ? "pointer" : "not-allowed"
1173
+ }}
1174
+ >
1175
+ Commit
1176
+ </button>
1177
+ </div>
1178
+
1179
+ {/* Staged Changes Section */}
1180
+ {stagedFiles.length > 0 && (
1181
+ <div>
1182
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "0.35rem" }}>
1183
+ <span style={{ fontSize: "0.7rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)" }}>
1184
+ Staged Changes ({stagedFiles.length})
924
1185
  </span>
1186
+ <button
1187
+ onClick={() => handleGitUnstageAll()}
1188
+ title="Unstage All"
1189
+ style={actionHeaderBtnStyle}
1190
+ >
1191
+
1192
+ </button>
925
1193
  </div>
926
- );
927
- })}
1194
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.25rem" }}>
1195
+ {stagedFiles.map((file) => (
1196
+ <GitFileRow
1197
+ key={file.relativePath}
1198
+ file={file}
1199
+ isStaged={true}
1200
+ onAction={handleGitUnstage}
1201
+ onSelect={handleSelectGitFile}
1202
+ />
1203
+ ))}
1204
+ </div>
1205
+ </div>
1206
+ )}
1207
+
1208
+ {/* Changes (Unstaged) Section */}
1209
+ <div>
1210
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "0.35rem" }}>
1211
+ <span style={{ fontSize: "0.7rem", fontWeight: 700, textTransform: "uppercase", color: "var(--muted)" }}>
1212
+ Changes ({unstagedFiles.length})
1213
+ </span>
1214
+ {unstagedFiles.length > 0 && (
1215
+ <button
1216
+ onClick={() => handleGitStageAll()}
1217
+ title="Stage All"
1218
+ style={actionHeaderBtnStyle}
1219
+ >
1220
+
1221
+ </button>
1222
+ )}
1223
+ </div>
1224
+ {unstagedFiles.length === 0 && stagedFiles.length === 0 ? (
1225
+ <div style={{ color: "var(--success)", fontSize: "0.75rem", padding: "0.5rem 0" }}>
1226
+ ✔ Workspace clean.
1227
+ </div>
1228
+ ) : unstagedFiles.length === 0 ? (
1229
+ <div style={{ color: "var(--muted)", fontSize: "0.7rem", fontStyle: "italic", padding: "0.25rem 0" }}>
1230
+ No unstaged changes.
1231
+ </div>
1232
+ ) : (
1233
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.25rem" }}>
1234
+ {unstagedFiles.map((file) => (
1235
+ <GitFileRow
1236
+ key={file.relativePath}
1237
+ file={file}
1238
+ isStaged={false}
1239
+ onAction={handleGitStage}
1240
+ onSelect={handleSelectGitFile}
1241
+ onIgnore={handleGitIgnore}
1242
+ onDiscard={handleGitDiscard}
1243
+ />
1244
+ ))}
1245
+ </div>
1246
+ )}
1247
+ </div>
1248
+ </div>
1249
+ ) : (
1250
+ <div style={{ padding: "1rem", color: "var(--muted)", fontSize: "0.75rem", fontStyle: "italic" }}>
1251
+ Not a git repository.
928
1252
  </div>
929
1253
  )}
930
1254
  </div>
@@ -1129,7 +1453,26 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1129
1453
  )}
1130
1454
  </div>
1131
1455
  </div>
1132
- )}
1456
+ <div
1457
+ onMouseDown={startResizingSidebar}
1458
+ style={{
1459
+ width: "4px",
1460
+ cursor: "col-resize",
1461
+ backgroundColor: isResizingSidebar ? "var(--primary)" : "var(--border)",
1462
+ transition: "background-color 0.15s ease",
1463
+ zIndex: 10,
1464
+ alignSelf: "stretch",
1465
+ }}
1466
+ onMouseEnter={(e) => {
1467
+ if (!isResizingSidebar) e.currentTarget.style.backgroundColor = "var(--border-focus)";
1468
+ }}
1469
+ onMouseLeave={(e) => {
1470
+ if (!isResizingSidebar) e.currentTarget.style.backgroundColor = "var(--border)";
1471
+ }}
1472
+ title="Drag to resize sidebar"
1473
+ />
1474
+ </>
1475
+ )}
1133
1476
 
1134
1477
  {/* Editor & Console Split View */}
1135
1478
  <div style={mainContentAreaStyle}>
@@ -1178,7 +1521,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1178
1521
  </div>
1179
1522
 
1180
1523
  {/* Editor Panel (top half) */}
1181
- <div style={editorPanelStyle}>
1524
+ <div style={{ ...editorPanelStyle, borderBottom: "none" }}>
1182
1525
  {/* Tabs header */}
1183
1526
  <div style={tabHeaderStyle}>
1184
1527
  <div style={tabsWrapperStyle}>
@@ -1210,17 +1553,59 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1210
1553
  </div>
1211
1554
 
1212
1555
  {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>
1556
+ <div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
1557
+ {isMarkdown && (
1558
+ <div style={modeToggleContainerStyle}>
1559
+ <button
1560
+ onClick={() => setEditorMode('edit')}
1561
+ style={{
1562
+ ...modeToggleItemStyle,
1563
+ backgroundColor: currentMode === 'edit' ? 'rgba(255,255,255,0.08)' : 'transparent',
1564
+ color: currentMode === 'edit' ? 'var(--foreground)' : 'var(--muted)',
1565
+ borderColor: currentMode === 'edit' ? 'var(--border-focus)' : 'transparent',
1566
+ }}
1567
+ title="Edit file"
1568
+ >
1569
+ ✏️ Edit
1570
+ </button>
1571
+ <button
1572
+ onClick={() => setEditorMode('split')}
1573
+ style={{
1574
+ ...modeToggleItemStyle,
1575
+ backgroundColor: currentMode === 'split' ? 'rgba(255,255,255,0.08)' : 'transparent',
1576
+ color: currentMode === 'split' ? 'var(--foreground)' : 'var(--muted)',
1577
+ borderColor: currentMode === 'split' ? 'var(--border-focus)' : 'transparent',
1578
+ }}
1579
+ title="Split editor & preview"
1580
+ >
1581
+ 🥞 Split
1582
+ </button>
1583
+ <button
1584
+ onClick={() => setEditorMode('preview')}
1585
+ style={{
1586
+ ...modeToggleItemStyle,
1587
+ backgroundColor: currentMode === 'preview' ? 'rgba(255,255,255,0.08)' : 'transparent',
1588
+ color: currentMode === 'preview' ? 'var(--foreground)' : 'var(--muted)',
1589
+ borderColor: currentMode === 'preview' ? 'var(--border-focus)' : 'transparent',
1590
+ }}
1591
+ title="Preview rendered document"
1592
+ >
1593
+ 👁️ Preview
1594
+ </button>
1595
+ </div>
1596
+ )}
1597
+ <button
1598
+ onClick={handleSaveFile}
1599
+ disabled={!activeFile.isModified}
1600
+ style={{
1601
+ ...saveButtonStyle,
1602
+ opacity: activeFile.isModified ? 1 : 0.5,
1603
+ cursor: activeFile.isModified ? "pointer" : "not-allowed",
1604
+ }}
1605
+ >
1606
+ 💾 Save (Ctrl+S)
1607
+ </button>
1608
+ </div>
1224
1609
  )}
1225
1610
  </div>
1226
1611
 
@@ -1249,6 +1634,33 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1249
1634
  );
1250
1635
  })}
1251
1636
  </div>
1637
+ ) : isMarkdown ? (
1638
+ currentMode === 'preview' ? (
1639
+ <div style={{ height: "100%", overflow: "hidden" }}>
1640
+ <MarkdownPreview content={activeFile.content} />
1641
+ </div>
1642
+ ) : currentMode === 'split' ? (
1643
+ <div style={{ display: "flex", width: "100%", height: "100%", overflow: "hidden" }}>
1644
+ <div style={{ flex: 1, height: "100%" }}>
1645
+ <textarea
1646
+ value={activeFile.content}
1647
+ onChange={(e) => handleContentChange(e.target.value)}
1648
+ style={textareaStyle}
1649
+ spellCheck={false}
1650
+ />
1651
+ </div>
1652
+ <div style={{ flex: 1, height: "100%", overflow: "hidden" }}>
1653
+ <MarkdownPreview content={activeFile.content} />
1654
+ </div>
1655
+ </div>
1656
+ ) : (
1657
+ <textarea
1658
+ value={activeFile.content}
1659
+ onChange={(e) => handleContentChange(e.target.value)}
1660
+ style={textareaStyle}
1661
+ spellCheck={false}
1662
+ />
1663
+ )
1252
1664
  ) : (
1253
1665
  <textarea
1254
1666
  value={activeFile.content}
@@ -1272,8 +1684,28 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1272
1684
  </div>
1273
1685
  </div>
1274
1686
 
1687
+ {/* Vertical divider */}
1688
+ <div
1689
+ onMouseDown={startResizingTerminal}
1690
+ style={{
1691
+ height: "4px",
1692
+ cursor: "row-resize",
1693
+ backgroundColor: isResizingTerminal ? "var(--primary)" : "var(--border)",
1694
+ transition: "background-color 0.15s ease",
1695
+ zIndex: 10,
1696
+ width: "100%",
1697
+ }}
1698
+ onMouseEnter={(e) => {
1699
+ if (!isResizingTerminal) e.currentTarget.style.backgroundColor = "var(--border-focus)";
1700
+ }}
1701
+ onMouseLeave={(e) => {
1702
+ if (!isResizingTerminal) e.currentTarget.style.backgroundColor = "var(--border)";
1703
+ }}
1704
+ title="Drag to resize console"
1705
+ />
1706
+
1275
1707
  {/* Console / PTY Bridge Panel (bottom half) */}
1276
- <div style={terminalPanelStyle}>
1708
+ <div style={{ ...terminalPanelStyle, flex: "none", height: `${terminalHeight}px` }}>
1277
1709
  {/* Dashboard control headers injected above xterm */}
1278
1710
  <div style={terminalHeaderStyle}>
1279
1711
  <span style={{ display: "flex", alignItems: "center", gap: "0.5rem", fontWeight: 600, fontSize: "0.8rem" }}>
@@ -1410,7 +1842,7 @@ export default function IdeLayout({ sessionId, workspaceRoot, scopedDirs, onClos
1410
1842
  </div>
1411
1843
  </div>
1412
1844
 
1413
- <div style={{ ...terminalConsoleWrapperStyle, display: "flex", width: "100%", height: "100%" }}>
1845
+ <div style={{ ...terminalConsoleWrapperStyle, display: "flex", width: "100%" }}>
1414
1846
  <div style={{ flex: 1, height: "100%", borderRight: splitSessionId ? "1px solid var(--border)" : "none", overflow: "hidden" }}>
1415
1847
  <TerminalConsole sessionId={sessionId} height="100%" wsPort={wsPort} />
1416
1848
  </div>
@@ -1617,7 +2049,7 @@ const sidebarIconStyle: React.CSSProperties = {
1617
2049
  };
1618
2050
 
1619
2051
  const sidebarPanelStyle: React.CSSProperties = {
1620
- width: "260px",
2052
+ width: "300px",
1621
2053
  backgroundColor: "#080c14",
1622
2054
  borderRight: "1px solid var(--border)",
1623
2055
  display: "flex",
@@ -2209,3 +2641,199 @@ const sessionTabItemStyle: React.CSSProperties = {
2209
2641
  cursor: "pointer",
2210
2642
  transition: "all 0.15s ease",
2211
2643
  };
2644
+
2645
+ const modeToggleContainerStyle: React.CSSProperties = {
2646
+ display: "flex",
2647
+ alignItems: "center",
2648
+ backgroundColor: "rgba(0, 0, 0, 0.3)",
2649
+ border: "1px solid var(--border)",
2650
+ borderRadius: "6px",
2651
+ padding: "2px",
2652
+ marginRight: "0.5rem",
2653
+ };
2654
+
2655
+ const modeToggleItemStyle: React.CSSProperties = {
2656
+ padding: "0.2rem 0.5rem",
2657
+ borderRadius: "4px",
2658
+ border: "1px solid transparent",
2659
+ fontSize: "0.65rem",
2660
+ fontWeight: 600,
2661
+ cursor: "pointer",
2662
+ transition: "all 0.15s ease",
2663
+ display: "flex",
2664
+ alignItems: "center",
2665
+ gap: "3px",
2666
+ };
2667
+
2668
+ interface GitFileRowProps {
2669
+ file: GitFile;
2670
+ isStaged: boolean;
2671
+ onAction: (file: string) => void;
2672
+ onSelect: (file: GitFile) => void;
2673
+ onIgnore?: (file: string) => void;
2674
+ onDiscard?: (file: string) => void;
2675
+ }
2676
+
2677
+ function GitFileRow({ file, isStaged, onAction, onSelect, onIgnore, onDiscard }: GitFileRowProps) {
2678
+ const [hovered, setHovered] = useState(false);
2679
+ let badgeColor = "#eab308";
2680
+ const displayStatus = isStaged ? (file.indexStatus || "M") : (file.worktreeStatus || "M");
2681
+
2682
+ if (displayStatus === "??" || displayStatus === "?") badgeColor = "#10b981";
2683
+ if (displayStatus === "A") badgeColor = "#10b981";
2684
+ if (displayStatus === "D") badgeColor = "#ef4444";
2685
+
2686
+ return (
2687
+ <div
2688
+ onClick={() => onSelect(file)}
2689
+ onMouseEnter={() => setHovered(true)}
2690
+ onMouseLeave={() => setHovered(false)}
2691
+ style={{
2692
+ display: "flex",
2693
+ alignItems: "center",
2694
+ justifyContent: "space-between",
2695
+ padding: "0.35rem 0.5rem",
2696
+ borderRadius: "4px",
2697
+ backgroundColor: "rgba(255,255,255,0.02)",
2698
+ cursor: "pointer",
2699
+ fontSize: "0.75rem",
2700
+ fontFamily: "var(--font-mono)",
2701
+ position: "relative"
2702
+ }}
2703
+ >
2704
+ <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1, marginRight: "0.5rem" }} title={file.relativePath}>
2705
+ {file.name}
2706
+ </span>
2707
+
2708
+ <div style={{ display: "flex", alignItems: "center", gap: "0.35rem" }}>
2709
+ {hovered && (
2710
+ <div style={{ display: "flex", gap: "0.25rem" }}>
2711
+ {!isStaged && onIgnore && (
2712
+ <button
2713
+ onClick={(e) => {
2714
+ e.stopPropagation();
2715
+ onIgnore(file.relativePath);
2716
+ }}
2717
+ title="Add to .gitignore"
2718
+ style={{
2719
+ background: "none",
2720
+ border: "none",
2721
+ cursor: "pointer",
2722
+ fontSize: "0.65rem",
2723
+ color: "var(--muted)",
2724
+ padding: "1px 4px",
2725
+ borderRadius: "3px",
2726
+ backgroundColor: "rgba(255,255,255,0.05)"
2727
+ }}
2728
+ onMouseEnter={(e) => e.currentTarget.style.color = "var(--danger)"}
2729
+ onMouseLeave={(e) => e.currentTarget.style.color = "var(--muted)"}
2730
+ >
2731
+ 🚫
2732
+ </button>
2733
+ )}
2734
+ {!isStaged && onDiscard && (
2735
+ <button
2736
+ onClick={(e) => {
2737
+ e.stopPropagation();
2738
+ onDiscard(file.relativePath);
2739
+ }}
2740
+ title="Discard changes"
2741
+ style={{
2742
+ background: "none",
2743
+ border: "none",
2744
+ cursor: "pointer",
2745
+ fontSize: "0.65rem",
2746
+ color: "var(--muted)",
2747
+ padding: "1px 4px",
2748
+ borderRadius: "3px",
2749
+ backgroundColor: "rgba(255,255,255,0.05)"
2750
+ }}
2751
+ onMouseEnter={(e) => e.currentTarget.style.color = "var(--danger)"}
2752
+ onMouseLeave={(e) => e.currentTarget.style.color = "var(--muted)"}
2753
+ >
2754
+ 🗑️
2755
+ </button>
2756
+ )}
2757
+ <button
2758
+ onClick={(e) => {
2759
+ e.stopPropagation();
2760
+ onAction(file.relativePath);
2761
+ }}
2762
+ title={isStaged ? "Unstage Change" : "Stage Change"}
2763
+ style={{
2764
+ background: "none",
2765
+ border: "none",
2766
+ cursor: "pointer",
2767
+ fontSize: "0.65rem",
2768
+ color: "var(--muted)",
2769
+ padding: "1px 4px",
2770
+ borderRadius: "3px",
2771
+ backgroundColor: "rgba(255,255,255,0.05)"
2772
+ }}
2773
+ onMouseEnter={(e) => e.currentTarget.style.color = "var(--foreground)"}
2774
+ onMouseLeave={(e) => e.currentTarget.style.color = "var(--muted)"}
2775
+ >
2776
+ {isStaged ? "➖" : "➕"}
2777
+ </button>
2778
+ </div>
2779
+ )}
2780
+ <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" }}>
2781
+ {displayStatus.substring(0, 1)}
2782
+ </span>
2783
+ </div>
2784
+ </div>
2785
+ );
2786
+ }
2787
+
2788
+ const commitTextareaStyle: React.CSSProperties = {
2789
+ width: "100%",
2790
+ minHeight: "60px",
2791
+ backgroundColor: "#05070a",
2792
+ border: "1px solid var(--border)",
2793
+ borderRadius: "6px",
2794
+ padding: "0.5rem",
2795
+ color: "#e2e8f0",
2796
+ fontSize: "0.75rem",
2797
+ resize: "vertical",
2798
+ outline: "none",
2799
+ fontFamily: "var(--font-sans), sans-serif",
2800
+ };
2801
+
2802
+ const commitButtonStyle: React.CSSProperties = {
2803
+ width: "100%",
2804
+ padding: "0.35rem",
2805
+ backgroundColor: "var(--primary)",
2806
+ border: "none",
2807
+ color: "#ffffff",
2808
+ borderRadius: "6px",
2809
+ fontSize: "0.75rem",
2810
+ fontWeight: 600,
2811
+ cursor: "pointer",
2812
+ transition: "opacity 0.1s ease",
2813
+ };
2814
+
2815
+ const gitIconButtonStyle: React.CSSProperties = {
2816
+ background: "none",
2817
+ border: "1px solid var(--border)",
2818
+ borderRadius: "4px",
2819
+ padding: "2px 5px",
2820
+ cursor: "pointer",
2821
+ fontSize: "0.7rem",
2822
+ display: "flex",
2823
+ alignItems: "center",
2824
+ justifyContent: "center",
2825
+ backgroundColor: "rgba(255,255,255,0.02)",
2826
+ };
2827
+
2828
+ const actionHeaderBtnStyle: React.CSSProperties = {
2829
+ background: "none",
2830
+ border: "none",
2831
+ cursor: "pointer",
2832
+ fontSize: "0.65rem",
2833
+ color: "var(--muted)",
2834
+ padding: "2px 4px",
2835
+ borderRadius: "3px",
2836
+ display: "flex",
2837
+ alignItems: "center",
2838
+ justifyContent: "center"
2839
+ };