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() + '...';
}