diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80cd2a6c..67e0dacc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,17 +73,6 @@ jobs: npm_config_build_from_source=true npx @electron/rebuild -f -a "$A" -v "$ELECTRON_VERSION" -w sqlite3,node-pty,keytar done - - name: Rebuild native modules (per-arch) - run: | - set -euo pipefail - ELECTRON_VERSION=$(node -p "require('electron/package.json').version") - ARCH_INPUT="${{ github.event.inputs.arch }}" - if [ -z "$ARCH_INPUT" ] || [ "$ARCH_INPUT" = "both" ]; then ARGS=(x64 arm64); else ARGS=("$ARCH_INPUT"); fi - echo "Rebuilding sqlite3,node-pty,keytar for Electron $ELECTRON_VERSION: ${ARGS[*]}" - for A in "${ARGS[@]}"; do - npm_config_build_from_source=true npx @electron/rebuild -f -a "$A" -v "$ELECTRON_VERSION" -w sqlite3,node-pty,keytar - done - - name: Inject PostHog config (dev build job) env: PH_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY }} diff --git a/src/main/services/GitHubService.ts b/src/main/services/GitHubService.ts index f4baec65..b7a96ec6 100644 --- a/src/main/services/GitHubService.ts +++ b/src/main/services/GitHubService.ts @@ -592,14 +592,28 @@ export class GitHubService { try { const token = await this.getStoredToken(); - if (!token) { - // No stored token, user needs to authenticate - return false; + if (token) { + // Test the token by making a simple API call + const user = await this.getUserInfo(token); + return !!user; } - // Test the token by making a simple API call - const user = await this.getUserInfo(token); - return !!user; + // No stored token: treat an existing `gh auth login` as authenticated and + // opportunistically persist the token so we can re-auth `gh` later. + try { + const { stdout } = await this.execGH('gh auth status'); + const loggedIn = /Logged in to github\.com/i.test(stdout); + if (!loggedIn) return false; + + const { stdout: tokenStdout } = await this.execGH('gh auth token'); + const cliToken = String(tokenStdout || '').trim(); + if (cliToken) { + await this.storeToken(cliToken); + } + return true; + } catch { + return false; + } } catch (error) { console.error('Authentication check failed:', error); return false; diff --git a/src/renderer/components/TaskTerminalPanel.tsx b/src/renderer/components/TaskTerminalPanel.tsx index 1047122a..e8935811 100644 --- a/src/renderer/components/TaskTerminalPanel.tsx +++ b/src/renderer/components/TaskTerminalPanel.tsx @@ -1,11 +1,18 @@ -import React, { useMemo, useState, useEffect } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Plus, Terminal, X, Bot } from 'lucide-react'; import { TerminalPane } from './TerminalPane'; -import { Bot, Terminal, Plus, X } from 'lucide-react'; import { useTheme } from '../hooks/useTheme'; import { useTaskTerminals } from '@/lib/taskTerminalsStore'; import { cn } from '@/lib/utils'; -import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from './ui/tooltip'; import type { Provider } from '../types'; +import { captureTelemetry } from '../lib/telemetryClient'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; interface Task { id: string; @@ -29,15 +36,27 @@ const TaskTerminalPanelComponent: React.FC = ({ projectPath, }) => { const { effectiveTheme } = useTheme(); + const taskKey = task?.id ?? 'task-placeholder'; const taskTerminals = useTaskTerminals(taskKey, task?.path); const globalTerminals = useTaskTerminals('global', projectPath, { defaultCwd: projectPath }); + const [mode, setMode] = useState<'task' | 'global'>(task ? 'task' : 'global'); + const [userInitiatedModeChange, setUserInitiatedModeChange] = useState(false); + const [switchingTerminalId, setSwitchingTerminalId] = useState(null); + + const handleModeChange = useCallback((value: string) => { + if (value !== 'task' && value !== 'global') return; + setUserInitiatedModeChange(true); + setMode(value); + setTimeout(() => setUserInitiatedModeChange(false), 1000); + }, []); + useEffect(() => { - if (!task && mode === 'task') { + if (!task && mode === 'task' && !userInitiatedModeChange) { setMode('global'); } - }, [task, mode]); + }, [task, mode, userInitiatedModeChange]); const { terminals, @@ -72,7 +91,6 @@ const TaskTerminalPanelComponent: React.FC = ({ brightWhite?: string; } | null>(null); - // Fetch native terminal theme on mount useEffect(() => { void (async () => { try { @@ -81,78 +99,66 @@ const TaskTerminalPanelComponent: React.FC = ({ setNativeTheme(result.config.theme); } } catch (error) { - // Silently fail - fall back to default theme console.warn('Failed to load native terminal theme', error); } })(); }, []); - // Default theme (VS Code inspired) - const defaultTheme = useMemo(() => { - // Mistral-specific theme: white in light mode, app blue-gray background in dark mode + const themeOverride = useMemo(() => { const isMistral = provider === 'mistral'; const darkBackground = isMistral ? '#202938' : '#1e1e1e'; - return effectiveTheme === 'dark' - ? { - background: darkBackground, - foreground: '#d4d4d4', - cursor: '#aeafad', - cursorAccent: darkBackground, - selectionBackground: '#264f78', - black: '#000000', - red: '#cd3131', - green: '#0dbc79', - yellow: '#e5e510', - blue: '#2472c8', - magenta: '#bc3fbc', - cyan: '#11a8cd', - white: '#e5e5e5', - brightBlack: '#666666', - brightRed: '#f14c4c', - brightGreen: '#23d18b', - brightYellow: '#f5f543', - brightBlue: '#3b8eea', - brightMagenta: '#d670d6', - brightCyan: '#29b8db', - brightWhite: '#ffffff', - } - : { - background: '#ffffff', - foreground: '#1e1e1e', - cursor: '#1e1e1e', - cursorAccent: '#ffffff', - selectionBackground: '#add6ff', - black: '#000000', - red: '#cd3131', - green: '#0dbc79', - yellow: '#bf8803', - blue: '#0451a5', - magenta: '#bc05bc', - cyan: '#0598bc', - white: '#e5e5e5', - brightBlack: '#666666', - brightRed: '#cd3131', - brightGreen: '#14ce14', - brightYellow: '#b5ba00', - brightBlue: '#0451a5', - brightMagenta: '#bc05bc', - brightCyan: '#0598bc', - brightWhite: '#a5a5a5', - }; - }, [effectiveTheme, provider]); + const baseTheme = + effectiveTheme === 'dark' + ? { + background: darkBackground, + foreground: '#d4d4d4', + cursor: '#aeafad', + cursorAccent: darkBackground, + selectionBackground: '#264f78', + black: '#000000', + red: '#cd3131', + green: '#0dbc79', + yellow: '#e5e510', + blue: '#2472c8', + magenta: '#bc3fbc', + cyan: '#11a8cd', + white: '#e5e5e5', + brightBlack: '#666666', + brightRed: '#f14c4c', + brightGreen: '#23d18b', + brightYellow: '#f5f543', + brightBlue: '#3b8eea', + brightMagenta: '#d670d6', + brightCyan: '#29b8db', + brightWhite: '#ffffff', + } + : { + background: '#ffffff', + foreground: '#1e1e1e', + cursor: '#1e1e1e', + cursorAccent: '#ffffff', + selectionBackground: '#add6ff', + black: '#000000', + red: '#cd3131', + green: '#0dbc79', + yellow: '#bf8803', + blue: '#0451a5', + magenta: '#bc05bc', + cyan: '#0598bc', + white: '#e5e5e5', + brightBlack: '#666666', + brightRed: '#cd3131', + brightGreen: '#14ce14', + brightYellow: '#b5ba00', + brightBlue: '#0451a5', + brightMagenta: '#bc05bc', + brightCyan: '#0598bc', + brightWhite: '#a5a5a5', + }; - // Merge native theme with defaults (native theme takes precedence) - const themeOverride = useMemo(() => { - if (!nativeTheme) { - return defaultTheme; - } - // Merge: native theme values override defaults, but we keep defaults for missing values - return { - ...defaultTheme, - ...nativeTheme, - }; - }, [nativeTheme, defaultTheme]); + return nativeTheme ? { ...baseTheme, ...nativeTheme } : baseTheme; + }, [effectiveTheme, provider, nativeTheme]); if (!task && !projectPath) { return ( @@ -171,66 +177,72 @@ const TaskTerminalPanelComponent: React.FC = ({ return (
+ + Choose between task worktree or global terminal scope + + {!task && ( + + Worktree terminal requires an active task + + )} + {!projectPath && ( + + Global terminal requires a project path + + )} +
- - - {!task ? ( - <> - - - - - - -

Select a task to access its worktree terminal.

-
- - ) : ( - - - - )} -
-
-
+
{terminals.map((terminal) => { const isActive = terminal.id === activeTerminalId; @@ -238,7 +250,25 @@ const TaskTerminalPanelComponent: React.FC = ({