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)
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
+11 -11
View File
@@ -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;
}
@@ -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'),
+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 { 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 (
<ProjectTabBar
projects={projects}
@@ -97,9 +91,6 @@ function ProjectTabBarWithContext({
onProjectClose={onProjectClose}
onAddProject={onAddProject}
onSettingsClick={onSettingsClick}
showArchived={showArchived}
archivedCount={archivedCount}
onToggleArchived={toggleShowArchived}
/>
);
}
@@ -721,7 +712,6 @@ export function App() {
onProjectClose={handleProjectTabClose}
onAddProject={handleAddProject}
onSettingsClick={() => setIsSettingsDialogOpen(true)}
tasks={tasks}
/>
</SortableContext>
@@ -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
<Plus className="h-4 w-4" />
</Button>
)}
{status === 'done' && onArchiveAll && tasks.length > 0 && (
{status === 'done' && onArchiveAll && tasks.length > 0 && !showArchived && (
<Button
variant="ghost"
size="icon"
@@ -227,6 +234,32 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli
<Archive className="h-4 w-4" />
</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>
@@ -281,7 +314,13 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
const { t } = useTranslation('tasks');
const [activeTask, setActiveTask] = useState<Task | 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
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}
/>
))}
</div>
@@ -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}
/>
);
})}
@@ -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({
</TooltipContent>
</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>
)}
@@ -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 && (
<p className="mt-2 text-xs text-muted-foreground line-clamp-2">
<p className="mt-2 text-xs text-muted-foreground">
{sanitizedDescription}
</p>
)}
@@ -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
+2 -2
View File
@@ -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() + '...';
}