neural-loom 0.2.70 → 0.3.0

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 (232) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/cache/.previewinfo +1 -1
  4. package/.next/cache/.rscinfo +1 -1
  5. package/.next/cache/.tsbuildinfo +1 -1
  6. package/.next/diagnostics/route-bundle-stats.json +11 -11
  7. package/.next/fallback-build-manifest.json +3 -3
  8. package/.next/next-server.js.nft.json +1 -1
  9. package/.next/prerender-manifest.json +3 -3
  10. package/.next/required-server-files.js +20 -20
  11. package/.next/required-server-files.json +20 -20
  12. package/.next/server/app/_global-error.html +1 -1
  13. package/.next/server/app/_global-error.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  18. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.html +1 -1
  20. package/.next/server/app/_not-found.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  22. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  23. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  24. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  25. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  26. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  27. package/.next/server/app/api/configs/route.js.nft.json +1 -1
  28. package/.next/server/app/api/context/aider/route.js.nft.json +1 -1
  29. package/.next/server/app/api/context/claude/route.js.nft.json +1 -1
  30. package/.next/server/app/api/context/route.js.nft.json +1 -1
  31. package/.next/server/app/api/context/ssh/route.js.nft.json +1 -1
  32. package/.next/server/app/api/files/route.js +1 -1
  33. package/.next/server/app/api/files/route.js.nft.json +1 -1
  34. package/.next/server/app/api/recents/route.js.nft.json +1 -1
  35. package/.next/server/app/api/sessions/inject/route.js.nft.json +1 -1
  36. package/.next/server/app/api/sessions/input/route.js.nft.json +1 -1
  37. package/.next/server/app/api/sessions/launch/route.js.nft.json +1 -1
  38. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  39. package/.next/server/app/api/sessions/scope/route.js.nft.json +1 -1
  40. package/.next/server/app/api/sessions/start/route.js.nft.json +1 -1
  41. package/.next/server/app/api/sessions/stats/route.js.nft.json +1 -1
  42. package/.next/server/app/api/sessions/stop/route.js.nft.json +1 -1
  43. package/.next/server/app/api/sessions/upload-image/route.js.nft.json +1 -1
  44. package/.next/server/app/api/sql/connections/route.js.nft.json +1 -1
  45. package/.next/server/app/api/sql/mcp/route.js +1 -1
  46. package/.next/server/app/api/sql/mcp/route.js.nft.json +1 -1
  47. package/.next/server/app/api/sql/query/route.js +1 -1
  48. package/.next/server/app/api/sql/query/route.js.nft.json +1 -1
  49. package/.next/server/app/api/sql/schema/route.js.nft.json +1 -1
  50. package/.next/server/app/api/telemetry/route.js +1 -1
  51. package/.next/server/app/api/telemetry/route.js.nft.json +1 -1
  52. package/.next/server/app/index.html +1 -1
  53. package/.next/server/app/index.rsc +2 -2
  54. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  55. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  56. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  57. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  58. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  59. package/.next/server/app/page/react-loadable-manifest.json +2 -2
  60. package/.next/server/app/page_client-reference-manifest.js +1 -1
  61. package/.next/server/chunks/[root-of-the-server]__0.hmw1f._.js +3 -0
  62. package/.next/server/chunks/[root-of-the-server]__0.hmw1f._.js.map +1 -0
  63. package/.next/server/chunks/[root-of-the-server]__00-sqou._.js +2 -2
  64. package/.next/server/chunks/[root-of-the-server]__00-sqou._.js.map +1 -1
  65. package/.next/server/chunks/[root-of-the-server]__00l5btc._.js +4 -4
  66. package/.next/server/chunks/[root-of-the-server]__00l5btc._.js.map +1 -1
  67. package/.next/server/chunks/[root-of-the-server]__06men8g._.js +4 -4
  68. package/.next/server/chunks/[root-of-the-server]__06men8g._.js.map +1 -1
  69. package/.next/server/chunks/[root-of-the-server]__097vtn1._.js +2 -2
  70. package/.next/server/chunks/[root-of-the-server]__097vtn1._.js.map +1 -1
  71. package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js +2 -2
  72. package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js.map +1 -1
  73. package/.next/server/chunks/{[root-of-the-server]__0.ptwor._.js → [root-of-the-server]__0ajln56._.js} +7 -7
  74. package/.next/server/chunks/[root-of-the-server]__0ajln56._.js.map +1 -0
  75. package/.next/server/chunks/{[root-of-the-server]__0co.g5k._.js → [root-of-the-server]__0auh-2z._.js} +5 -5
  76. package/.next/server/chunks/[root-of-the-server]__0auh-2z._.js.map +1 -0
  77. package/.next/server/chunks/[root-of-the-server]__0e03ite._.js +2 -2
  78. package/.next/server/chunks/[root-of-the-server]__0e03ite._.js.map +1 -1
  79. package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js +2 -2
  80. package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js.map +1 -1
  81. package/.next/server/chunks/[root-of-the-server]__0h.~hv6._.js +2 -2
  82. package/.next/server/chunks/[root-of-the-server]__0h.~hv6._.js.map +1 -1
  83. package/.next/server/chunks/[root-of-the-server]__0i83~95._.js +1 -1
  84. package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js +19 -19
  85. package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js.map +1 -1
  86. package/.next/server/chunks/[root-of-the-server]__0s1axek._.js +5 -5
  87. package/.next/server/chunks/[root-of-the-server]__0s1axek._.js.map +1 -1
  88. package/.next/server/chunks/[root-of-the-server]__0t713sv._.js +1 -1
  89. package/.next/server/chunks/[root-of-the-server]__0t713sv._.js.map +1 -1
  90. package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js +5 -5
  91. package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js.map +1 -1
  92. package/.next/server/chunks/[root-of-the-server]__0viil6b._.js +2 -2
  93. package/.next/server/chunks/[root-of-the-server]__0viil6b._.js.map +1 -1
  94. package/.next/server/chunks/[root-of-the-server]__0ykd4~u._.js +3 -0
  95. package/.next/server/chunks/[root-of-the-server]__0ykd4~u._.js.map +1 -0
  96. package/.next/server/chunks/[root-of-the-server]__11h_nxt._.js +1 -1
  97. package/.next/server/chunks/[root-of-the-server]__11h_nxt._.js.map +1 -1
  98. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_016qk~0.js +2 -2
  99. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_016qk~0.js.map +1 -1
  100. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js +1 -1
  101. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js.map +1 -1
  102. package/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js +11 -0
  103. package/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js.map +1 -0
  104. package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js +3 -0
  105. package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js.map +1 -0
  106. package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js.map +1 -0
  107. package/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js +3 -0
  108. package/.next/server/middleware/middleware-manifest.json +32 -0
  109. package/.next/server/middleware-build-manifest.js +3 -3
  110. package/.next/server/middleware-manifest.json +30 -2
  111. package/.next/server/pages/404.html +1 -1
  112. package/.next/server/pages/500.html +1 -1
  113. package/.next/server/server-reference-manifest.js +1 -1
  114. package/.next/server/server-reference-manifest.json +1 -1
  115. package/.next/static/04j4ht7eCOOXSLQ5Wixq0/_clientMiddlewareManifest.js +6 -0
  116. package/.next/static/chunks/{0fxouf2n_5adf.js → 0uyuco-451j.n.js} +2 -2
  117. package/.next/static/chunks/{0r3x2gcn0sde..js → 14~-ojwsxf~oe.js} +4 -4
  118. package/.next/trace +1 -1
  119. package/.next/trace-build +1 -1
  120. package/README.md +18 -0
  121. package/bin/cli.js +12 -2
  122. package/package.json +9 -6
  123. package/src/app/api/files/route.ts +30 -0
  124. package/src/app/api/git/route.ts +46 -31
  125. package/src/app/api/sessions/stats/route.ts +40 -22
  126. package/src/app/api/sql/query/route.ts +2 -15
  127. package/src/app/api/telemetry/route.ts +2 -1
  128. package/src/app/components/TerminalConsole.tsx +103 -39
  129. package/src/lib/agents/AgentRunner.ts +41 -5
  130. package/src/lib/agents/AiderRunner.ts +20 -36
  131. package/src/lib/agents/ClaudeRunner.ts +8 -62
  132. package/src/lib/agents/DockerRunner.ts +18 -54
  133. package/src/lib/agents/MockRunner.ts +3 -1
  134. package/src/lib/agents/SSHRunner.ts +8 -54
  135. package/src/lib/agents/SessionPersister.ts +11 -1
  136. package/src/lib/agents/ShellRunner.ts +10 -46
  137. package/src/lib/agents/WebSocketServer.ts +17 -2
  138. package/src/lib/agents/pricing.ts +45 -0
  139. package/src/lib/agents/runtime.ts +92 -0
  140. package/src/lib/gitSafe.ts +15 -0
  141. package/src/lib/security.ts +76 -0
  142. package/src/lib/sql/McpServer.ts +2 -11
  143. package/src/lib/sql/adapters/MssqlAdapter.ts +16 -1
  144. package/src/lib/sql/adapters/MysqlAdapter.ts +23 -1
  145. package/src/lib/sql/adapters/PostgresAdapter.ts +22 -1
  146. package/src/lib/sql/adapters/SqliteAdapter.ts +21 -10
  147. package/src/lib/sql/sqlGuard.ts +20 -0
  148. package/src/lib/workspaceParser.ts +23 -6
  149. package/src/middleware.ts +27 -0
  150. package/.next/dev/build-manifest.json +0 -50
  151. package/.next/dev/fallback-build-manifest.json +0 -37
  152. package/.next/dev/logs/next-development.log +0 -3737
  153. package/.next/dev/server/chunks/[root-of-the-server]__11uf~fd._.js +0 -2396
  154. package/.next/dev/server/chunks/[root-of-the-server]__11uf~fd._.js.map +0 -20
  155. package/.next/dev/server/chunks/ssr/[externals]__0.o~_~l._.js +0 -32
  156. package/.next/dev/server/chunks/ssr/[externals]__0.o~_~l._.js.map +0 -5
  157. package/.next/dev/server/chunks/ssr/[externals]_next_dist_shared_lib_no-fallback-error_external_0fgm~bn.js +0 -8
  158. package/.next/dev/server/chunks/ssr/[externals]_next_dist_shared_lib_no-fallback-error_external_0fgm~bn.js.map +0 -5
  159. package/.next/dev/server/chunks/ssr/[root-of-the-server]__0je5zm2._.js +0 -285
  160. package/.next/dev/server/chunks/ssr/[root-of-the-server]__0je5zm2._.js.map +0 -9
  161. package/.next/dev/server/chunks/ssr/node_modules_01ed9g5._.js +0 -6621
  162. package/.next/dev/server/chunks/ssr/node_modules_01ed9g5._.js.map +0 -76
  163. package/.next/dev/server/chunks/ssr/node_modules_129w7gn._.js +0 -4399
  164. package/.next/dev/server/chunks/ssr/node_modules_129w7gn._.js.map +0 -35
  165. package/.next/dev/server/chunks/ssr/node_modules_next_12i4inu._.js +0 -55
  166. package/.next/dev/server/chunks/ssr/node_modules_next_12i4inu._.js.map +0 -7
  167. package/.next/dev/server/middleware-build-manifest.js +0 -50
  168. package/.next/dev/server/next-font-manifest.js +0 -1
  169. package/.next/dev/server/next-font-manifest.json +0 -11
  170. package/.next/dev/server/pages/_app/build-manifest.json +0 -23
  171. package/.next/dev/server/pages/_app/client-build-manifest.json +0 -5
  172. package/.next/dev/server/pages/_app/next-font-manifest.json +0 -6
  173. package/.next/dev/server/pages/_app/pages-manifest.json +0 -3
  174. package/.next/dev/server/pages/_app/react-loadable-manifest.json +0 -1
  175. package/.next/dev/server/pages/_app.js +0 -4
  176. package/.next/dev/server/pages/_app.js.map +0 -5
  177. package/.next/dev/server/pages/_document/next-font-manifest.json +0 -6
  178. package/.next/dev/server/pages/_document/pages-manifest.json +0 -3
  179. package/.next/dev/server/pages/_document/react-loadable-manifest.json +0 -1
  180. package/.next/dev/server/pages/_document.js +0 -5
  181. package/.next/dev/server/pages/_document.js.map +0 -5
  182. package/.next/dev/server/pages/_error/build-manifest.json +0 -22
  183. package/.next/dev/server/pages/_error/client-build-manifest.json +0 -5
  184. package/.next/dev/server/pages/_error/next-font-manifest.json +0 -6
  185. package/.next/dev/server/pages/_error/pages-manifest.json +0 -3
  186. package/.next/dev/server/pages/_error/react-loadable-manifest.json +0 -1
  187. package/.next/dev/server/pages/_error.js +0 -8
  188. package/.next/dev/server/pages/_error.js.map +0 -5
  189. package/.next/dev/server/pages-manifest.json +0 -5
  190. package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js +0 -23
  191. package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js.map +0 -6
  192. package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js +0 -23
  193. package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js.map +0 -6
  194. package/.next/dev/static/chunks/[root-of-the-server]__01mw43t._.js +0 -476
  195. package/.next/dev/static/chunks/[root-of-the-server]__01mw43t._.js.map +0 -6
  196. package/.next/dev/static/chunks/[root-of-the-server]__0c0okpg._.js +0 -476
  197. package/.next/dev/static/chunks/[root-of-the-server]__0c0okpg._.js.map +0 -6
  198. package/.next/dev/static/chunks/node_modules_0lx093h._.js +0 -1343
  199. package/.next/dev/static/chunks/node_modules_0lx093h._.js.map +0 -13
  200. package/.next/dev/static/chunks/node_modules_next_app_0jt-zj..js +0 -8
  201. package/.next/dev/static/chunks/node_modules_next_app_0jt-zj..js.map +0 -6
  202. package/.next/dev/static/chunks/node_modules_next_dist_0rt-2cr._.js +0 -2334
  203. package/.next/dev/static/chunks/node_modules_next_dist_0rt-2cr._.js.map +0 -26
  204. package/.next/dev/static/chunks/node_modules_next_dist_0u_w_5s._.js +0 -2165
  205. package/.next/dev/static/chunks/node_modules_next_dist_0u_w_5s._.js.map +0 -25
  206. package/.next/dev/static/chunks/node_modules_next_dist_client_0pe1dg-._.js +0 -4091
  207. package/.next/dev/static/chunks/node_modules_next_dist_client_0pe1dg-._.js.map +0 -49
  208. package/.next/dev/static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js +0 -6064
  209. package/.next/dev/static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js.map +0 -17
  210. package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0ek5eq7._.js +0 -6535
  211. package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0ek5eq7._.js.map +0 -72
  212. package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0n1wg2c._.js +0 -6294
  213. package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0n1wg2c._.js.map +0 -70
  214. package/.next/dev/static/chunks/node_modules_react-dom_0bruynb._.js +0 -15381
  215. package/.next/dev/static/chunks/node_modules_react-dom_0bruynb._.js.map +0 -9
  216. package/.next/dev/static/chunks/pages/_app.js +0 -13
  217. package/.next/dev/static/chunks/pages/_error.js +0 -12
  218. package/.next/dev/static/chunks/pages__app_0-bg2z-._.js.map +0 -11
  219. package/.next/dev/static/chunks/pages__app_07xvfw~._.js +0 -5
  220. package/.next/dev/static/chunks/pages__error_0.i_zsq._.js.map +0 -11
  221. package/.next/dev/static/chunks/pages__error_07xvfw~._.js +0 -5
  222. package/.next/dev/static/chunks/turbopack-pages__app_0-bg2z-._.js +0 -2236
  223. package/.next/dev/static/chunks/turbopack-pages__error_0.i_zsq._.js +0 -2236
  224. package/.next/server/chunks/[externals]__048y_op._.js +0 -3
  225. package/.next/server/chunks/[externals]__048y_op._.js.map +0 -1
  226. package/.next/server/chunks/[root-of-the-server]__0.ptwor._.js.map +0 -1
  227. package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js +0 -3
  228. package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js.map +0 -1
  229. package/.next/server/chunks/[root-of-the-server]__0co.g5k._.js.map +0 -1
  230. package/.next/static/llmJsGQafolZV5_sqEw4l/_clientMiddlewareManifest.js +0 -1
  231. /package/.next/static/{llmJsGQafolZV5_sqEw4l → 04j4ht7eCOOXSLQ5Wixq0}/_buildManifest.js +0 -0
  232. /package/.next/static/{llmJsGQafolZV5_sqEw4l → 04j4ht7eCOOXSLQ5Wixq0}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
 
