Fix/small fixes 2.7.3 (#631)

* refactor(ui): move show archived button from project tab to Done column

The "Show Archived" toggle button was located in the project tab header,
which was not intuitive since archived tasks are related to the Done
column. This moves the button to the Done column header where it makes
more contextual sense.

Changes:
- Added toggle button to Done column in KanbanBoard.tsx
- Button only appears when archived tasks exist (count > 0)
- Hide "Archive All" button when viewing archived tasks to avoid confusion
- Removed button and related props from SortableProjectTab, ProjectTabBar, App

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(terminal): fix terminal auto-naming in packaged release builds

Terminal auto-naming was failing silently in release builds because
getAutoBuildSourcePath() didn't check process.resourcesPath for packaged
apps. Added app.isPackaged check to use bundled backend path first,
with fallback to userData for user-updated backends.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(terminal): add SHIFT+Enter and CMD/Ctrl+Backspace keyboard shortcuts

Add missing keyboard shortcuts to match VS Code/Cursor terminal behavior:
- SHIFT+Enter: Insert newline for multi-line input in Claude Code CLI
- CMD+Backspace (Mac) / Ctrl+Backspace (Windows/Linux): Delete line

xterm.js doesn't natively support these shortcuts, so we intercept them
in the custom key handler and send the appropriate escape sequences.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(ui): move show archived button from project tab to Done column

The "Show Archived" toggle button was located in the project tab header,
which was not intuitive since archived tasks are related to the Done
column. This moves the button to the Done column header where it makes
more contextual sense.

Changes:
- Added toggle button to Done column in KanbanBoard.tsx
- Button only appears when archived tasks exist (count > 0)
- Hide "Archive All" button when viewing archived tasks to avoid confusion
- Removed button and related props from SortableProjectTab, ProjectTabBar, App

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): address PR review findings for consistency and clarity

- Use shared getEffectiveSourcePath() in insights/config.ts for consistent
  userData fallback path detection (matches terminal-name-generator.ts)
- Simplify redundant modifier key condition in useXterm.ts using existing
  isMod variable
- Make archivedCount check explicit with !== undefined in KanbanBoard.tsx
- Add 'common' namespace to useTranslation for proper cross-namespace access

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): remove task card description truncation

User requested full task descriptions on Kanban cards instead of
150-character previews. Removed character limit from sanitizeMarkdownForDisplay
call and removed line-clamp-2 CSS class. Kanban columns already have ScrollArea
so cards will scroll naturally if they get taller.

* fix(ui): properly disable task card description truncation

The previous change only removed the explicit 150 char limit, falling back
to the default 200 char limit. This fix:
- Updates sanitizeMarkdownForDisplay() to treat maxLength=0 as "no truncation"
- Passes 0 from TaskCard to fully disable description truncation on cards

* fix(main): consistent path resolution order in terminal-name-generator

The path resolution order was inconsistent with path-resolver.ts:
- terminal-name-generator checked bundled backend BEFORE userData override
- path-resolver checks userData override FIRST (correct priority)

