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:
@@ -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
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() + '...';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user