diff --git a/apps/frontend/src/main/agent/agent-process.ts b/apps/frontend/src/main/agent/agent-process.ts index e96cb5c8..60bc8fe3 100644 --- a/apps/frontend/src/main/agent/agent-process.ts +++ b/apps/frontend/src/main/agent/agent-process.ts @@ -201,6 +201,8 @@ export class AgentProcessManager { // Auto-detect from app location (configured path was invalid or not set) const possiblePaths = [ + // Packaged app: backend is in extraResources (process.resourcesPath/backend) + ...(app.isPackaged ? [path.join(process.resourcesPath, 'backend')] : []), // Dev mode: from dist/main -> ../../backend (apps/frontend/out/main -> apps/backend) path.resolve(__dirname, '..', '..', '..', 'backend'), // Alternative: from app root -> apps/backend diff --git a/apps/frontend/src/main/insights/config.ts b/apps/frontend/src/main/insights/config.ts index 3ef00f30..d69a70d5 100644 --- a/apps/frontend/src/main/insights/config.ts +++ b/apps/frontend/src/main/insights/config.ts @@ -5,6 +5,7 @@ import { getProfileEnv } from '../rate-limit-detector'; import { getValidatedPythonPath } from '../python-detector'; import { getConfiguredPythonPath, pythonEnvManager } from '../python-env-manager'; import { getAugmentedEnv } from '../env-utils'; +import { getEffectiveSourcePath } from '../updater/path-resolver'; /** * Configuration manager for insights service @@ -41,24 +42,23 @@ export class InsightsConfig { /** * Get the auto-claude source path (detects automatically if not configured) + * Uses getEffectiveSourcePath() which handles userData override for user-updated backend */ getAutoBuildSourcePath(): string | null { if (this.autoBuildSourcePath && existsSync(this.autoBuildSourcePath)) { return this.autoBuildSourcePath; } - const possiblePaths = [ - // Apps structure: from out/main -> apps/backend - path.resolve(__dirname, '..', '..', '..', 'backend'), - path.resolve(app.getAppPath(), '..', 'backend'), - path.resolve(process.cwd(), 'apps', 'backend') - ]; - - for (const p of possiblePaths) { - if (existsSync(p) && existsSync(path.join(p, 'runners', 'spec_runner.py'))) { - return p; - } + // Use shared path resolver which handles: + // 1. User settings (autoBuildPath) + // 2. userData override (backend-source) for user-updated backend + // 3. Bundled backend (process.resourcesPath/backend) + // 4. Development paths + const effectivePath = getEffectiveSourcePath(); + if (existsSync(effectivePath) && existsSync(path.join(effectivePath, 'runners', 'spec_runner.py'))) { + return effectivePath; } + return null; } diff --git a/apps/frontend/src/main/terminal-name-generator.ts b/apps/frontend/src/main/terminal-name-generator.ts index afe31de1..d4429496 100644 --- a/apps/frontend/src/main/terminal-name-generator.ts +++ b/apps/frontend/src/main/terminal-name-generator.ts @@ -46,6 +46,23 @@ export class TerminalNameGenerator extends EventEmitter { return this.autoBuildSourcePath; } + // In packaged app, check userData override first (consistent with path-resolver.ts) + if (app.isPackaged) { + // Check for user-updated backend source first (takes priority over bundled) + const overridePath = path.join(app.getPath('userData'), 'backend-source'); + if (existsSync(overridePath) && existsSync(path.join(overridePath, 'runners', 'spec_runner.py'))) { + debug('Using user-updated backend from userData:', overridePath); + return overridePath; + } + // Fall back to bundled backend in resources + const resourcesPath = path.join(process.resourcesPath, 'backend'); + if (existsSync(resourcesPath) && existsSync(path.join(resourcesPath, 'runners', 'spec_runner.py'))) { + debug('Using bundled backend from resources:', resourcesPath); + return resourcesPath; + } + } + + // Development mode paths const possiblePaths = [ // Apps structure: from out/main -> apps/backend path.resolve(__dirname, '..', '..', '..', 'backend'), diff --git a/apps/frontend/src/renderer/App.tsx b/apps/frontend/src/renderer/App.tsx index cd6afdae..e9bfca32 100644 --- a/apps/frontend/src/renderer/App.tsx +++ b/apps/frontend/src/renderer/App.tsx @@ -63,10 +63,9 @@ import { COLOR_THEMES, UI_SCALE_MIN, UI_SCALE_MAX, UI_SCALE_DEFAULT } from '../s import type { Task, Project, ColorTheme } from '../shared/types'; import { ProjectTabBar } from './components/ProjectTabBar'; import { AddProjectModal } from './components/AddProjectModal'; -import { ViewStateProvider, useViewState } from './contexts/ViewStateContext'; +import { ViewStateProvider } from './contexts/ViewStateContext'; -// Wrapper component that connects ProjectTabBar to ViewStateContext -// (needed because App renders the Provider and can't use useViewState directly) +// Wrapper component for ProjectTabBar interface ProjectTabBarWithContextProps { projects: Project[]; activeProjectId: string | null; @@ -74,7 +73,6 @@ interface ProjectTabBarWithContextProps { onProjectClose: (projectId: string) => void; onAddProject: () => void; onSettingsClick: () => void; - tasks: Task[]; } function ProjectTabBarWithContext({ @@ -83,12 +81,8 @@ function ProjectTabBarWithContext({ onProjectSelect, onProjectClose, onAddProject, - onSettingsClick, - tasks + onSettingsClick }: ProjectTabBarWithContextProps) { - const { showArchived, toggleShowArchived } = useViewState(); - const archivedCount = tasks.filter(t => t.metadata?.archivedAt).length; - return ( ); } @@ -721,7 +712,6 @@ export function App() { onProjectClose={handleProjectTabClose} onAddProject={handleAddProject} onSettingsClick={() => setIsSettingsDialogOpen(true)} - tasks={tasks} /> diff --git a/apps/frontend/src/renderer/components/KanbanBoard.tsx b/apps/frontend/src/renderer/components/KanbanBoard.tsx index 6d653143..6541eede 100644 --- a/apps/frontend/src/renderer/components/KanbanBoard.tsx +++ b/apps/frontend/src/renderer/components/KanbanBoard.tsx @@ -22,6 +22,7 @@ import { import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive, RefreshCw } from 'lucide-react'; import { ScrollArea } from './ui/scroll-area'; import { Button } from './ui/button'; +import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; import { TaskCard } from './TaskCard'; import { SortableTaskCard } from './SortableTaskCard'; import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants'; @@ -44,6 +45,9 @@ interface DroppableColumnProps { isOver: boolean; onAddClick?: () => void; onArchiveAll?: () => void; + archivedCount?: number; + showArchived?: boolean; + onToggleArchived?: () => void; } /** @@ -83,6 +87,9 @@ function droppableColumnPropsAreEqual( if (prevProps.onTaskClick !== nextProps.onTaskClick) return false; if (prevProps.onAddClick !== nextProps.onAddClick) return false; if (prevProps.onArchiveAll !== nextProps.onArchiveAll) return false; + if (prevProps.archivedCount !== nextProps.archivedCount) return false; + if (prevProps.showArchived !== nextProps.showArchived) return false; + if (prevProps.onToggleArchived !== nextProps.onToggleArchived) return false; // Deep compare tasks const tasksEqual = tasksAreEquivalent(prevProps.tasks, nextProps.tasks); @@ -136,8 +143,8 @@ const getEmptyStateContent = (status: TaskStatus, t: (key: string) => string): { } }; -const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick, onArchiveAll }: DroppableColumnProps) { - const { t } = useTranslation('tasks'); +const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick, onArchiveAll, archivedCount, showArchived, onToggleArchived }: DroppableColumnProps) { + const { t } = useTranslation(['tasks', 'common']); const { setNodeRef } = useDroppable({ id: status }); @@ -216,7 +223,7 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli )} - {status === 'done' && onArchiveAll && tasks.length > 0 && ( + {status === 'done' && onArchiveAll && tasks.length > 0 && !showArchived && ( )} + {status === 'done' && archivedCount !== undefined && archivedCount > 0 && onToggleArchived && ( + + + + + + {showArchived ? t('common:projectTab.hideArchived') : t('common:projectTab.showArchived')} + + + )} @@ -281,7 +314,13 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR const { t } = useTranslation('tasks'); const [activeTask, setActiveTask] = useState(null); const [overColumnId, setOverColumnId] = useState(null); - const { showArchived } = useViewState(); + const { showArchived, toggleShowArchived } = useViewState(); + + // Calculate archived count for Done column button + const archivedCount = useMemo(() => + tasks.filter(t => t.metadata?.archivedAt).length, + [tasks] + ); // Filter tasks based on archive status const filteredTasks = useMemo(() => { @@ -445,6 +484,9 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR isOver={overColumnId === status} onAddClick={status === 'backlog' ? onNewTaskClick : undefined} onArchiveAll={status === 'done' ? handleArchiveAll : undefined} + archivedCount={status === 'done' ? archivedCount : undefined} + showArchived={status === 'done' ? showArchived : undefined} + onToggleArchived={status === 'done' ? toggleShowArchived : undefined} /> ))} diff --git a/apps/frontend/src/renderer/components/ProjectTabBar.tsx b/apps/frontend/src/renderer/components/ProjectTabBar.tsx index ef6e34d2..a3a9bc14 100644 --- a/apps/frontend/src/renderer/components/ProjectTabBar.tsx +++ b/apps/frontend/src/renderer/components/ProjectTabBar.tsx @@ -15,9 +15,6 @@ interface ProjectTabBarProps { className?: string; // Control props for active tab onSettingsClick?: () => void; - showArchived?: boolean; - archivedCount?: number; - onToggleArchived?: () => void; } export function ProjectTabBar({ @@ -27,10 +24,7 @@ export function ProjectTabBar({ onProjectClose, onAddProject, className, - onSettingsClick, - showArchived, - archivedCount, - onToggleArchived + onSettingsClick }: ProjectTabBarProps) { // Keyboard shortcuts for tab navigation useEffect(() => { @@ -109,9 +103,6 @@ export function ProjectTabBar({ }} // Pass control props only for active tab onSettingsClick={isActiveTab ? onSettingsClick : undefined} - showArchived={isActiveTab ? showArchived : undefined} - archivedCount={isActiveTab ? archivedCount : undefined} - onToggleArchived={isActiveTab ? onToggleArchived : undefined} /> ); })} diff --git a/apps/frontend/src/renderer/components/SortableProjectTab.tsx b/apps/frontend/src/renderer/components/SortableProjectTab.tsx index dc53e991..e6a9d6fd 100644 --- a/apps/frontend/src/renderer/components/SortableProjectTab.tsx +++ b/apps/frontend/src/renderer/components/SortableProjectTab.tsx @@ -1,7 +1,7 @@ import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { useTranslation } from 'react-i18next'; -import { Settings2, Archive } from 'lucide-react'; +import { Settings2 } from 'lucide-react'; import { cn } from '../lib/utils'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; import type { Project } from '../../shared/types'; @@ -15,9 +15,6 @@ interface SortableProjectTabProps { onClose: (e: React.MouseEvent) => void; // Optional control props for active tab onSettingsClick?: () => void; - showArchived?: boolean; - archivedCount?: number; - onToggleArchived?: () => void; } // Detect if running on macOS for keyboard shortcut display @@ -31,10 +28,7 @@ export function SortableProjectTab({ tabIndex, onSelect, onClose, - onSettingsClick, - showArchived, - archivedCount, - onToggleArchived + onSettingsClick }: SortableProjectTabProps) { const { t } = useTranslation('common'); // Build tooltip with keyboard shortcut hint (only for tabs 1-9) @@ -148,42 +142,6 @@ export function SortableProjectTab({ )} - - {/* Archive toggle button with badge - responsive sizing */} - {onToggleArchived && ( - - - - - - {showArchived ? t('projectTab.hideArchived') : t('projectTab.showArchived')} - - - )} )} diff --git a/apps/frontend/src/renderer/components/TaskCard.tsx b/apps/frontend/src/renderer/components/TaskCard.tsx index 994342cd..cc3d7bb9 100644 --- a/apps/frontend/src/renderer/components/TaskCard.tsx +++ b/apps/frontend/src/renderer/components/TaskCard.tsx @@ -100,8 +100,9 @@ export const TaskCard = memo(function TaskCard({ task, onClick }: TaskCardProps) const isIncomplete = isIncompleteHumanReview(task); // Memoize expensive computations to avoid running on every render + // Pass 0 to disable truncation - show full description on cards const sanitizedDescription = useMemo( - () => task.description ? sanitizeMarkdownForDisplay(task.description, 150) : null, + () => task.description ? sanitizeMarkdownForDisplay(task.description, 0) : null, [task.description] ); @@ -278,7 +279,7 @@ export const TaskCard = memo(function TaskCard({ task, onClick }: TaskCardProps) {/* Description - sanitized to handle markdown content (memoized) */} {sanitizedDescription && ( -

+

{sanitizedDescription}

)} diff --git a/apps/frontend/src/renderer/components/terminal/useXterm.ts b/apps/frontend/src/renderer/components/terminal/useXterm.ts index 9da5471a..e25c855e 100644 --- a/apps/frontend/src/renderer/components/terminal/useXterm.ts +++ b/apps/frontend/src/renderer/components/terminal/useXterm.ts @@ -73,6 +73,22 @@ export function useXterm({ terminalId, onCommandEnter, onResize }: UseXtermOptio xterm.attachCustomKeyEventHandler((event) => { const isMod = event.metaKey || event.ctrlKey; + // Handle SHIFT+Enter for multi-line input (send newline character) + // This matches VS Code/Cursor behavior for multi-line input in Claude Code + if (event.key === 'Enter' && event.shiftKey && !isMod && event.type === 'keydown') { + // Send ESC + newline - same as OPTION+Enter which works for multi-line + xterm.input('\x1b\n'); + return false; // Prevent default xterm handling + } + + // Handle CMD+Backspace (Mac) or Ctrl+Backspace (Windows/Linux) to delete line + // Sends Ctrl+U which is the terminal standard for "kill line backward" + const isDeleteLine = event.key === 'Backspace' && event.type === 'keydown' && isMod; + if (isDeleteLine) { + xterm.input('\x15'); // Ctrl+U + return false; + } + // Let Cmd/Ctrl + number keys pass through for project tab switching if (isMod && event.key >= '1' && event.key <= '9') { return false; // Don't handle in xterm, let it bubble up diff --git a/apps/frontend/src/renderer/lib/utils.ts b/apps/frontend/src/renderer/lib/utils.ts index dfed7152..2799994f 100644 --- a/apps/frontend/src/renderer/lib/utils.ts +++ b/apps/frontend/src/renderer/lib/utils.ts @@ -79,8 +79,8 @@ export function sanitizeMarkdownForDisplay(text: string, maxLength: number = 200 .replace(/\s+/g, ' ') .trim(); - // Truncate if needed - if (sanitized.length > maxLength) { + // Truncate if needed (0 means no truncation) + if (maxLength > 0 && sanitized.length > maxLength) { sanitized = sanitized.substring(0, maxLength).trim() + '...'; }