This could cause version mismatches when users update their backend.
Now both modules check userData override first, falling back to bundled.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andy
2026-01-03 22:50:53 +01:00
committed by GitHub
parent f7b02e8795
commit 39da81935b
10 changed files with 103 additions and 86 deletions
@@ -201,6 +201,8 @@ export class AgentProcessManager {
// Auto-detect from app location (configured path was invalid or not set) // Auto-detect from app location (configured path was invalid or not set)
const possiblePaths = [ 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) // Dev mode: from dist/main -> ../../backend (apps/frontend/out/main -> apps/backend)
path.resolve(__dirname, '..', '..', '..', 'backend'), path.resolve(__dirname, '..', '..', '..', 'backend'),
// Alternative: from app root -> apps/backend // Alternative: from app root -> apps/backend
+11 -11
View File
@@ -5,6 +5,7 @@ import { getProfileEnv } from '../rate-limit-detector';
import { getValidatedPythonPath } from '../python-detector'; import { getValidatedPythonPath } from '../python-detector';
import { getConfiguredPythonPath, pythonEnvManager } from '../python-env-manager'; import { getConfiguredPythonPath, pythonEnvManager } from '../python-env-manager';
import { getAugmentedEnv } from '../env-utils'; import { getAugmentedEnv } from '../env-utils';
import { getEffectiveSourcePath } from '../updater/path-resolver';
/** /**
* Configuration manager for insights service * Configuration manager for insights service
@@ -41,24 +42,23 @@ export class InsightsConfig {
/** /**
* Get the auto-claude source path (detects automatically if not configured) * Get the auto-claude source path (detects automatically if not configured)
* Uses getEffectiveSourcePath() which handles userData override for user-updated backend
*/ */
getAutoBuildSourcePath(): string | null { getAutoBuildSourcePath(): string | null {
if (this.autoBuildSourcePath && existsSync(this.autoBuildSourcePath)) { if (this.autoBuildSourcePath && existsSync(this.autoBuildSourcePath)) {
return this.autoBuildSourcePath; return this.autoBuildSourcePath;
} }
const possiblePaths = [ // Use shared path resolver which handles:
// Apps structure: from out/main -> apps/backend // 1. User settings (autoBuildPath)
path.resolve(__dirname, '..', '..', '..', 'backend'), // 2. userData override (backend-source) for user-updated backend
path.resolve(app.getAppPath(), '..', 'backend'), // 3. Bundled backend (process.resourcesPath/backend)
path.resolve(process.cwd(), 'apps', 'backend') // 4. Development paths
]; const effectivePath = getEffectiveSourcePath();
if (existsSync(effectivePath) && existsSync(path.join(effectivePath, 'runners', 'spec_runner.py'))) {
for (const p of possiblePaths) { return effectivePath;
if (existsSync(p) && existsSync(path.join(p, 'runners', 'spec_runner.py'))) {
return p;
}
} }
return null; return null;
} }
@@ -46,6 +46,23 @@ export class TerminalNameGenerator extends EventEmitter {
return this.autoBuildSourcePath; 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 = [ const possiblePaths = [
// Apps structure: from out/main -> apps/backend // Apps structure: from out/main -> apps/backend
path.resolve(__dirname, '..', '..', '..', 'backend'), path.resolve(__dirname, '..', '..', '..', 'backend'),
+3 -13
View File
@@ -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 type { Task, Project, ColorTheme } from '../shared/types';
import { ProjectTabBar } from './components/ProjectTabBar'; import { ProjectTabBar } from './components/ProjectTabBar';
import { AddProjectModal } from './components/AddProjectModal'; import { AddProjectModal } from './components/AddProjectModal';
import { ViewStateProvider, useViewState } from './contexts/ViewStateContext'; import { ViewStateProvider } from './contexts/ViewStateContext';
// Wrapper component that connects ProjectTabBar to ViewStateContext // Wrapper component for ProjectTabBar
// (needed because App renders the Provider and can't use useViewState directly)
interface ProjectTabBarWithContextProps { interface ProjectTabBarWithContextProps {
projects: Project[]; projects: Project[];
activeProjectId: string | null; activeProjectId: string | null;
@@ -74,7 +73,6 @@ interface ProjectTabBarWithContextProps {
onProjectClose: (projectId: string) => void; onProjectClose: (projectId: string) => void;
onAddProject: () => void; onAddProject: () => void;
onSettingsClick: () => void; onSettingsClick: () => void;
tasks: Task[];
} }
function ProjectTabBarWithContext({ function ProjectTabBarWithContext({
@@ -83,12 +81,8 @@ function ProjectTabBarWithContext({
onProjectSelect, onProjectSelect,
onProjectClose, onProjectClose,
onAddProject, onAddProject,
onSettingsClick, onSettingsClick
tasks
}: ProjectTabBarWithContextProps) { }: ProjectTabBarWithContextProps) {
const { showArchived, toggleShowArchived } = useViewState();
const archivedCount = tasks.filter(t => t.metadata?.archivedAt).length;
return ( return (
<ProjectTabBar <ProjectTabBar
projects={projects} projects={projects}
@@ -97,9 +91,6 @@ function ProjectTabBarWithContext({
onProjectClose={onProjectClose} onProjectClose={onProjectClose}
onAddProject={onAddProject} onAddProject={onAddProject}
onSettingsClick={onSettingsClick} onSettingsClick={onSettingsClick}
showArchived={showArchived}
archivedCount={archivedCount}
onToggleArchived={toggleShowArchived}
/> />
); );
} }
@@ -721,7 +712,6 @@ export function App() {
onProjectClose={handleProjectTabClose} onProjectClose={handleProjectTabClose}
onAddProject={handleAddProject} onAddProject={handleAddProject}
onSettingsClick={() => setIsSettingsDialogOpen(true)} onSettingsClick={() => setIsSettingsDialogOpen(true)}
tasks={tasks}
/> />
</SortableContext> </SortableContext>
@@ -22,6 +22,7 @@ import {
import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive, RefreshCw } from 'lucide-react'; import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive, RefreshCw } from 'lucide-react';
import { ScrollArea } from './ui/scroll-area'; import { ScrollArea } from './ui/scroll-area';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { TaskCard } from './TaskCard'; import { TaskCard } from './TaskCard';
import { SortableTaskCard } from './SortableTaskCard'; import { SortableTaskCard } from './SortableTaskCard';
import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants'; import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants';
@@ -44,6 +45,9 @@ interface DroppableColumnProps {
isOver: boolean; isOver: boolean;
onAddClick?: () => void; onAddClick?: () => void;
onArchiveAll?: () => void; onArchiveAll?: () => void;
archivedCount?: number;
showArchived?: boolean;
onToggleArchived?: () => void;
} }
/** /**
@@ -83,6 +87,9 @@ function droppableColumnPropsAreEqual(
if (prevProps.onTaskClick !== nextProps.onTaskClick) return false; if (prevProps.onTaskClick !== nextProps.onTaskClick) return false;
if (prevProps.onAddClick !== nextProps.onAddClick) return false; if (prevProps.onAddClick !== nextProps.onAddClick) return false;
if (prevProps.onArchiveAll !== nextProps.onArchiveAll) 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 // Deep compare tasks
const tasksEqual = tasksAreEquivalent(prevProps.tasks, nextProps.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 DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick, onArchiveAll, archivedCount, showArchived, onToggleArchived }: DroppableColumnProps) {
const { t } = useTranslation('tasks'); const { t } = useTranslation(['tasks', 'common']);
const { setNodeRef } = useDroppable({ const { setNodeRef } = useDroppable({
id: status id: status
}); });
@@ -216,7 +223,7 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
</Button> </Button>
)} )}
{status === 'done' && onArchiveAll && tasks.length > 0 && ( {status === 'done' && onArchiveAll && tasks.length > 0 && !showArchived && (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@@ -227,6 +234,32 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli
<Archive className="h-4 w-4" /> <Archive className="h-4 w-4" />
</Button> </Button>
)} )}
{status === 'done' && archivedCount !== undefined && archivedCount > 0 && onToggleArchived && (
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
'h-7 w-7 transition-colors relative',
showArchived
? 'text-primary bg-primary/10 hover:bg-primary/20'
: 'hover:bg-muted-foreground/10 hover:text-muted-foreground'
)}
onClick={onToggleArchived}
aria-pressed={showArchived}
>
<Archive className="h-4 w-4" />
<span className="absolute -top-1 -right-1 text-[10px] font-medium bg-muted rounded-full min-w-[14px] h-[14px] flex items-center justify-center">
{archivedCount}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{showArchived ? t('common:projectTab.hideArchived') : t('common:projectTab.showArchived')}
</TooltipContent>
</Tooltip>
)}
</div> </div>
</div> </div>
@@ -281,7 +314,13 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
const { t } = useTranslation('tasks'); const { t } = useTranslation('tasks');
const [activeTask, setActiveTask] = useState<Task | null>(null); const [activeTask, setActiveTask] = useState<Task | null>(null);
const [overColumnId, setOverColumnId] = useState<string | null>(null); const [overColumnId, setOverColumnId] = useState<string | null>(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 // Filter tasks based on archive status
const filteredTasks = useMemo(() => { const filteredTasks = useMemo(() => {
@@ -445,6 +484,9 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
isOver={overColumnId === status} isOver={overColumnId === status}
onAddClick={status === 'backlog' ? onNewTaskClick : undefined} onAddClick={status === 'backlog' ? onNewTaskClick : undefined}
onArchiveAll={status === 'done' ? handleArchiveAll : undefined} onArchiveAll={status === 'done' ? handleArchiveAll : undefined}
archivedCount={status === 'done' ? archivedCount : undefined}
showArchived={status === 'done' ? showArchived : undefined}
onToggleArchived={status === 'done' ? toggleShowArchived : undefined}
/> />
))} ))}
</div> </div>
@@ -15,9 +15,6 @@ interface ProjectTabBarProps {
className?: string; className?: string;
// Control props for active tab // Control props for active tab
onSettingsClick?: () => void; onSettingsClick?: () => void;
showArchived?: boolean;
archivedCount?: number;
onToggleArchived?: () => void;
} }
export function ProjectTabBar({ export function ProjectTabBar({
@@ -27,10 +24,7 @@ export function ProjectTabBar({
onProjectClose, onProjectClose,
onAddProject, onAddProject,
className, className,
onSettingsClick, onSettingsClick
showArchived,
archivedCount,
onToggleArchived
}: ProjectTabBarProps) { }: ProjectTabBarProps) {
// Keyboard shortcuts for tab navigation // Keyboard shortcuts for tab navigation
useEffect(() => { useEffect(() => {
@@ -109,9 +103,6 @@ export function ProjectTabBar({
}} }}
// Pass control props only for active tab // Pass control props only for active tab
onSettingsClick={isActiveTab ? onSettingsClick : undefined} onSettingsClick={isActiveTab ? onSettingsClick : undefined}
showArchived={isActiveTab ? showArchived : undefined}
archivedCount={isActiveTab ? archivedCount : undefined}
onToggleArchived={isActiveTab ? onToggleArchived : undefined}
/> />
); );
})} })}
@@ -1,7 +1,7 @@
import { useSortable } from '@dnd-kit/sortable'; import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Settings2, Archive } from 'lucide-react'; import { Settings2 } from 'lucide-react';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import type { Project } from '../../shared/types'; import type { Project } from '../../shared/types';
@@ -15,9 +15,6 @@ interface SortableProjectTabProps {
onClose: (e: React.MouseEvent) => void; onClose: (e: React.MouseEvent) => void;
// Optional control props for active tab // Optional control props for active tab
onSettingsClick?: () => void; onSettingsClick?: () => void;
showArchived?: boolean;
archivedCount?: number;
onToggleArchived?: () => void;
} }
// Detect if running on macOS for keyboard shortcut display // Detect if running on macOS for keyboard shortcut display
@@ -31,10 +28,7 @@ export function SortableProjectTab({
tabIndex, tabIndex,
onSelect, onSelect,
onClose, onClose,
onSettingsClick, onSettingsClick
showArchived,
archivedCount,
onToggleArchived
}: SortableProjectTabProps) { }: SortableProjectTabProps) {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
// Build tooltip with keyboard shortcut hint (only for tabs 1-9) // Build tooltip with keyboard shortcut hint (only for tabs 1-9)
@@ -148,42 +142,6 @@ export function SortableProjectTab({
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)} )}
{/* Archive toggle button with badge - responsive sizing */}
{onToggleArchived && (
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<button
type="button"
className={cn(
'h-5 sm:h-6 px-1 sm:px-1.5 rounded',
'flex items-center justify-center gap-0.5 sm:gap-1',
'transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',
showArchived
? 'text-primary bg-primary/10 hover:bg-primary/20'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
)}
onClick={(e) => {
e.stopPropagation();
onToggleArchived();
}}
aria-label={showArchived ? t('projectTab.hideArchivedTasks') : t('projectTab.showArchivedTasks')}
aria-pressed={showArchived}
>
<Archive className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
{typeof archivedCount === 'number' && archivedCount > 0 && (
<span className="text-[9px] sm:text-[10px] font-medium min-w-[12px] sm:min-w-[14px] text-center">
{archivedCount}
</span>
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
<span>{showArchived ? t('projectTab.hideArchived') : t('projectTab.showArchived')}</span>
</TooltipContent>
</Tooltip>
)}
</div> </div>
)} )}
@@ -100,8 +100,9 @@ export const TaskCard = memo(function TaskCard({ task, onClick }: TaskCardProps)
const isIncomplete = isIncompleteHumanReview(task); const isIncomplete = isIncompleteHumanReview(task);
// Memoize expensive computations to avoid running on every render // Memoize expensive computations to avoid running on every render
// Pass 0 to disable truncation - show full description on cards
const sanitizedDescription = useMemo( const sanitizedDescription = useMemo(
() => task.description ? sanitizeMarkdownForDisplay(task.description, 150) : null, () => task.description ? sanitizeMarkdownForDisplay(task.description, 0) : null,
[task.description] [task.description]
); );
@@ -278,7 +279,7 @@ export const TaskCard = memo(function TaskCard({ task, onClick }: TaskCardProps)
{/* Description - sanitized to handle markdown content (memoized) */} {/* Description - sanitized to handle markdown content (memoized) */}
{sanitizedDescription && ( {sanitizedDescription && (
<p className="mt-2 text-xs text-muted-foreground line-clamp-2"> <p className="mt-2 text-xs text-muted-foreground">
{sanitizedDescription} {sanitizedDescription}
</p> </p>
)} )}
@@ -73,6 +73,22 @@ export function useXterm({ terminalId, onCommandEnter, onResize }: UseXtermOptio
xterm.attachCustomKeyEventHandler((event) => { xterm.attachCustomKeyEventHandler((event) => {
const isMod = event.metaKey || event.ctrlKey; 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 // Let Cmd/Ctrl + number keys pass through for project tab switching
if (isMod && event.key >= '1' && event.key <= '9') { if (isMod && event.key >= '1' && event.key <= '9') {
return false; // Don't handle in xterm, let it bubble up return false; // Don't handle in xterm, let it bubble up
+2 -2
View File
@@ -79,8 +79,8 @@ export function sanitizeMarkdownForDisplay(text: string, maxLength: number = 200
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.trim(); .trim();
// Truncate if needed // Truncate if needed (0 means no truncation)
if (sanitized.length > maxLength) { if (maxLength > 0 && sanitized.length > maxLength) {
sanitized = sanitized.substring(0, maxLength).trim() + '...'; sanitized = sanitized.substring(0, maxLength).trim() + '...';
} }