diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c32051..92819e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -168,8 +168,98 @@ importers: specifier: ^3.0.0 version: 3.2.4(@types/node@22.19.17)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0) + packages/codeflow-canvas: + dependencies: + '@abhinav2203/codeflow-core': + specifier: ^1.1.6 + version: 1.1.6 + '@monaco-editor/react': + specifier: ^4.0.0 + version: 4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@xyflow/react': + specifier: ^12.0.0 + version: 12.10.2(@types/react@18.3.30)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + dotenv: + specifier: ^16.0.0 + version: 16.6.1 + monaco-editor: + specifier: ^0.52.0 + version: 0.52.2 + react: + specifier: ^18.0.0 + version: 18.3.1 + react-dom: + specifier: ^18.0.0 + version: 18.3.1(react@18.3.1) + react-rnd: + specifier: ^10.5.3 + version: 10.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zustand: + specifier: ^5.0.0 + version: 5.0.12(@types/react@18.3.30)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + '@types/react': + specifier: ^18.0.0 + version: 18.3.30 + next: + specifier: ^16.0.0 + version: 16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@22.19.17)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0) + packages/codeflow-core: {} + packages/codeflow-dtwin: + dependencies: + '@abhinav2203/codeflow-core': + specifier: ^1.1.6 + version: 1.1.6 + '@abhinav2203/codeflow-execution': + specifier: ^0.1.0 + version: 0.1.0 + '@abhinav2203/codeflow-store': + specifier: ^1.0.14 + version: 1.0.14(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) + dotenv: + specifier: ^16.0.0 + version: 16.6.1 + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@22.19.17)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0) + + packages/codeflow-evolution: + dependencies: + '@abhinav2203/codeflow-core': + specifier: ^1.1.6 + version: 1.1.6 + dotenv: + specifier: ^16.0.0 + version: 16.6.1 + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.0.0 + version: 3.2.4(@types/node@22.19.17)(jsdom@28.1.0)(lightningcss@1.32.0)(tsx@4.21.0) + packages/codeflow-execution: dependencies: '@abhinav2203/codeflow-core': @@ -290,6 +380,14 @@ packages: '@abhinav2203/codeflow-core@1.1.5': resolution: {integrity: sha512-+vM2Vv9jQk2uippgwI1JLFVHlS5L7HOlkzK9PiTX6lP6XCQewuyZXM8T+Tz0lA9j0f0q6sgL6kRMIrXU+TxWww==} + '@abhinav2203/codeflow-core@1.1.6': + resolution: {integrity: sha512-K58dcjWIH+fwHQJFnb0frIALZ7TqA+pQwn3rjsPEglN9EvYWjdnUTiReFIJK220R8xOzgnWkTW6snA+7IDX8Dg==} + + '@abhinav2203/codeflow-execution@0.1.0': + resolution: {integrity: sha512-t38VtaMUa/G254JGDWuw4wIQLuYxQL9EmxJmuBa2j5L5bYbDO1tQCC3dHSMYXJdcZ+1LmHTD/GbhMYImJ6D1Kg==} + engines: {node: '>=20.0.0'} + hasBin: true + '@abhinav2203/codeflow-store@1.0.13': resolution: {integrity: sha512-o/S1ej8UG4vPpkBmCXs2XQAOSjYjAt5o82x6MG2jBVfar8Iy+6hHQRE9/cASm71vl+nqcCOXSKxpEeWo2f8Bfw==} hasBin: true @@ -1658,6 +1756,9 @@ packages: '@types/offscreencanvas@2019.7.3': resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1668,6 +1769,9 @@ packages: peerDependencies: '@types/react': '*' + '@types/react@18.3.30': + resolution: {integrity: sha512-3ek6mwJL5/VBewBcY4S66cqlCtK3qi4WIq37Z0m/NHw1hjhI7274Mx1qz/+ggSzyBCOEf7eHjBN6INjPAWYfYw==} + '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} @@ -2451,6 +2555,10 @@ packages: dompurify@3.2.7: resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + draco3d@1.5.7: resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} @@ -3421,6 +3529,9 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + monaco-editor@0.55.1: resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} @@ -3810,6 +3921,11 @@ packages: react: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + react-dom@19.2.5: resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: @@ -3845,6 +3961,10 @@ packages: react-dom: optional: true + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + react@19.2.5: resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} @@ -3927,6 +4047,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -4830,6 +4953,30 @@ snapshots: - '@types/emscripten' - tree-sitter + '@abhinav2203/codeflow-core@1.1.6': + dependencies: + tree-sitter-c: 0.24.1 + tree-sitter-cpp: 0.23.4 + tree-sitter-go: 0.25.0 + tree-sitter-javascript: 0.25.0 + tree-sitter-python: 0.25.0 + tree-sitter-rust: 0.24.0 + tree-sitter-typescript: 0.23.2 + ts-morph: 27.0.2 + web-tree-sitter: 0.25.10 + zod: 4.3.6 + transitivePeerDependencies: + - '@types/emscripten' + - tree-sitter + + '@abhinav2203/codeflow-execution@0.1.0': + dependencies: + '@abhinav2203/codeflow-core': 1.1.6 + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/emscripten' + - tree-sitter + '@abhinav2203/codeflow-store@1.0.13(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))': dependencies: '@abhinav2203/codeflow-core': 1.1.5 @@ -5481,6 +5628,13 @@ snapshots: dependencies: state-local: 1.0.7 + '@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.52.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@monaco-editor/loader': 1.7.0 @@ -5886,6 +6040,8 @@ snapshots: '@types/offscreencanvas@2019.7.3': {} + '@types/prop-types@15.7.15': {} + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -5894,6 +6050,11 @@ snapshots: dependencies: '@types/react': 19.2.14 + '@types/react@18.3.30': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -6197,6 +6358,17 @@ snapshots: - bare-buffer - react-native-b4a + '@xyflow/react@12.10.2(@types/react@18.3.30)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@xyflow/system': 0.0.76 + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.7(@types/react@18.3.30)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + '@xyflow/react@12.10.2(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@xyflow/system': 0.0.76 @@ -6739,6 +6911,8 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + dotenv@16.6.1: {} + draco3d@1.5.7: {} dunder-proto@1.0.1: @@ -7871,6 +8045,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 + monaco-editor@0.52.2: {} + monaco-editor@0.55.1: dependencies: dompurify: 3.2.7 @@ -7894,6 +8070,31 @@ snapshots: negotiator@1.0.0: {} + next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 16.2.4 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.23 + caniuse-lite: 1.0.30001791 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 16.2.4 + '@next/swc-darwin-x64': 16.2.4 + '@next/swc-linux-arm64-gnu': 16.2.4 + '@next/swc-linux-arm64-musl': 16.2.4 + '@next/swc-linux-x64-gnu': 16.2.4 + '@next/swc-linux-x64-musl': 16.2.4 + '@next/swc-win32-arm64-msvc': 16.2.4 + '@next/swc-win32-x64-msvc': 16.2.4 + '@playwright/test': 1.59.1 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + next@16.2.4(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 16.2.4 @@ -8267,16 +8468,34 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + re-resizable@6.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + re-resizable@6.11.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + react-dom@19.2.5(react@19.2.5): dependencies: react: 19.2.5 scheduler: 0.27.0 + react-draggable@4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-draggable@4.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: clsx: 2.1.1 @@ -8290,6 +8509,14 @@ snapshots: react-is@18.3.1: {} + react-rnd@10.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + re-resizable: 6.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-draggable: 4.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tslib: 2.6.2 + react-rnd@10.5.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: re-resizable: 6.11.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -8304,6 +8531,10 @@ snapshots: optionalDependencies: react-dom: 19.2.5(react@19.2.5) + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + react@19.2.5: {} readable-stream@3.6.2: @@ -8449,6 +8680,10 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + scheduler@0.27.0: {} semver@6.3.1: {} @@ -8712,6 +8947,13 @@ snapshots: dependencies: js-tokens: 9.0.1 + styled-jsx@5.1.6(@babel/core@7.29.0)(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + optionalDependencies: + '@babel/core': 7.29.0 + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.5): dependencies: client-only: 0.0.1 @@ -9061,6 +9303,10 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + use-sync-external-store@1.6.0(react@19.2.5): dependencies: react: 19.2.5 @@ -9356,6 +9602,13 @@ snapshots: zod@4.3.6: {} + zustand@4.5.7(@types/react@18.3.30)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.30 + react: 18.3.1 + zustand@4.5.7(@types/react@19.2.14)(react@19.2.5): dependencies: use-sync-external-store: 1.6.0(react@19.2.5) @@ -9363,6 +9616,12 @@ snapshots: '@types/react': 19.2.14 react: 19.2.5 + zustand@5.0.12(@types/react@18.3.30)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)): + optionalDependencies: + '@types/react': 18.3.30 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + zustand@5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)): optionalDependencies: '@types/react': 19.2.14 diff --git a/src/lib/server/terminal-sessions.ts b/src/lib/server/terminal-sessions.ts index bbbdc68..330e80f 100644 --- a/src/lib/server/terminal-sessions.ts +++ b/src/lib/server/terminal-sessions.ts @@ -31,9 +31,40 @@ const DEFAULT_WORKSPACE_ROOT = process.env.CODEFLOW_REPO_ROOT ?? /* turbopackIgn const OUTPUT_CAP_BYTES = 128 * 1024; const OUTPUT_TRUNCATION_NOTICE = "[CodeFlow] Older terminal output truncated.\n"; +const IDLE_TIMEOUT_MS = 10 * 60 * 1000; +const EXPIRED_OUTPUT_GRACE_MS = 60 * 1000; +const MAX_SESSIONS = 50; + const sessions = new Map(); let sessionCounter = 0; +const isIdleExpired = (session: InternalTerminalSession, now: number): boolean => { + const lastActivity = Date.parse(session.lastActivityAt); + if (Number.isNaN(lastActivity)) { + return true; + } + + if (session.status === "running") { + return now - lastActivity > IDLE_TIMEOUT_MS; + } + + return now - lastActivity > EXPIRED_OUTPUT_GRACE_MS; +}; + +const purgeIdleSessions = (now: number = Date.now()): void => { + for (const [id, session] of sessions) { + if (!isIdleExpired(session, now)) { + continue; + } + + if (session.status === "running") { + session.child.kill("SIGTERM"); + } + + sessions.delete(id); + } +}; + const stripTruncationNotice = (value: string): string => value.startsWith(OUTPUT_TRUNCATION_NOTICE) ? value.slice(OUTPUT_TRUNCATION_NOTICE.length) : value; @@ -122,12 +153,15 @@ const recordInput = (session: InternalTerminalSession, input: string) => { ); }; -export const listTerminalSessions = (): TerminalSessionSummary[] => - [...sessions.values()] +export const listTerminalSessions = (): TerminalSessionSummary[] => { + purgeIdleSessions(); + return [...sessions.values()] .map(toSummary) .sort((left, right) => right.startedAt.localeCompare(left.startedAt)); +}; export const getTerminalSession = (id: string): TerminalSessionSnapshot | null => { + purgeIdleSessions(); const session = sessions.get(id); return session ? toSnapshot(session) : null; }; @@ -136,6 +170,13 @@ export const createTerminalSession = async (options?: { cwd?: string; title?: string; }): Promise => { + purgeIdleSessions(); + if (sessions.size >= MAX_SESSIONS) { + throw new Error( + `Terminal session limit reached (${MAX_SESSIONS}). Close an existing session before opening a new one.` + ); + } + const cwd = await resolveInitialCwd(options?.cwd); const shell = getShellPath(); const child = spawn(shell, [], {