neural-loom 0.2.70 → 0.3.1
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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/cache/.previewinfo +1 -1
- package/.next/cache/.rscinfo +1 -1
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/diagnostics/route-bundle-stats.json +11 -11
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +20 -20
- package/.next/required-server-files.json +20 -20
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/configs/route.js.nft.json +1 -1
- package/.next/server/app/api/context/aider/route.js.nft.json +1 -1
- package/.next/server/app/api/context/claude/route.js.nft.json +1 -1
- package/.next/server/app/api/context/route.js.nft.json +1 -1
- package/.next/server/app/api/context/ssh/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/recents/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/inject/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/input/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/launch/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/scope/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/start/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/stats/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/stop/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/upload-image/route.js.nft.json +1 -1
- package/.next/server/app/api/sql/connections/route.js.nft.json +1 -1
- package/.next/server/app/api/sql/mcp/route.js +1 -1
- package/.next/server/app/api/sql/mcp/route.js.nft.json +1 -1
- package/.next/server/app/api/sql/query/route.js +1 -1
- package/.next/server/app/api/sql/query/route.js.nft.json +1 -1
- package/.next/server/app/api/sql/schema/route.js.nft.json +1 -1
- package/.next/server/app/api/telemetry/route.js +1 -1
- package/.next/server/app/api/telemetry/route.js.nft.json +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/page/react-loadable-manifest.json +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0.hmw1f._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0.hmw1f._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__00-sqou._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__00-sqou._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__00l5btc._.js +4 -4
- package/.next/server/chunks/[root-of-the-server]__00l5btc._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__06men8g._.js +4 -4
- package/.next/server/chunks/[root-of-the-server]__06men8g._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__097vtn1._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__097vtn1._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js.map +1 -1
- package/.next/server/chunks/{[root-of-the-server]__0.ptwor._.js → [root-of-the-server]__0ajln56._.js} +7 -7
- package/.next/server/chunks/[root-of-the-server]__0ajln56._.js.map +1 -0
- package/.next/server/chunks/{[root-of-the-server]__0co.g5k._.js → [root-of-the-server]__0auh-2z._.js} +5 -5
- package/.next/server/chunks/[root-of-the-server]__0auh-2z._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__0e03ite._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0e03ite._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0h.~hv6._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0h.~hv6._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0i83~95._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js +19 -19
- package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0s1axek._.js +5 -5
- package/.next/server/chunks/[root-of-the-server]__0s1axek._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0t713sv._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0t713sv._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js +5 -5
- package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0viil6b._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0viil6b._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0ykd4~u._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0ykd4~u._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__11h_nxt._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__11h_nxt._.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_016qk~0.js +2 -2
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_016qk~0.js.map +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js +1 -1
- package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js.map +1 -1
- package/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js +11 -0
- package/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js.map +1 -0
- package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js +3 -0
- package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js.map +1 -0
- package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js.map +1 -0
- package/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js +3 -0
- package/.next/server/middleware/middleware-manifest.json +32 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/middleware-manifest.json +30 -2
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/3wzjx3BGzCrq3vZdFE3N-/_clientMiddlewareManifest.js +6 -0
- package/.next/static/chunks/{0r3x2gcn0sde..js → 0jboepcsqzdio.js} +4 -4
- package/.next/static/chunks/{0fxouf2n_5adf.js → 125kj92.ndij4.js} +2 -2
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/README.md +18 -0
- package/bin/cli.js +12 -2
- package/package.json +9 -6
- package/src/app/api/files/route.ts +30 -0
- package/src/app/api/git/route.ts +46 -31
- package/src/app/api/sessions/stats/route.ts +40 -22
- package/src/app/api/sql/query/route.ts +2 -15
- package/src/app/api/telemetry/route.ts +2 -1
- package/src/app/components/TerminalConsole.tsx +144 -39
- package/src/lib/agents/AgentRunner.ts +41 -5
- package/src/lib/agents/AiderRunner.ts +20 -36
- package/src/lib/agents/ClaudeRunner.ts +8 -62
- package/src/lib/agents/DockerRunner.ts +18 -54
- package/src/lib/agents/MockRunner.ts +3 -1
- package/src/lib/agents/SSHRunner.ts +8 -54
- package/src/lib/agents/SessionPersister.ts +28 -2
- package/src/lib/agents/ShellRunner.ts +10 -46
- package/src/lib/agents/WebSocketServer.ts +17 -2
- package/src/lib/agents/pricing.ts +45 -0
- package/src/lib/agents/runtime.ts +92 -0
- package/src/lib/gitSafe.ts +15 -0
- package/src/lib/security.ts +76 -0
- package/src/lib/sql/McpServer.ts +2 -11
- package/src/lib/sql/adapters/MssqlAdapter.ts +16 -1
- package/src/lib/sql/adapters/MysqlAdapter.ts +23 -1
- package/src/lib/sql/adapters/PostgresAdapter.ts +22 -1
- package/src/lib/sql/adapters/SqliteAdapter.ts +21 -10
- package/src/lib/sql/sqlGuard.ts +20 -0
- package/src/lib/workspaceParser.ts +23 -6
- package/src/middleware.ts +27 -0
- package/.next/dev/build-manifest.json +0 -50
- package/.next/dev/fallback-build-manifest.json +0 -37
- package/.next/dev/logs/next-development.log +0 -3737
- package/.next/dev/server/chunks/[root-of-the-server]__11uf~fd._.js +0 -2396
- package/.next/dev/server/chunks/[root-of-the-server]__11uf~fd._.js.map +0 -20
- package/.next/dev/server/chunks/ssr/[externals]__0.o~_~l._.js +0 -32
- package/.next/dev/server/chunks/ssr/[externals]__0.o~_~l._.js.map +0 -5
- package/.next/dev/server/chunks/ssr/[externals]_next_dist_shared_lib_no-fallback-error_external_0fgm~bn.js +0 -8
- package/.next/dev/server/chunks/ssr/[externals]_next_dist_shared_lib_no-fallback-error_external_0fgm~bn.js.map +0 -5
- package/.next/dev/server/chunks/ssr/[root-of-the-server]__0je5zm2._.js +0 -285
- package/.next/dev/server/chunks/ssr/[root-of-the-server]__0je5zm2._.js.map +0 -9
- package/.next/dev/server/chunks/ssr/node_modules_01ed9g5._.js +0 -6621
- package/.next/dev/server/chunks/ssr/node_modules_01ed9g5._.js.map +0 -76
- package/.next/dev/server/chunks/ssr/node_modules_129w7gn._.js +0 -4399
- package/.next/dev/server/chunks/ssr/node_modules_129w7gn._.js.map +0 -35
- package/.next/dev/server/chunks/ssr/node_modules_next_12i4inu._.js +0 -55
- package/.next/dev/server/chunks/ssr/node_modules_next_12i4inu._.js.map +0 -7
- package/.next/dev/server/middleware-build-manifest.js +0 -50
- package/.next/dev/server/next-font-manifest.js +0 -1
- package/.next/dev/server/next-font-manifest.json +0 -11
- package/.next/dev/server/pages/_app/build-manifest.json +0 -23
- package/.next/dev/server/pages/_app/client-build-manifest.json +0 -5
- package/.next/dev/server/pages/_app/next-font-manifest.json +0 -6
- package/.next/dev/server/pages/_app/pages-manifest.json +0 -3
- package/.next/dev/server/pages/_app/react-loadable-manifest.json +0 -1
- package/.next/dev/server/pages/_app.js +0 -4
- package/.next/dev/server/pages/_app.js.map +0 -5
- package/.next/dev/server/pages/_document/next-font-manifest.json +0 -6
- package/.next/dev/server/pages/_document/pages-manifest.json +0 -3
- package/.next/dev/server/pages/_document/react-loadable-manifest.json +0 -1
- package/.next/dev/server/pages/_document.js +0 -5
- package/.next/dev/server/pages/_document.js.map +0 -5
- package/.next/dev/server/pages/_error/build-manifest.json +0 -22
- package/.next/dev/server/pages/_error/client-build-manifest.json +0 -5
- package/.next/dev/server/pages/_error/next-font-manifest.json +0 -6
- package/.next/dev/server/pages/_error/pages-manifest.json +0 -3
- package/.next/dev/server/pages/_error/react-loadable-manifest.json +0 -1
- package/.next/dev/server/pages/_error.js +0 -8
- package/.next/dev/server/pages/_error.js.map +0 -5
- package/.next/dev/server/pages-manifest.json +0 -5
- package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js +0 -23
- package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js.map +0 -6
- package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js +0 -23
- package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js.map +0 -6
- package/.next/dev/static/chunks/[root-of-the-server]__01mw43t._.js +0 -476
- package/.next/dev/static/chunks/[root-of-the-server]__01mw43t._.js.map +0 -6
- package/.next/dev/static/chunks/[root-of-the-server]__0c0okpg._.js +0 -476
- package/.next/dev/static/chunks/[root-of-the-server]__0c0okpg._.js.map +0 -6
- package/.next/dev/static/chunks/node_modules_0lx093h._.js +0 -1343
- package/.next/dev/static/chunks/node_modules_0lx093h._.js.map +0 -13
- package/.next/dev/static/chunks/node_modules_next_app_0jt-zj..js +0 -8
- package/.next/dev/static/chunks/node_modules_next_app_0jt-zj..js.map +0 -6
- package/.next/dev/static/chunks/node_modules_next_dist_0rt-2cr._.js +0 -2334
- package/.next/dev/static/chunks/node_modules_next_dist_0rt-2cr._.js.map +0 -26
- package/.next/dev/static/chunks/node_modules_next_dist_0u_w_5s._.js +0 -2165
- package/.next/dev/static/chunks/node_modules_next_dist_0u_w_5s._.js.map +0 -25
- package/.next/dev/static/chunks/node_modules_next_dist_client_0pe1dg-._.js +0 -4091
- package/.next/dev/static/chunks/node_modules_next_dist_client_0pe1dg-._.js.map +0 -49
- package/.next/dev/static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js +0 -6064
- package/.next/dev/static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js.map +0 -17
- package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0ek5eq7._.js +0 -6535
- package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0ek5eq7._.js.map +0 -72
- package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0n1wg2c._.js +0 -6294
- package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0n1wg2c._.js.map +0 -70
- package/.next/dev/static/chunks/node_modules_react-dom_0bruynb._.js +0 -15381
- package/.next/dev/static/chunks/node_modules_react-dom_0bruynb._.js.map +0 -9
- package/.next/dev/static/chunks/pages/_app.js +0 -13
- package/.next/dev/static/chunks/pages/_error.js +0 -12
- package/.next/dev/static/chunks/pages__app_0-bg2z-._.js.map +0 -11
- package/.next/dev/static/chunks/pages__app_07xvfw~._.js +0 -5
- package/.next/dev/static/chunks/pages__error_0.i_zsq._.js.map +0 -11
- package/.next/dev/static/chunks/pages__error_07xvfw~._.js +0 -5
- package/.next/dev/static/chunks/turbopack-pages__app_0-bg2z-._.js +0 -2236
- package/.next/dev/static/chunks/turbopack-pages__error_0.i_zsq._.js +0 -2236
- package/.next/server/chunks/[externals]__048y_op._.js +0 -3
- package/.next/server/chunks/[externals]__048y_op._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0.ptwor._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__0co.g5k._.js.map +0 -1
- package/.next/static/llmJsGQafolZV5_sqEw4l/_clientMiddlewareManifest.js +0 -1
- /package/.next/static/{llmJsGQafolZV5_sqEw4l → 3wzjx3BGzCrq3vZdFE3N-}/_buildManifest.js +0 -0
- /package/.next/static/{llmJsGQafolZV5_sqEw4l → 3wzjx3BGzCrq3vZdFE3N-}/_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,33 @@ interface LogBufferItem {
|
|
|
19
19
|
message?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
interface CopyResult {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
method?: "clipboard-api" | "execCommand";
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function copyTextToClipboard(text: string): Promise<CopyResult> {
|
|
29
|
+
if (typeof window === "undefined") return { ok: false, error: "no window" };
|
|
30
|
+
if (!text) return { ok: false, error: "empty selection" };
|
|
31
|
+
|
|
32
|
+
let apiError = "";
|
|
33
|
+
|
|
34
|
+
// Preferred path: async Clipboard API. Reliable in a secure context, which
|
|
35
|
+
// includes localhost (NeuralLoom's default bind).
|
|
36
|
+
if (window.isSecureContext && navigator.clipboard?.writeText) {
|
|
37
|
+
try {
|
|
38
|
+
await navigator.clipboard.writeText(text);
|
|
39
|
+
return { ok: true, method: "clipboard-api" };
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// Fall through to the execCommand fallback (e.g. not focused / denied).
|
|
42
|
+
apiError = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
43
|
+
}
|
|
44
|
+
} else if (!window.isSecureContext) {
|
|
45
|
+
apiError = "not a secure context (access via http://localhost for the Clipboard API)";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fallback: hidden textarea + execCommand("copy").
|
|
26
49
|
try {
|
|
27
50
|
const textArea = document.createElement("textarea");
|
|
28
51
|
textArea.value = text;
|
|
@@ -33,24 +56,14 @@ function copyTextToClipboard(text: string): boolean {
|
|
|
33
56
|
document.body.appendChild(textArea);
|
|
34
57
|
textArea.focus();
|
|
35
58
|
textArea.select();
|
|
36
|
-
successful = document.execCommand("copy");
|
|
59
|
+
const successful = document.execCommand("copy");
|
|
37
60
|
document.body.removeChild(textArea);
|
|
61
|
+
if (successful) return { ok: true, method: "execCommand" };
|
|
62
|
+
return { ok: false, error: `execCommand returned false${apiError ? ` (clipboard API: ${apiError})` : ""}` };
|
|
38
63
|
} catch (err) {
|
|
39
|
-
|
|
40
|
-
|
|
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;
|
|
64
|
+
const ex = err instanceof Error ? err.message : String(err);
|
|
65
|
+
return { ok: false, error: `${ex}${apiError ? ` | clipboard API: ${apiError}` : ""}` };
|
|
51
66
|
}
|
|
52
|
-
|
|
53
|
-
return false;
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
export default function TerminalConsole({ sessionId, height = "400px", wsPort = 9601 }: TerminalConsoleProps) {
|
|
@@ -71,6 +84,44 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
71
84
|
const [caseSensitive, setCaseSensitive] = useState(false);
|
|
72
85
|
const searchAddonRef = useRef<SearchAddon | null>(null);
|
|
73
86
|
|
|
87
|
+
// Copy feedback ("Copied!" indicator) and last error (surfaced for diagnosis)
|
|
88
|
+
const [copied, setCopied] = useState(false);
|
|
89
|
+
const [copyError, setCopyError] = useState<string | null>(null);
|
|
90
|
+
const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
91
|
+
|
|
92
|
+
// Copy the given text (or the current terminal selection) to the clipboard.
|
|
93
|
+
// Stable identity so the terminal effect can use it without re-initializing.
|
|
94
|
+
// `explicit` = triggered by a button/shortcut (surface "nothing selected").
|
|
95
|
+
const copySelection = useCallback((text?: string, explicit = false): void => {
|
|
96
|
+
const term = xtermRef.current;
|
|
97
|
+
const selection = (text ?? term?.getSelection() ?? "").trim();
|
|
98
|
+
if (!selection) {
|
|
99
|
+
if (explicit) {
|
|
100
|
+
setCopyError("Nothing selected to copy");
|
|
101
|
+
setTimeout(() => setCopyError(null), 2500);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
copyTextToClipboard(selection).then((result) => {
|
|
106
|
+
if (result.ok) {
|
|
107
|
+
setCopyError(null);
|
|
108
|
+
setCopied(true);
|
|
109
|
+
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
|
|
110
|
+
copiedTimerRef.current = setTimeout(() => setCopied(false), 1200);
|
|
111
|
+
} else {
|
|
112
|
+
console.error("[TerminalConsole] copy failed:", result.error);
|
|
113
|
+
setCopyError(result.error || "copy failed");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
// Clear the copy-feedback timer on unmount
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
return () => {
|
|
121
|
+
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
|
|
122
|
+
};
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
74
125
|
// Focus search input when opened
|
|
75
126
|
useEffect(() => {
|
|
76
127
|
if (searchOpen && searchInputRef.current) {
|
|
@@ -189,27 +240,18 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
189
240
|
term.focus();
|
|
190
241
|
|
|
191
242
|
const container = terminalRef.current;
|
|
243
|
+
// Keep right-click from clearing an active selection so the context-menu
|
|
244
|
+
// copy can read it.
|
|
192
245
|
const handleMouseDown = (e: MouseEvent) => {
|
|
193
|
-
|
|
194
|
-
if (e.button === 2 && term.hasSelection()) {
|
|
195
|
-
console.log("[TerminalConsole] mousedown right-click, stopping propagation");
|
|
196
|
-
e.stopPropagation();
|
|
197
|
-
}
|
|
246
|
+
if (e.button === 2 && term.hasSelection()) e.stopPropagation();
|
|
198
247
|
};
|
|
199
248
|
const handleMouseUp = (e: MouseEvent) => {
|
|
200
|
-
|
|
201
|
-
if (e.button === 2 && term.hasSelection()) {
|
|
202
|
-
console.log("[TerminalConsole] mouseup right-click, stopping propagation");
|
|
203
|
-
e.stopPropagation();
|
|
204
|
-
}
|
|
249
|
+
if (e.button === 2 && term.hasSelection()) e.stopPropagation();
|
|
205
250
|
};
|
|
206
251
|
const handleContextMenu = (e: MouseEvent) => {
|
|
207
|
-
console.log("[TerminalConsole] contextmenu - hasSelection:", term.hasSelection(), "selection:", term.getSelection());
|
|
208
252
|
if (term.hasSelection()) {
|
|
209
253
|
e.preventDefault();
|
|
210
|
-
|
|
211
|
-
const success = copyTextToClipboard(selection);
|
|
212
|
-
console.log("[TerminalConsole] contextmenu copy success:", success);
|
|
254
|
+
copySelection(term.getSelection());
|
|
213
255
|
term.clearSelection();
|
|
214
256
|
}
|
|
215
257
|
};
|
|
@@ -219,6 +261,17 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
219
261
|
container.addEventListener("contextmenu", handleContextMenu);
|
|
220
262
|
}
|
|
221
263
|
|
|
264
|
+
// Copy-on-select: capture the selection the moment it is made. Claude Code's
|
|
265
|
+
// TUI redraws constantly, which clears the xterm selection — so we copy
|
|
266
|
+
// immediately (debounced) rather than waiting for a later Ctrl+C/right-click.
|
|
267
|
+
let selTimer: ReturnType<typeof setTimeout> | null = null;
|
|
268
|
+
const selDisposable = term.onSelectionChange(() => {
|
|
269
|
+
const sel = term.getSelection();
|
|
270
|
+
if (!sel || !sel.trim()) return;
|
|
271
|
+
if (selTimer) clearTimeout(selTimer);
|
|
272
|
+
selTimer = setTimeout(() => copySelection(sel), 150);
|
|
273
|
+
});
|
|
274
|
+
|
|
222
275
|
// Attach search key handler and clipboard copy handler
|
|
223
276
|
term.attachCustomKeyEventHandler((e) => {
|
|
224
277
|
// Search: Ctrl+F
|
|
@@ -227,11 +280,20 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
227
280
|
setSearchOpen(true);
|
|
228
281
|
return false;
|
|
229
282
|
}
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
if ((e.ctrlKey || e.metaKey) && e.key === "c" && e.type === "keydown") {
|
|
283
|
+
|
|
284
|
+
// Explicit copy: Ctrl+Shift+C / Cmd+Shift+C (always, when a selection exists)
|
|
285
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === "c" || e.key === "C") && e.type === "keydown") {
|
|
233
286
|
if (term.hasSelection()) {
|
|
234
|
-
|
|
287
|
+
copySelection(term.getSelection());
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Copy: Ctrl+C / Cmd+C only when a selection is active (otherwise let it
|
|
293
|
+
// pass through to the PTY as an interrupt).
|
|
294
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === "c" && e.type === "keydown") {
|
|
295
|
+
if (term.hasSelection()) {
|
|
296
|
+
copySelection(term.getSelection());
|
|
235
297
|
return false; // Prevent sending interrupt to PTY
|
|
236
298
|
}
|
|
237
299
|
}
|
|
@@ -414,6 +476,8 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
414
476
|
return () => {
|
|
415
477
|
isDisposed = true;
|
|
416
478
|
clearTimeout(fitTimeout);
|
|
479
|
+
if (selTimer) clearTimeout(selTimer);
|
|
480
|
+
selDisposable.dispose();
|
|
417
481
|
resizeObserver.disconnect();
|
|
418
482
|
ws.close();
|
|
419
483
|
if (container) {
|
|
@@ -428,7 +492,7 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
428
492
|
console.warn("Error disposing terminal:", e);
|
|
429
493
|
}
|
|
430
494
|
};
|
|
431
|
-
}, [sessionId, wsPort]);
|
|
495
|
+
}, [sessionId, wsPort, copySelection]);
|
|
432
496
|
const handlePaste = async (e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
433
497
|
const items = e.clipboardData?.items;
|
|
434
498
|
if (!items) return;
|
|
@@ -530,6 +594,47 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
530
594
|
)}
|
|
531
595
|
</div>
|
|
532
596
|
<div style={{ display: "flex", gap: "0.35rem", alignItems: "center" }}>
|
|
597
|
+
{copied && (
|
|
598
|
+
<span style={{
|
|
599
|
+
color: "#50fa7b",
|
|
600
|
+
fontSize: "0.65rem",
|
|
601
|
+
fontWeight: 600,
|
|
602
|
+
}}>
|
|
603
|
+
✓ Copied
|
|
604
|
+
</span>
|
|
605
|
+
)}
|
|
606
|
+
{copyError && (
|
|
607
|
+
<span
|
|
608
|
+
title={copyError}
|
|
609
|
+
style={{
|
|
610
|
+
color: "#ff6b6b",
|
|
611
|
+
fontSize: "0.65rem",
|
|
612
|
+
fontWeight: 600,
|
|
613
|
+
maxWidth: "260px",
|
|
614
|
+
overflow: "hidden",
|
|
615
|
+
textOverflow: "ellipsis",
|
|
616
|
+
whiteSpace: "nowrap",
|
|
617
|
+
}}
|
|
618
|
+
>
|
|
619
|
+
⚠ Copy failed: {copyError}
|
|
620
|
+
</span>
|
|
621
|
+
)}
|
|
622
|
+
<button
|
|
623
|
+
onClick={() => copySelection(undefined, true)}
|
|
624
|
+
style={{
|
|
625
|
+
padding: "2px 6px",
|
|
626
|
+
borderRadius: "3px",
|
|
627
|
+
border: "1px solid var(--border)",
|
|
628
|
+
backgroundColor: "transparent",
|
|
629
|
+
color: "var(--muted)",
|
|
630
|
+
fontSize: "0.65rem",
|
|
631
|
+
fontWeight: 600,
|
|
632
|
+
cursor: "pointer",
|
|
633
|
+
}}
|
|
634
|
+
title="Copy selected terminal text (also auto-copies on selection)"
|
|
635
|
+
>
|
|
636
|
+
📋 Copy
|
|
637
|
+
</button>
|
|
533
638
|
<button
|
|
534
639
|
onClick={togglePause}
|
|
535
640
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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 {
|
|
8
|
+
import { isWindows, findGitBash, loadNodePty } from "./runtime";
|
|
8
9
|
|
|
9
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
7
|
+
import { findGitBash, loadNodePty, loadClaudeExtraFlags } from "./runtime";
|
|
7
8
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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");
|