4
- import { useEffect, useRef, useState } from "react";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
5
  import { Terminal } from "xterm";
6
6
  import { FitAddon } from "xterm-addon-fit";
7
7
  import { SearchAddon } from "@xterm/addon-search";
@@ -19,10 +19,22 @@ interface LogBufferItem {
19
19
  message?: string;
20
20
  }
21
21
 
22
- function copyTextToClipboard(text: string): boolean {
23
- if (typeof window === "undefined") return false;
24
-
25
- let successful = false;
22
+ async function copyTextToClipboard(text: string): Promise<boolean> {
23
+ if (typeof window === "undefined" || !text) return false;
24
+
25
+ // Preferred path: async Clipboard API. Reliable in a secure context, which
26
+ // includes localhost (NeuralLoom's default bind).
27
+ if (window.isSecureContext && navigator.clipboard?.writeText) {
28
+ try {
29
+ await navigator.clipboard.writeText(text);
30
+ return true;
31
+ } catch {
32
+ // Fall through to the execCommand fallback (e.g. permission denied).
33
+ }
34
+ }
35
+
36
+ // Fallback: hidden textarea + execCommand("copy") for non-secure contexts
37
+ // (e.g. accessed over a plain-HTTP LAN address).
26
38
  try {
27
39
  const textArea = document.createElement("textarea");
28
40
  textArea.value = text;
@@ -33,24 +45,13 @@ function copyTextToClipboard(text: string): boolean {
33
45
  document.body.appendChild(textArea);
34
46
  textArea.focus();
35
47
  textArea.select();
36
- successful = document.execCommand("copy");
48
+ const successful = document.execCommand("copy");
37
49
  document.body.removeChild(textArea);
50
+ return successful;
38
51
  } catch (err) {
39
- console.warn("Fallback execCommand copy failed:", err);
52
+ console.warn("Clipboard copy failed:", err);
53
+ return false;
40
54
  }
41
-
42
- if (successful) {
43
- return true;
44
- }
45
-
46
- if (navigator.clipboard && navigator.clipboard.writeText) {
47
- navigator.clipboard.writeText(text).catch((err) => {
48
- console.error("Failed to copy using clipboard API:", err);
49
- });
50
- return true;
51
- }
52
-
53
- return false;
54
55
  }
55
56
 
56
57
  export default function TerminalConsole({ sessionId, height = "400px", wsPort = 9601 }: TerminalConsoleProps) {
@@ -71,6 +72,31 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
71
72
  const [caseSensitive, setCaseSensitive] = useState(false);
72
73
  const searchAddonRef = useRef<SearchAddon | null>(null);
73
74
 
75
+ // Copy feedback ("Copied!" indicator)
76
+ const [copied, setCopied] = useState(false);
77
+ const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
78
+
79
+ // Copy the given text (or the current terminal selection) to the clipboard.
80
+ // Stable identity so the terminal effect can use it without re-initializing.
81
+ const copySelection = useCallback((text?: string): void => {
82
+ const term = xtermRef.current;
83
+ const selection = (text ?? term?.getSelection() ?? "").trim();
84
+ if (!selection) return;
85
+ copyTextToClipboard(selection).then((ok) => {
86
+ if (!ok) return;
87
+ setCopied(true);
88
+ if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
89
+ copiedTimerRef.current = setTimeout(() => setCopied(false), 1200);
90
+ });
91
+ }, []);
92
+
93
+ // Clear the copy-feedback timer on unmount
94
+ useEffect(() => {
95
+ return () => {
96
+ if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
97
+ };
98
+ }, []);
99
+
74
100
  // Focus search input when opened
75
101
  useEffect(() => {
76
102
  if (searchOpen && searchInputRef.current) {
@@ -189,27 +215,18 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
189
215
  term.focus();
190
216
 
191
217
  const container = terminalRef.current;
218
+ // Keep right-click from clearing an active selection so the context-menu
219
+ // copy can read it.
192
220
  const handleMouseDown = (e: MouseEvent) => {
193
- console.log("[TerminalConsole] mousedown - button:", e.button, "hasSelection:", term.hasSelection(), "selection:", term.getSelection());
194
- if (e.button === 2 && term.hasSelection()) {
195
- console.log("[TerminalConsole] mousedown right-click, stopping propagation");
196
- e.stopPropagation();
197
- }
221
+ if (e.button === 2 && term.hasSelection()) e.stopPropagation();
198
222
  };
199
223
  const handleMouseUp = (e: MouseEvent) => {
200
- console.log("[TerminalConsole] mouseup - button:", e.button, "hasSelection:", term.hasSelection(), "selection:", term.getSelection());
201
- if (e.button === 2 && term.hasSelection()) {
202
- console.log("[TerminalConsole] mouseup right-click, stopping propagation");
203
- e.stopPropagation();
204
- }
224
+ if (e.button === 2 && term.hasSelection()) e.stopPropagation();
205
225
  };
206
226
  const handleContextMenu = (e: MouseEvent) => {
207
- console.log("[TerminalConsole] contextmenu - hasSelection:", term.hasSelection(), "selection:", term.getSelection());
208
227
  if (term.hasSelection()) {
209
228
  e.preventDefault();
210
- const selection = term.getSelection();
211
- const success = copyTextToClipboard(selection);
212
- console.log("[TerminalConsole] contextmenu copy success:", success);
229
+ copySelection(term.getSelection());
213
230
  term.clearSelection();
214
231
  }
215
232
  };
@@ -219,6 +236,17 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
219
236
  container.addEventListener("contextmenu", handleContextMenu);
220
237
  }
221
238
 
239
+ // Copy-on-select: capture the selection the moment it is made. Claude Code's
240
+ // TUI redraws constantly, which clears the xterm selection — so we copy
241
+ // immediately (debounced) rather than waiting for a later Ctrl+C/right-click.
242
+ let selTimer: ReturnType<typeof setTimeout> | null = null;
243
+ const selDisposable = term.onSelectionChange(() => {
244
+ const sel = term.getSelection();
245
+ if (!sel || !sel.trim()) return;
246
+ if (selTimer) clearTimeout(selTimer);
247
+ selTimer = setTimeout(() => copySelection(sel), 150);
248
+ });
249
+
222
250
  // Attach search key handler and clipboard copy handler
223
251
  term.attachCustomKeyEventHandler((e) => {
224
252
  // Search: Ctrl+F
@@ -227,11 +255,20 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
227
255
  setSearchOpen(true);
228
256
  return false;
229
257
  }
230
-
231
- // Copy: Ctrl+C or Cmd+C (when selection is active)
232
- if ((e.ctrlKey || e.metaKey) && e.key === "c" && e.type === "keydown") {
258
+
259
+ // Explicit copy: Ctrl+Shift+C / Cmd+Shift+C (always, when a selection exists)
260
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "c" || e.key === "C") && e.type === "keydown") {
261
+ if (term.hasSelection()) {
262
+ copySelection(term.getSelection());
263
+ return false;
264
+ }
265
+ }
266
+
267
+ // Copy: Ctrl+C / Cmd+C only when a selection is active (otherwise let it
268
+ // pass through to the PTY as an interrupt).
269
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === "c" && e.type === "keydown") {
233
270
  if (term.hasSelection()) {
234
- copyTextToClipboard(term.getSelection());
271
+ copySelection(term.getSelection());
235
272
  return false; // Prevent sending interrupt to PTY
236
273
  }
237
274
  }
@@ -414,6 +451,8 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
414
451
  return () => {
415
452
  isDisposed = true;
416
453
  clearTimeout(fitTimeout);
454
+ if (selTimer) clearTimeout(selTimer);
455
+ selDisposable.dispose();
417
456
  resizeObserver.disconnect();
418
457
  ws.close();
419
458
  if (container) {
@@ -428,7 +467,7 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
428
467
  console.warn("Error disposing terminal:", e);
429
468
  }
430
469
  };
431
- }, [sessionId, wsPort]);
470
+ }, [sessionId, wsPort, copySelection]);
432
471
  const handlePaste = async (e: React.ClipboardEvent<HTMLDivElement>) => {
433
472
  const items = e.clipboardData?.items;
434
473
  if (!items) return;
@@ -530,6 +569,31 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
530
569
  )}
