mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-19 10:43:08 +00:00
Improve pull request flow, add branch selection for worktree creation, fix auto-mode concurrency count (#787)
* Changes from fix/fetch-before-pull-fetch * feat: Improve pull request flow, add branch selection for worktree creation, fix for automode concurrency count * feat: Add validation for remote names and improve error handling * Address PR comments and mobile layout fixes * ``` refactor: Extract PR target resolution logic into dedicated service ``` * feat: Add app shell UI and improve service imports. Address PR comments * fix: Improve security validation and cache handling in git operations * feat: Add GET /list endpoint and improve parameter handling * chore: Improve validation, accessibility, and error handling across apps * chore: Format vite server port configuration * fix: Add error handling for gh pr list command and improve offline fallbacks * fix: Preserve existing PR creation time and improve remote handling
This commit is contained in:
@@ -203,6 +203,8 @@ export interface CreatePROptions {
|
||||
baseBranch?: string;
|
||||
draft?: boolean;
|
||||
remote?: string;
|
||||
/** Remote to create the PR against (e.g. upstream). If not specified, inferred from repo setup. */
|
||||
targetRemote?: string;
|
||||
}
|
||||
|
||||
// Re-export types from electron.d.ts for external use
|
||||
|
||||
@@ -727,7 +727,10 @@ export class HttpApiClient implements ElectronAPI {
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
this.reconnectAttempts = 0; // Reset backoff on visibility change
|
||||
this.connectWebSocket();
|
||||
// Use silent mode: a 401 during visibility-change reconnect should NOT
|
||||
// trigger a full logout cascade. The session is verified separately via
|
||||
// verifySession() in __root.tsx's fast-hydrate path.
|
||||
this.connectWebSocket({ silent: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -737,10 +740,15 @@ export class HttpApiClient implements ElectronAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a short-lived WebSocket token from the server
|
||||
* Used for secure WebSocket authentication without exposing session tokens in URLs
|
||||
* Fetch a short-lived WebSocket token from the server.
|
||||
* Used for secure WebSocket authentication without exposing session tokens in URLs.
|
||||
*
|
||||
* @param options.silent - When true, a 401/403 will NOT trigger handleUnauthorized().
|
||||
* Use this for background reconnections (e.g., visibility-change) where a transient
|
||||
* auth failure should not force a full logout cascade. The actual session validity
|
||||
* is verified separately via verifySession() in the fast-hydrate path.
|
||||
*/
|
||||
private async fetchWsToken(): Promise<string | null> {
|
||||
private async fetchWsToken(options?: { silent?: boolean }): Promise<string | null> {
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -759,7 +767,11 @@ export class HttpApiClient implements ElectronAPI {
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
handleUnauthorized();
|
||||
if (options?.silent) {
|
||||
logger.debug('fetchWsToken: 401/403 during silent reconnect — skipping logout');
|
||||
} else {
|
||||
handleUnauthorized();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -780,7 +792,7 @@ export class HttpApiClient implements ElectronAPI {
|
||||
}
|
||||
}
|
||||
|
||||
private connectWebSocket(): void {
|
||||
private connectWebSocket(options?: { silent?: boolean }): void {
|
||||
if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
||||
return;
|
||||
}
|
||||
@@ -790,14 +802,14 @@ export class HttpApiClient implements ElectronAPI {
|
||||
// Wait for API key initialization to complete before attempting connection
|
||||
// This prevents race conditions during app startup
|
||||
waitForApiKeyInit()
|
||||
.then(() => this.doConnectWebSocketInternal())
|
||||
.then(() => this.doConnectWebSocketInternal(options))
|
||||
.catch((error) => {
|
||||
logger.error('Failed to initialize for WebSocket connection:', error);
|
||||
this.isConnecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
private doConnectWebSocketInternal(): void {
|
||||
private doConnectWebSocketInternal(options?: { silent?: boolean }): void {
|
||||
// Electron mode typically authenticates with the injected API key.
|
||||
// However, in external-server/cookie-auth flows, the API key may be unavailable.
|
||||
// In that case, fall back to the same wsToken/cookie authentication used in web mode
|
||||
@@ -806,7 +818,7 @@ export class HttpApiClient implements ElectronAPI {
|
||||
const apiKey = getApiKey();
|
||||
if (!apiKey) {
|
||||
logger.warn('Electron mode: API key missing, attempting wsToken/cookie auth for WebSocket');
|
||||
this.fetchWsToken()
|
||||
this.fetchWsToken(options)
|
||||
.then((wsToken) => {
|
||||
const wsUrl = this.serverUrl.replace(/^http/, 'ws') + '/api/events';
|
||||
if (wsToken) {
|
||||
@@ -830,7 +842,7 @@ export class HttpApiClient implements ElectronAPI {
|
||||
}
|
||||
|
||||
// In web mode, fetch a short-lived wsToken first
|
||||
this.fetchWsToken()
|
||||
this.fetchWsToken(options)
|
||||
.then((wsToken) => {
|
||||
const wsUrl = this.serverUrl.replace(/^http/, 'ws') + '/api/events';
|
||||
if (wsToken) {
|
||||
@@ -961,7 +973,7 @@ export class HttpApiClient implements ElectronAPI {
|
||||
return headers;
|
||||
}
|
||||
|
||||
private async post<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||
private async post<T>(endpoint: string, body?: unknown, signal?: AbortSignal): Promise<T> {
|
||||
// Ensure API key is initialized before making request
|
||||
await waitForApiKeyInit();
|
||||
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||
@@ -969,6 +981,7 @@ export class HttpApiClient implements ElectronAPI {
|
||||
headers: this.getHeaders(),
|
||||
credentials: 'include', // Include cookies for session auth
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal,
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
@@ -1899,7 +1912,8 @@ export class HttpApiClient implements ElectronAPI {
|
||||
error?: string;
|
||||
}>;
|
||||
} = {
|
||||
getAll: (projectPath: string) => this.post('/api/features/list', { projectPath }),
|
||||
getAll: (projectPath: string) =>
|
||||
this.get(`/api/features/list?projectPath=${encodeURIComponent(projectPath)}`),
|
||||
get: (projectPath: string, featureId: string) =>
|
||||
this.post('/api/features/get', { projectPath, featureId }),
|
||||
create: (projectPath: string, feature: Feature) =>
|
||||
@@ -2155,8 +2169,8 @@ export class HttpApiClient implements ElectronAPI {
|
||||
}),
|
||||
checkChanges: (worktreePath: string) =>
|
||||
this.post('/api/worktree/check-changes', { worktreePath }),
|
||||
listBranches: (worktreePath: string, includeRemote?: boolean) =>
|
||||
this.post('/api/worktree/list-branches', { worktreePath, includeRemote }),
|
||||
listBranches: (worktreePath: string, includeRemote?: boolean, signal?: AbortSignal) =>
|
||||
this.post('/api/worktree/list-branches', { worktreePath, includeRemote }, signal),
|
||||
switchBranch: (worktreePath: string, branchName: string) =>
|
||||
this.post('/api/worktree/switch-branch', { worktreePath, branchName }),
|
||||
listRemotes: (worktreePath: string) =>
|
||||
|
||||
@@ -63,6 +63,26 @@ export function isSlowConnection(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the app is running as an installed PWA (standalone mode).
|
||||
* Checks both the standard display-mode media query and the iOS-specific
|
||||
* navigator.standalone property for comprehensive detection.
|
||||
*
|
||||
* When running as a PWA, the browser chrome is hidden so safe area insets
|
||||
* can be reduced further to maximize usable screen space.
|
||||
*/
|
||||
export const isPwaStandalone: boolean = (() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
// Standard: works on Chrome, Edge, Firefox, and modern Safari
|
||||
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
|
||||
|
||||
// iOS Safari: navigator.standalone is true when launched from home screen
|
||||
const isIOSStandalone = (navigator as Navigator & { standalone?: boolean }).standalone === true;
|
||||
|
||||
return isStandalone || isIOSStandalone;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Multiplier for polling intervals on mobile.
|
||||
* Mobile devices benefit from less frequent polling to save battery and bandwidth.
|
||||
|
||||
Reference in New Issue
Block a user