Fix concurrency limits and remote branch fetching issues (#788)

* Changes from fix/bug-fixes

* feat: Refactor worktree iteration and improve error logging across services

* feat: Extract URL/port patterns to module level and fix abort condition

* fix: Improve IPv6 loopback handling, select component layout, and terminal UI

* feat: Add thinking level defaults and adjust list row padding

* Update apps/ui/src/store/app-store.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* feat: Add worktree-aware terminal creation and split options, fix npm security issues from audit

* feat: Add tracked remote detection to pull dialog flow

* feat: Add merge state tracking to git operations

* feat: Improve merge detection and add post-merge action preferences

* Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: Pass merge detection info to stash reapplication and handle merge state consistently

* fix: Call onPulled callback in merge handlers and add validation checks

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
gsxdsm
2026-02-20 13:48:22 -08:00
committed by GitHub
parent 7df2182818
commit 0a5540c9a2
70 changed files with 4525 additions and 857 deletions

View File

@@ -19,6 +19,69 @@ const logger = createLogger('DevServerService');
// Maximum scrollback buffer size (characters) - matches TerminalService pattern
const MAX_SCROLLBACK_SIZE = 50000; // ~50KB per dev server
// URL patterns for detecting full URLs from dev server output.
// Defined once at module level to avoid reallocation on every call to detectUrlFromOutput.
// Ordered from most specific (framework-specific) to least specific.
const URL_PATTERNS: Array<{ pattern: RegExp; description: string }> = [
// Vite / Nuxt / SvelteKit / Astro / Angular CLI format: "Local: http://..."
{
pattern: /(?:Local|Network|External):\s+(https?:\/\/[^\s]+)/i,
description: 'Vite/Nuxt/SvelteKit/Astro/Angular format',
},
// Next.js format: "ready - started server on 0.0.0.0:3000, url: http://localhost:3000"
// Next.js 14+: "▲ Next.js 14.0.0\n- Local: http://localhost:3000"
{
pattern: /(?:ready|started server).*?(?:url:\s*)?(https?:\/\/[^\s,]+)/i,
description: 'Next.js format',
},
// Remix format: "started at http://localhost:3000"
// Django format: "Starting development server at http://127.0.0.1:8000/"
// Rails / Puma: "Listening on http://127.0.0.1:3000"
// Generic: "listening at http://...", "available at http://...", "running at http://..."
{
pattern:
/(?:starting|started|listening|running|available|serving|accessible)\s+(?:at|on)\s+(https?:\/\/[^\s,)]+)/i,
description: 'Generic "starting/started/listening at" format',
},
// PHP built-in server: "Development Server (http://localhost:8000) started"
{
pattern: /(?:server|development server)\s*\(\s*(https?:\/\/[^\s)]+)\s*\)/i,
description: 'PHP server format',
},
// Webpack Dev Server: "Project is running at http://localhost:8080/"
{
pattern: /(?:project|app|application)\s+(?:is\s+)?running\s+(?:at|on)\s+(https?:\/\/[^\s,]+)/i,
description: 'Webpack/generic "running at" format',
},
// Go / Rust / generic: "Serving on http://...", "Server on http://..."
{
pattern: /(?:serving|server)\s+(?:on|at)\s+(https?:\/\/[^\s,]+)/i,
description: 'Generic "serving on" format',
},
// Localhost URL with port (conservative - must be localhost/127.0.0.1/[::]/0.0.0.0)
// This catches anything that looks like a dev server URL
{
pattern: /(https?:\/\/(?:localhost|127\.0\.0\.1|\[::\]|0\.0\.0\.0):\d+\S*)/i,
description: 'Generic localhost URL with port',
},
];
// Port-only patterns for detecting port numbers from dev server output
// when a full URL is not present in the output.
// Defined once at module level to avoid reallocation on every call to detectUrlFromOutput.
const PORT_PATTERNS: Array<{ pattern: RegExp; description: string }> = [
// "listening on port 3000", "server on port 3000", "started on port 3000"
{
pattern: /(?:listening|running|started|serving|available)\s+on\s+port\s+(\d+)/i,
description: '"listening on port" format',
},
// "Port: 3000", "port 3000" (at start of line or after whitespace)
{
pattern: /(?:^|\s)port[:\s]+(\d{4,5})(?:\s|$|[.,;])/im,
description: '"port:" format',
},
];
// Throttle output to prevent overwhelming WebSocket under heavy load
const OUTPUT_THROTTLE_MS = 4; // ~250fps max update rate for responsive feedback
const OUTPUT_BATCH_SIZE = 4096; // Smaller batches for lower latency
@@ -105,9 +168,52 @@ class DevServerService {
}
}
/**
* Strip ANSI escape codes from a string
* Dev server output often contains color codes that can interfere with URL detection
*/
private stripAnsi(str: string): string {
// Matches ANSI escape sequences: CSI sequences, OSC sequences, and simple escapes
// eslint-disable-next-line no-control-regex
return str.replace(/\x1B(?:\[[0-9;]*[a-zA-Z]|\].*?(?:\x07|\x1B\\)|\[[?]?[0-9;]*[hl])/g, '');
}
/**
* Extract port number from a URL string.
* Returns the explicit port if present, or null if no port is specified.
* Default protocol ports (80/443) are intentionally NOT returned to avoid
* overwriting allocated dev server ports with protocol defaults.
*/
private extractPortFromUrl(url: string): number | null {
try {
const parsed = new URL(url);
if (parsed.port) {
return parseInt(parsed.port, 10);
}
return null;
} catch {
return null;
}
}
/**
* Detect actual server URL from output
* Parses stdout/stderr for common URL patterns from dev servers
* Parses stdout/stderr for common URL patterns from dev servers.
*
* Supports detection of URLs from:
* - Vite: "Local: http://localhost:5173/"
* - Next.js: "ready - started server on 0.0.0.0:3000, url: http://localhost:3000"
* - Nuxt: "Local: http://localhost:3000/"
* - Remix: "started at http://localhost:3000"
* - Astro: "Local http://localhost:4321/"
* - SvelteKit: "Local: http://localhost:5173/"
* - CRA/Webpack: "On Your Network: http://192.168.1.1:3000"
* - Angular: "Local: http://localhost:4200/"
* - Express/Fastify/Koa: "Server listening on port 3000"
* - Django: "Starting development server at http://127.0.0.1:8000/"
* - Rails: "Listening on http://127.0.0.1:3000"
* - PHP: "Development Server (http://localhost:8000) started"
* - Generic: Any localhost URL with a port
*/
private detectUrlFromOutput(server: DevServerInfo, content: string): void {
// Skip if URL already detected
@@ -115,39 +221,95 @@ class DevServerService {
return;
}
// Common URL patterns from various dev servers:
// - Vite: "Local: http://localhost:5173/"
// - Next.js: "ready - started server on 0.0.0.0:3000, url: http://localhost:3000"
// - CRA/Webpack: "On Your Network: http://192.168.1.1:3000"
// - Generic: Any http:// or https:// URL
const urlPatterns = [
/(?:Local|Network):\s+(https?:\/\/[^\s]+)/i, // Vite format
/(?:ready|started server).*?(?:url:\s*)?(https?:\/\/[^\s,]+)/i, // Next.js format
/(https?:\/\/(?:localhost|127\.0\.0\.1|\[::\]):\d+)/i, // Generic localhost URL
/(https?:\/\/[^\s<>"{}|\\^`[\]]+)/i, // Any HTTP(S) URL
];
// Strip ANSI escape codes to prevent color codes from breaking regex matching
const cleanContent = this.stripAnsi(content);
for (const pattern of urlPatterns) {
const match = content.match(pattern);
// Phase 1: Try to detect a full URL from output
// Patterns are defined at module level (URL_PATTERNS) and reused across calls
for (const { pattern, description } of URL_PATTERNS) {
const match = cleanContent.match(pattern);
if (match && match[1]) {
const detectedUrl = match[1].trim();
// Validate it looks like a reasonable URL
let detectedUrl = match[1].trim();
// Remove trailing punctuation that might have been captured
detectedUrl = detectedUrl.replace(/[.,;:!?)\]}>]+$/, '');
if (detectedUrl.startsWith('http://') || detectedUrl.startsWith('https://')) {
// Normalize 0.0.0.0 to localhost for browser accessibility
detectedUrl = detectedUrl.replace(
/\/\/0\.0\.0\.0(:\d+)?/,
(_, port) => `//localhost${port || ''}`
);
// Normalize [::] to localhost for browser accessibility
detectedUrl = detectedUrl.replace(
/\/\/\[::\](:\d+)?/,
(_, port) => `//localhost${port || ''}`
);
// Normalize [::1] (IPv6 loopback) to localhost for browser accessibility
detectedUrl = detectedUrl.replace(
/\/\/\[::1\](:\d+)?/,
(_, port) => `//localhost${port || ''}`
);
server.url = detectedUrl;
server.urlDetected = true;
logger.info(
`Detected actual server URL: ${detectedUrl} (allocated port was ${server.port})`
);
// Update the port to match the detected URL's actual port
const detectedPort = this.extractPortFromUrl(detectedUrl);
if (detectedPort && detectedPort !== server.port) {
logger.info(
`Port mismatch: allocated ${server.port}, detected ${detectedPort} from ${description}`
);
server.port = detectedPort;
}
logger.info(`Detected server URL via ${description}: ${detectedUrl}`);
// Emit URL update event
if (this.emitter) {
this.emitter.emit('dev-server:url-detected', {
worktreePath: server.worktreePath,
url: detectedUrl,
port: server.port,
timestamp: new Date().toISOString(),
});
}
break;
return;
}
}
}
// Phase 2: Try to detect just a port number from output (no full URL)
// Some servers only print "listening on port 3000" without a full URL
// Patterns are defined at module level (PORT_PATTERNS) and reused across calls
for (const { pattern, description } of PORT_PATTERNS) {
const match = cleanContent.match(pattern);
if (match && match[1]) {
const detectedPort = parseInt(match[1], 10);
// Sanity check: port should be in a reasonable range
if (detectedPort > 0 && detectedPort <= 65535) {
const detectedUrl = `http://localhost:${detectedPort}`;
server.url = detectedUrl;
server.urlDetected = true;
if (detectedPort !== server.port) {
logger.info(
`Port mismatch: allocated ${server.port}, detected ${detectedPort} from ${description}`
);
server.port = detectedPort;
}
logger.info(`Detected server port via ${description}: ${detectedPort}${detectedUrl}`);
// Emit URL update event
if (this.emitter) {
this.emitter.emit('dev-server:url-detected', {
worktreePath: server.worktreePath,
url: detectedUrl,
port: server.port,
timestamp: new Date().toISOString(),
});
}
return;
}
}
}
@@ -673,6 +835,7 @@ class DevServerService {
worktreePath: string;
port: number;
url: string;
urlDetected: boolean;
}>;
};
} {
@@ -680,6 +843,7 @@ class DevServerService {
worktreePath: s.worktreePath,
port: s.port,
url: s.url,
urlDetected: s.urlDetected,
}));
return {