531
570
  </div>
532
571
  <div style={{ display: "flex", gap: "0.35rem", alignItems: "center" }}>
572
+ {copied && (
573
+ <span style={{
574
+ color: "#50fa7b",
575
+ fontSize: "0.65rem",
576
+ fontWeight: 600,
577
+ }}>
578
+ ✓ Copied
579
+ </span>
580
+ )}
581
+ <button
582
+ onClick={() => copySelection()}
583
+ style={{
584
+ padding: "2px 6px",
585
+ borderRadius: "3px",
586
+ border: "1px solid var(--border)",
587
+ backgroundColor: "transparent",
588
+ color: "var(--muted)",
589
+ fontSize: "0.65rem",
590
+ fontWeight: 600,
591
+ cursor: "pointer",
592
+ }}
593
+ title="Copy selected terminal text (also auto-copies on selection)"
594
+ >
595
+ 📋 Copy
596
+ </button>
533
597
  <button
534
598
  onClick={togglePause}
535
599
  style={{
@@ -1,5 +1,11 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { getConfigDir } from "../config";
4
+ import { SessionPersister } from "./SessionPersister";
5
+ import { estimateCost } from "./pricing";
6
+
7
+ /** Maximum number of log chunks retained per session (older chunks are dropped). */
8
+ export const MAX_LOG_LINES = 5000;
3
9
 
4
10
  export interface RunnerStatus {
5
11
  id: string;
@@ -40,6 +46,8 @@ export abstract class AgentRunner {
40
46
  lastSyncedWorkspaceFolders?: string[];
41
47
  tokenUsage: { input: number; output: number; cost: number } = { input: 0, output: 0, cost: 0 };
42
48
  agentState: 'idle' | 'thinking' | 'complete' | 'needs-input' = 'idle';
49
+ /** Model name, when known, used for fallback cost estimation. */
50
+ protected model?: string;
43
51
  protected hasAnalyzedInitialState = false;
44
52
  private stateTimer: NodeJS.Timeout | null = null;
45
53
  private idleTimer: NodeJS.Timeout | null = null;
@@ -64,10 +72,9 @@ export abstract class AgentRunner {
64
72
 
65
73
  if (diffInput > 0 || diffOutput > 0 || diffCost > 0) {
66
74
  try {
67
- const configDir = path.join(process.cwd(), ".ai", "neural-loom");
68
- if (!fs.existsSync(configDir)) {
69
- fs.mkdirSync(configDir, { recursive: true });
70
- }
75
+ // Use the canonical config dir (honours ORIGINAL_CWD and the read-only
76
+ // fallback) rather than process.cwd(), which is the package root under npx.
77
+ const configDir = getConfigDir();
71
78
  const historyFile = path.join(configDir, "token-history.json");
72
79
  let history: TokenTransaction[] = [];
73
80
  if (fs.existsSync(historyFile)) {
@@ -139,7 +146,7 @@ export abstract class AgentRunner {
139
146
 
140
147
  // Fallback cost estimation if input/output is updated but cost is 0/unmatched
141
148
  if (this.tokenUsage.cost === 0 && (this.tokenUsage.input > 0 || this.tokenUsage.output > 0)) {
142
- this.tokenUsage.cost = (this.tokenUsage.input * 0.000003) + (this.tokenUsage.output * 0.000015);
149
+ this.tokenUsage.cost = estimateCost(this.tokenUsage.input, this.tokenUsage.output, this.model);
143
150
  }
144
151
  this.logTokenUsageUpdate();
145
152
  }
@@ -171,6 +178,11 @@ export abstract class AgentRunner {
171
178
  // No-op by default, overridden by PTY runners
172
179
  }
173
180
 
181
+ /** OS process id of the underlying PTY, when one exists. Overridden by PTY runners. */
182
+ getPid(): number | undefined {
183
+ return undefined;
184
+ }
185
+
174
186
  protected listeners: ((data: string) => void)[] = [];
175
187
 
176
188
  public onOutput(callback: (data: string) => void): void {
@@ -186,6 +198,30 @@ export abstract class AgentRunner {
186
198
  this.listeners.forEach(cb => cb(data));
187
199
  }
188
200
 
201
+ /** Whether appendLog parses token usage / persists the session. Subclasses
202
+ * (e.g. the ephemeral workspace shell) may disable these. */
203
+ protected parseTokensOnLog = true;
204
+ protected persistOnLog = true;
205
+
206
+ /**
207
+ * Append a chunk to the session log, enforce the retention cap, emit it to
208
+ * subscribers and (per the flags above) parse token usage / persist the
209
+ * session. Shared by all PTY-backed runners.
210
+ */
211
+ protected appendLog(line: string): void {
212
+ this.logs.push(line);
213
+ if (this.logs.length > MAX_LOG_LINES) {
214
+ this.logs.shift();
215
+ }
216
+ if (this.parseTokensOnLog) {
217
+ this.parseTokenUsage(line);
218
+ }
219
+ this.emitOutput(line);
220
+ if (this.persistOnLog) {
221
+ SessionPersister.saveSession(this);
222
+ }
223
+ }
224
+
189
225
  /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
190
226
  protected async writeToPty(ptyProcess: any, text: string): Promise<void> {
191
227
  if (!ptyProcess) return;
@@ -1,32 +1,13 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { exec } from "child_process";
3
+ import { exec, execFile } from "child_process";
4
+ import { promisify } from "util";
4
5
  import { getConfigDir } from "../config";
5
6
  import type { IPty } from "node-pty";
6
7
  import { AgentRunner } from "./AgentRunner";
7
- import { SessionPersister } from "./SessionPersister";
8
+ import { isWindows, findGitBash, loadNodePty } from "./runtime";
8
9
 
9
- function isWindows(): boolean {
10
- return typeof process !== "undefined" && process.platform === "win32";
11
- }
12
-
13
- function findGitBash(): string | null {
14
- if (!isWindows()) {
15
- return "bash";
16
- }
17
- const pf = process.env.PROGRAMFILES || "C:\\Program Files";
18
- const pf86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
19
- const local = process.env.LOCALAPPDATA || "";
20
-
21
- const candidates: string[] = [
22
- process.env.WINGMAN_BASH_PATH,
23
- path.join(pf, "Git", "bin", "bash.exe"),
24
- path.join(pf86, "Git", "bin", "bash.exe"),
25
- local ? path.join(local, "Programs", "Git", "bin", "bash.exe") : "",
26
- ].filter((p): p is string => typeof p === "string" && p !== "");
27
-
28
- return candidates.find(p => fs.existsSync(p)) || null;
29
- }
10
+ const execFilePromise = promisify(execFile);
30
11
 
31
12
  function getFilesForAider(dir: string): string {
32
13
  const maxFiles = 150;
@@ -87,16 +68,6 @@ export class AiderRunner extends AgentRunner {
87
68
  this.createdAt = new Date().toISOString();
88
69
  }
89
70
 
90
- private appendLog(line: string) {
91
- this.logs.push(line);
92
- if (this.logs.length > 5000) {
93
- this.logs.shift();
94
- }
95
- this.parseTokenUsage(line);
96
- this.emitOutput(line);
97
- SessionPersister.saveSession(this);
98
- }
99
-
100
71
  async start(resume?: boolean): Promise<void> {
101
72
  if (this.status === 'running') return;
102
73
 
@@ -115,9 +86,7 @@ export class AiderRunner extends AgentRunner {
115
86
  const shellArgs = useCmd ? ["/c"] : ["-c"];
116
87
 
117
88
  try {
118
- if (!this.ptyModule) {
119
- this.ptyModule = eval("require")("node-pty");
120
- }
89
+ this.ptyModule = loadNodePty();
121
90
 
122
91
  // Load Aider configuration
123
92
  let aiderConfig = {
@@ -137,6 +106,8 @@ export class AiderRunner extends AgentRunner {
137
106
  console.error("Failed to parse Aider configuration:", err);
138
107
  }
139
108
  }
109
+ // Record the model so fallback cost estimates use the right rate.
110
+ this.model = aiderConfig.model;
140
111
 
141
112
  // Setup process environment with injected keys
142
113
  const customEnv = {
@@ -282,6 +253,15 @@ export class AiderRunner extends AgentRunner {
282
253
  }
283
254
  this.ptyProcess = null;
284
255
  }
256
+ // For containerised Aider, ensure the container is torn down (the PTY hangup
257
+ // alone may not trigger the run's --rm cleanup).
258
+ if (this.type === "aider-docker") {
259
+ try {
260
+ await execFilePromise("docker", ["stop", `neural-loom-session-${this.id}`]);
261
+ } catch {
262
+ // Container already gone or docker unavailable.
263
+ }
264
+ }
285
265
  this.appendLog("\r\n\x1b[1;31m[System] Aider session terminated.\x1b[0m\r\n");
286
266
  }
287
267
 
@@ -304,5 +284,9 @@ export class AiderRunner extends AgentRunner {
304
284
  }
305
285
  }
306
286
  }
287
+
288
+ override getPid(): number | undefined {
289
+ return this.ptyProcess?.pid;
290
+ }
307
291
  }
308
292
 
@@ -1,33 +1,8 @@
1
1
  import fs from "fs";
2
- import path from "path";
3
- import os from "os";
4
2
  import { execSync } from "child_process";
5
- import { getConfigDir } from "../config";
6
3
  import type { IPty } from "node-pty";
7
4
  import { AgentRunner } from "./AgentRunner";
8
- import { SessionPersister } from "./SessionPersister";
9
-
10
- function isWindows(): boolean {
11
- return typeof process !== "undefined" && process.platform === "win32";
12
- }
13
-
14
- function findGitBash(): string | null {
15
- if (!isWindows()) {
16
- return "bash";
17
- }
18
- const pf = process.env.PROGRAMFILES || "C:\\Program Files";
19
- const pf86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
20
- const local = process.env.LOCALAPPDATA || "";
21
-
22
- const candidates: string[] = [
23
- process.env.WINGMAN_BASH_PATH,
24
- path.join(pf, "Git", "bin", "bash.exe"),
25
- path.join(pf86, "Git", "bin", "bash.exe"),
26
- local ? path.join(local, "Programs", "Git", "bin", "bash.exe") : "",
27
- ].filter((p): p is string => typeof p === "string" && p !== "");
28
-
29
- return candidates.find(p => fs.existsSync(p)) || null;
30
- }
5
+ import { isWindows, findGitBash, loadNodePty, loadClaudeExtraFlags, windowsPathOverride } from "./runtime";
31
6
 
32
7
  export class ClaudeRunner extends AgentRunner {
33
8
  id: string;
@@ -47,16 +22,6 @@ export class ClaudeRunner extends AgentRunner {
47
22
  this.createdAt = new Date().toISOString();
48
23
  }
49
24
 
50
- private appendLog(line: string) {
51
- this.logs.push(line);
52
- if (this.logs.length > 5000) {
53
- this.logs.shift();
54
- }
55
- this.parseTokenUsage(line);
56
- this.emitOutput(line);
57
- SessionPersister.saveSession(this);
58
- }
59
-
60
25
  async start(resume?: boolean): Promise<void> {
61
26
  if (this.status === 'running') return;
62
27
 
@@ -71,10 +36,7 @@ export class ClaudeRunner extends AgentRunner {
71
36
  const shellArgs = useCmd ? ["/c"] : ["-c"];
72
37
 
73
38
  try {
74
- // Dynamic import to prevent bundler SSR/Turbopack errors on server start
75
- if (!this.ptyModule) {
76
- this.ptyModule = eval("require")("node-pty");
77
- }
39
+ this.ptyModule = loadNodePty();
78
40
 
79
41
  this.status = 'running';
80
42
  this.appendLog("\x1b[1;35m==================================================\x1b[0m\r\n");
@@ -85,23 +47,7 @@ export class ClaudeRunner extends AgentRunner {
85
47
  ? this.workspaceRoot
86
48
  : process.env.ORIGINAL_CWD || process.cwd();
87
49
 
88
- // Load Claude configuration to check for extraFlags (like --resume)
89
- let extraFlags = "";
90
- if (resume) {
91
- extraFlags = "--resume";
92
- } else {
93
- const claudeConfigPath = path.join(getConfigDir(), "claude.json");
94
- if (fs.existsSync(claudeConfigPath)) {
95
- try {
96
- const config = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
97
- if (config.extraFlags) {
98
- extraFlags = config.extraFlags;
99
- }
100
- } catch (err) {
101
- console.error("Failed to parse Claude configuration:", err);
102
- }
103
- }
104
- }
50
+ const extraFlags = loadClaudeExtraFlags(resume);
105
51
 
106
52
  // Setup process environment with injected keys
107
53
  const customEnv = {
@@ -111,11 +57,7 @@ export class ClaudeRunner extends AgentRunner {
111
57
  } as NodeJS.ProcessEnv;
112
58
 
113
59
  if (isWindows()) {
114
- const vsNode = "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Microsoft\\VisualStudio\\NodeJs";
115
- const localNpm = path.join(os.homedir(), "AppData", "Local", "npm");
116
- const existingPath = process.env.Path || process.env.PATH || "";
117
- const winPath = [vsNode, localNpm, existingPath].filter(Boolean).join(";");
118
-
60
+ const winPath = windowsPathOverride();
119
61
  for (const key of Object.keys(customEnv)) {
120
62
  if (key.toLowerCase() === "path") {
121
63
  delete customEnv[key];
@@ -198,5 +140,9 @@ export class ClaudeRunner extends AgentRunner {
198
140
  }
199
141
  }
200
142
  }
143
+
144
+ override getPid(): number | undefined {
145
+ return this.ptyProcess?.pid;
146
+ }
201
147
  }
202
148
 
@@ -1,31 +1,12 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { getConfigDir } from "../config";
3
+ import { promisify } from "util";
4
+ import { execFile } from "child_process";
4
5
  import type { IPty } from "node-pty";
5
6
  import { AgentRunner } from "./AgentRunner";
6
- import { SessionPersister } from "./SessionPersister";
7
+ import { findGitBash, loadNodePty, loadClaudeExtraFlags } from "./runtime";
7
8
 
8
- function isWindows(): boolean {
9
- return typeof process !== "undefined" && process.platform === "win32";
10
- }
11
-
12
- function findGitBash(): string | null {
13
- if (!isWindows()) {
14
- return "bash";
15
- }
16
- const pf = process.env.PROGRAMFILES || "C:\\Program Files";
17
- const pf86 = process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
18
- const local = process.env.LOCALAPPDATA || "";
19
-
20
- const candidates: string[] = [
21
- process.env.WINGMAN_BASH_PATH,
22
- path.join(pf, "Git", "bin", "bash.exe"),
23
- path.join(pf86, "Git", "bin", "bash.exe"),
24
- local ? path.join(local, "Programs", "Git", "bin", "bash.exe") : "",
25
- ].filter((p): p is string => typeof p === "string" && p !== "");
26
-
27
- return candidates.find(p => fs.existsSync(p)) || null;
28
- }
9
+ const execFilePromise = promisify(execFile);
29
10
 
30
11
  export class DockerRunner extends AgentRunner {
31
12
  id: string;
@@ -45,16 +26,6 @@ export class DockerRunner extends AgentRunner {
45
26
  this.createdAt = new Date().toISOString();
46
27
  }
47
28
 
48
- private appendLog(line: string) {
49
- this.logs.push(line);
50
- if (this.logs.length > 5000) {
51
- this.logs.shift();
52
- }
53
- this.parseTokenUsage(line);
54
- this.emitOutput(line);
55
- SessionPersister.saveSession(this);
56
- }
57
-
58
29
  async start(resume?: boolean): Promise<void> {
59
30
  if (this.status === 'running') return;
60
31
 
@@ -72,11 +43,9 @@ export class DockerRunner extends AgentRunner {
72
43
  }
73
44
 
74
45
  try {
75
- if (!this.ptyModule) {
76
- this.ptyModule = eval("require")("node-pty");
77
- }
46
+ this.ptyModule = loadNodePty();
78
47
 
79
- const targetCwd = (this.workspaceRoot && fs.existsSync(this.workspaceRoot))
48
+ const targetCwd = (this.workspaceRoot && fs.existsSync(this.workspaceRoot))
80
49
  ? this.workspaceRoot
81
50
  : process.env.ORIGINAL_CWD || process.cwd();
82
51
 
@@ -121,23 +90,7 @@ export class DockerRunner extends AgentRunner {
121
90
  }
122
91
  const mountsStr = mounts.join(" ");
123
92
 
124
- // Load Claude configuration to check for extraFlags (like --resume)
125
- let extraFlags = "";
126
- if (resume) {
127
- extraFlags = "--resume";
128
- } else {
129
- const claudeConfigPath = path.join(getConfigDir(), "claude.json");
130
- if (fs.existsSync(claudeConfigPath)) {
131
- try {
132
- const config = JSON.parse(fs.readFileSync(claudeConfigPath, "utf8"));
133
- if (config.extraFlags) {
134
- extraFlags = config.extraFlags;
135
- }
136
- } catch (err) {
137
- console.error("Failed to parse Claude configuration:", err);
138
- }
139
- }
140
- }
93
+ const extraFlags = loadClaudeExtraFlags(resume);
141
94
 
142
95
  const dockerCmd = `docker run -it --name neural-loom-session-${this.id} --rm ${mountsStr} -w /workspace node:20-alpine npx -y @anthropic-ai/claude-code ${extraFlags}`.trim();
143
96
 
@@ -180,6 +133,13 @@ export class DockerRunner extends AgentRunner {
180
133
  }
181
134
  this.ptyProcess = null;
182
135
  }
136
+ // Killing the PTY doesn't guarantee the `docker run` child receives a clean
137
+ // hangup, so explicitly stop the container (this also triggers its --rm cleanup).
138
+ try {
139
+ await execFilePromise("docker", ["stop", `neural-loom-session-${this.id}`]);
140
+ } catch {
141
+ // Container already gone or docker unavailable — nothing to clean up.
142
+ }
183
143
  this.appendLog("\r\n\x1b[1;31m[System] Docker container stopped and unmounted.\x1b[0m\r\n");
184
144
  }
185
145
 
@@ -202,5 +162,9 @@ export class DockerRunner extends AgentRunner {
202
162
  }
203
163
  }
204
164
  }
165
+
166
+ override getPid(): number | undefined {
167
+ return this.ptyProcess?.pid;
168
+ }
205
169
  }
206
170
 
@@ -28,7 +28,9 @@ export class MockRunner extends AgentRunner {
28
28
  this.createdAt = new Date().toISOString();
29
29
  }
30
30
 
31
- private appendLog(line: string) {
31
+ // Mock output is line-oriented, so it appends its own CRLF and overrides the
32
+ // base PTY-stream appendLog.
33
+ protected override appendLog(line: string) {
32
34
  this.logs.push(line);
33
35
  this.parseTokenUsage(line);
34
36
  this.emitOutput(line + "\r\n");