mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
feat: Implement responsive mobile header layout with menu consolidation
This commit is contained in:
@@ -19,7 +19,7 @@ export function BoardControls({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-5">
|
||||||
{/* Board Background Button */}
|
{/* Board Background Button */}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Bot, Wand2, Settings2, GitBranch } from 'lucide-react';
|
|||||||
import { UsagePopover } from '@/components/usage-popover';
|
import { UsagePopover } from '@/components/usage-popover';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
|
import { useIsMobile } from '@/hooks/use-media-query';
|
||||||
import { AutoModeSettingsDialog } from './dialogs/auto-mode-settings-dialog';
|
import { AutoModeSettingsDialog } from './dialogs/auto-mode-settings-dialog';
|
||||||
import { WorktreeSettingsDialog } from './dialogs/worktree-settings-dialog';
|
import { WorktreeSettingsDialog } from './dialogs/worktree-settings-dialog';
|
||||||
import { PlanSettingsDialog } from './dialogs/plan-settings-dialog';
|
import { PlanSettingsDialog } from './dialogs/plan-settings-dialog';
|
||||||
@@ -15,6 +16,7 @@ import { getHttpApiClient } from '@/lib/http-api-client';
|
|||||||
import { BoardSearchBar } from './board-search-bar';
|
import { BoardSearchBar } from './board-search-bar';
|
||||||
import { BoardControls } from './board-controls';
|
import { BoardControls } from './board-controls';
|
||||||
import { ViewToggle, type ViewMode } from './components';
|
import { ViewToggle, type ViewMode } from './components';
|
||||||
|
import { HeaderMobileMenu } from './header-mobile-menu';
|
||||||
|
|
||||||
export type { ViewMode };
|
export type { ViewMode };
|
||||||
|
|
||||||
@@ -120,8 +122,10 @@ export function BoardHeader({
|
|||||||
// Show if Codex is authenticated (CLI or API key)
|
// Show if Codex is authenticated (CLI or API key)
|
||||||
const showCodexUsage = !!codexAuthStatus?.authenticated;
|
const showCodexUsage = !!codexAuthStatus?.authenticated;
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
<div className="flex items-center justify-between gap-5 p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<BoardSearchBar
|
<BoardSearchBar
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
@@ -130,12 +134,7 @@ export function BoardHeader({
|
|||||||
creatingSpecProjectPath={creatingSpecProjectPath}
|
creatingSpecProjectPath={creatingSpecProjectPath}
|
||||||
currentProjectPath={projectPath}
|
currentProjectPath={projectPath}
|
||||||
/>
|
/>
|
||||||
{isMounted && (
|
{isMounted && <ViewToggle viewMode={viewMode} onViewModeChange={onViewModeChange} />}
|
||||||
<ViewToggle
|
|
||||||
viewMode={viewMode}
|
|
||||||
onViewModeChange={onViewModeChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<BoardControls
|
<BoardControls
|
||||||
isMounted={isMounted}
|
isMounted={isMounted}
|
||||||
onShowBoardBackground={onShowBoardBackground}
|
onShowBoardBackground={onShowBoardBackground}
|
||||||
@@ -143,12 +142,28 @@ export function BoardHeader({
|
|||||||
completedCount={completedCount}
|
completedCount={completedCount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
{/* Usage Popover - show if either provider is authenticated */}
|
{/* Usage Popover - show if either provider is authenticated */}
|
||||||
{isMounted && (showClaudeUsage || showCodexUsage) && <UsagePopover />}
|
{isMounted && (showClaudeUsage || showCodexUsage) && <UsagePopover />}
|
||||||
|
|
||||||
|
{/* Mobile view: show hamburger menu with all controls */}
|
||||||
|
{isMounted && isMobile && (
|
||||||
|
<HeaderMobileMenu
|
||||||
|
isWorktreePanelVisible={isWorktreePanelVisible}
|
||||||
|
onWorktreePanelToggle={handleWorktreePanelToggle}
|
||||||
|
maxConcurrency={maxConcurrency}
|
||||||
|
runningAgentsCount={runningAgentsCount}
|
||||||
|
onConcurrencyChange={onConcurrencyChange}
|
||||||
|
isAutoModeRunning={isAutoModeRunning}
|
||||||
|
onAutoModeToggle={onAutoModeToggle}
|
||||||
|
onOpenAutoModeSettings={() => setShowAutoModeSettings(true)}
|
||||||
|
onOpenPlanDialog={onOpenPlanDialog}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Desktop view: show full controls */}
|
||||||
{/* Worktrees Toggle - only show after mount to prevent hydration issues */}
|
{/* Worktrees Toggle - only show after mount to prevent hydration issues */}
|
||||||
{isMounted && (
|
{isMounted && !isMobile && (
|
||||||
<div className={controlContainerClass} data-testid="worktrees-toggle-container">
|
<div className={controlContainerClass} data-testid="worktrees-toggle-container">
|
||||||
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||||
<Label htmlFor="worktrees-toggle" className="text-sm font-medium cursor-pointer">
|
<Label htmlFor="worktrees-toggle" className="text-sm font-medium cursor-pointer">
|
||||||
@@ -180,7 +195,7 @@ export function BoardHeader({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Concurrency Control - only show after mount to prevent hydration issues */}
|
{/* Concurrency Control - only show after mount to prevent hydration issues */}
|
||||||
{isMounted && (
|
{isMounted && !isMobile && (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<button
|
<button
|
||||||
@@ -223,7 +238,7 @@ export function BoardHeader({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
|
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
|
||||||
{isMounted && (
|
{isMounted && !isMobile && (
|
||||||
<div className={controlContainerClass} data-testid="auto-mode-toggle-container">
|
<div className={controlContainerClass} data-testid="auto-mode-toggle-container">
|
||||||
<Label htmlFor="auto-mode-toggle" className="text-sm font-medium cursor-pointer">
|
<Label htmlFor="auto-mode-toggle" className="text-sm font-medium cursor-pointer">
|
||||||
Auto Mode
|
Auto Mode
|
||||||
@@ -253,25 +268,27 @@ export function BoardHeader({
|
|||||||
onSkipVerificationChange={setSkipVerificationInAutoMode}
|
onSkipVerificationChange={setSkipVerificationInAutoMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Plan Button with Settings */}
|
{/* Plan Button with Settings - only show on desktop, mobile has it in the menu */}
|
||||||
<div className={controlContainerClass} data-testid="plan-button-container">
|
{!isMobile && (
|
||||||
<button
|
<div className={controlContainerClass} data-testid="plan-button-container">
|
||||||
onClick={onOpenPlanDialog}
|
<button
|
||||||
className="flex items-center gap-1.5 hover:text-foreground transition-colors"
|
onClick={onOpenPlanDialog}
|
||||||
data-testid="plan-backlog-button"
|
className="flex items-center gap-1.5 hover:text-foreground transition-colors"
|
||||||
>
|
data-testid="plan-backlog-button"
|
||||||
<Wand2 className="w-4 h-4 text-muted-foreground" />
|
>
|
||||||
<span className="text-sm font-medium">Plan</span>
|
<Wand2 className="w-4 h-4 text-muted-foreground" />
|
||||||
</button>
|
<span className="text-sm font-medium">Plan</span>
|
||||||
<button
|
</button>
|
||||||
onClick={() => setShowPlanSettings(true)}
|
<button
|
||||||
className="p-1 rounded hover:bg-accent/50 transition-colors"
|
onClick={() => setShowPlanSettings(true)}
|
||||||
title="Plan Settings"
|
className="p-1 rounded hover:bg-accent/50 transition-colors"
|
||||||
data-testid="plan-settings-button"
|
title="Plan Settings"
|
||||||
>
|
data-testid="plan-settings-button"
|
||||||
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
>
|
||||||
</button>
|
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Plan Settings Dialog */}
|
{/* Plan Settings Dialog */}
|
||||||
<PlanSettingsDialog
|
<PlanSettingsDialog
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface ColumnDef {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Default column definitions for the list view
|
* Default column definitions for the list view
|
||||||
|
* Only showing title column with full width for a cleaner, more spacious layout
|
||||||
*/
|
*/
|
||||||
export const LIST_COLUMNS: ColumnDef[] = [
|
export const LIST_COLUMNS: ColumnDef[] = [
|
||||||
{
|
{
|
||||||
@@ -30,42 +31,7 @@ export const LIST_COLUMNS: ColumnDef[] = [
|
|||||||
label: 'Title',
|
label: 'Title',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
width: 'flex-1',
|
width: 'flex-1',
|
||||||
minWidth: 'min-w-[200px]',
|
minWidth: 'min-w-0',
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
label: 'Status',
|
|
||||||
sortable: true,
|
|
||||||
width: 'w-[140px]',
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'category',
|
|
||||||
label: 'Category',
|
|
||||||
sortable: true,
|
|
||||||
width: 'w-[120px]',
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'priority',
|
|
||||||
label: 'Priority',
|
|
||||||
sortable: true,
|
|
||||||
width: 'w-[100px]',
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'createdAt',
|
|
||||||
label: 'Created',
|
|
||||||
sortable: true,
|
|
||||||
width: 'w-[110px]',
|
|
||||||
align: 'left',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'updatedAt',
|
|
||||||
label: 'Updated',
|
|
||||||
sortable: true,
|
|
||||||
width: 'w-[110px]',
|
|
||||||
align: 'left',
|
align: 'left',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -92,13 +58,7 @@ export interface ListHeaderProps {
|
|||||||
/**
|
/**
|
||||||
* SortIcon displays the current sort state for a column
|
* SortIcon displays the current sort state for a column
|
||||||
*/
|
*/
|
||||||
function SortIcon({
|
function SortIcon({ column, sortConfig }: { column: SortColumn; sortConfig: SortConfig }) {
|
||||||
column,
|
|
||||||
sortConfig,
|
|
||||||
}: {
|
|
||||||
column: SortColumn;
|
|
||||||
sortConfig: SortConfig;
|
|
||||||
}) {
|
|
||||||
if (sortConfig.column !== column) {
|
if (sortConfig.column !== column) {
|
||||||
// Not sorted by this column - show neutral indicator
|
// Not sorted by this column - show neutral indicator
|
||||||
return (
|
return (
|
||||||
@@ -173,11 +133,7 @@ const SortableColumnHeader = memo(function SortableColumnHeader({
|
|||||||
/**
|
/**
|
||||||
* StaticColumnHeader renders a non-sortable header cell
|
* StaticColumnHeader renders a non-sortable header cell
|
||||||
*/
|
*/
|
||||||
const StaticColumnHeader = memo(function StaticColumnHeader({
|
const StaticColumnHeader = memo(function StaticColumnHeader({ column }: { column: ColumnDef }) {
|
||||||
column,
|
|
||||||
}: {
|
|
||||||
column: ColumnDef;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
|
|||||||
import { AlertCircle, Lock, Hand, Sparkles, FileText } from 'lucide-react';
|
import { AlertCircle, Lock, Hand, Sparkles, FileText } from 'lucide-react';
|
||||||
import type { Feature } from '@/store/app-store';
|
import type { Feature } from '@/store/app-store';
|
||||||
import type { PipelineConfig } from '@automaker/types';
|
import type { PipelineConfig } from '@automaker/types';
|
||||||
import { StatusBadge } from './status-badge';
|
|
||||||
import { RowActions, type RowActionHandlers } from './row-actions';
|
import { RowActions, type RowActionHandlers } from './row-actions';
|
||||||
import { LIST_COLUMNS, getColumnWidth, getColumnAlign } from './list-header';
|
import { getColumnWidth, getColumnAlign } from './list-header';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a date string for display in the table
|
* Format a date string for display in the table
|
||||||
@@ -123,8 +122,10 @@ const IndicatorBadges = memo(function IndicatorBadges({
|
|||||||
isCurrentAutoTask?: boolean;
|
isCurrentAutoTask?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const hasError = feature.error && !isCurrentAutoTask;
|
const hasError = feature.error && !isCurrentAutoTask;
|
||||||
const isBlocked = blockingDependencies.length > 0 && !feature.error && feature.status === 'backlog';
|
const isBlocked =
|
||||||
const showManualVerification = feature.skipTests && !feature.error && feature.status === 'backlog';
|
blockingDependencies.length > 0 && !feature.error && feature.status === 'backlog';
|
||||||
|
const showManualVerification =
|
||||||
|
feature.skipTests && !feature.error && feature.status === 'backlog';
|
||||||
const hasPlan = feature.planSpec?.content;
|
const hasPlan = feature.planSpec?.content;
|
||||||
|
|
||||||
// Check if just finished (within 2 minutes)
|
// Check if just finished (within 2 minutes)
|
||||||
@@ -237,11 +238,7 @@ const IndicatorBadges = memo(function IndicatorBadges({
|
|||||||
/**
|
/**
|
||||||
* PriorityBadge displays the priority indicator in the table
|
* PriorityBadge displays the priority indicator in the table
|
||||||
*/
|
*/
|
||||||
const PriorityBadge = memo(function PriorityBadge({
|
const PriorityBadge = memo(function PriorityBadge({ priority }: { priority: number | undefined }) {
|
||||||
priority,
|
|
||||||
}: {
|
|
||||||
priority: number | undefined;
|
|
||||||
}) {
|
|
||||||
const display = getPriorityDisplay(priority);
|
const display = getPriorityDisplay(priority);
|
||||||
|
|
||||||
if (!display) {
|
if (!display) {
|
||||||
@@ -358,10 +355,7 @@ export const ListRow = memo(function ListRow({
|
|||||||
>
|
>
|
||||||
{/* Checkbox column */}
|
{/* Checkbox column */}
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
<div
|
<div role="cell" className="flex items-center justify-center w-10 px-2 py-3 shrink-0">
|
||||||
role="cell"
|
|
||||||
className="flex items-center justify-center w-10 px-2 py-3 shrink-0"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
@@ -376,7 +370,7 @@ export const ListRow = memo(function ListRow({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Title column */}
|
{/* Title column - full width with margin for actions */}
|
||||||
<div
|
<div
|
||||||
role="cell"
|
role="cell"
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -414,94 +408,16 @@ export const ListRow = memo(function ListRow({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status column */}
|
|
||||||
<div
|
|
||||||
role="cell"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center px-3 py-3',
|
|
||||||
getColumnWidth('status'),
|
|
||||||
getColumnAlign('status')
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<StatusBadge
|
|
||||||
status={feature.status}
|
|
||||||
pipelineConfig={pipelineConfig}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Category column */}
|
|
||||||
<div
|
|
||||||
role="cell"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center px-3 py-3 text-sm text-muted-foreground truncate',
|
|
||||||
getColumnWidth('category'),
|
|
||||||
getColumnAlign('category')
|
|
||||||
)}
|
|
||||||
title={feature.category}
|
|
||||||
>
|
|
||||||
{feature.category || '-'}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Priority column */}
|
|
||||||
<div
|
|
||||||
role="cell"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center px-3 py-3',
|
|
||||||
getColumnWidth('priority'),
|
|
||||||
getColumnAlign('priority')
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<PriorityBadge priority={feature.priority} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Created At column */}
|
|
||||||
<div
|
|
||||||
role="cell"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center px-3 py-3 text-sm text-muted-foreground',
|
|
||||||
getColumnWidth('createdAt'),
|
|
||||||
getColumnAlign('createdAt')
|
|
||||||
)}
|
|
||||||
title={feature.createdAt ? new Date(feature.createdAt).toLocaleString() : undefined}
|
|
||||||
>
|
|
||||||
{formatRelativeDate(feature.createdAt)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Updated At column */}
|
|
||||||
<div
|
|
||||||
role="cell"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center px-3 py-3 text-sm text-muted-foreground',
|
|
||||||
getColumnWidth('updatedAt'),
|
|
||||||
getColumnAlign('updatedAt')
|
|
||||||
)}
|
|
||||||
title={feature.updatedAt ? new Date(feature.updatedAt).toLocaleString() : undefined}
|
|
||||||
>
|
|
||||||
{formatRelativeDate(feature.updatedAt)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions column */}
|
{/* Actions column */}
|
||||||
<div
|
<div role="cell" className="flex items-center justify-end px-3 py-3 w-[80px] shrink-0">
|
||||||
role="cell"
|
<RowActions feature={feature} handlers={handlers} isCurrentAutoTask={isCurrentAutoTask} />
|
||||||
className="flex items-center justify-end px-3 py-3 w-[80px] shrink-0"
|
|
||||||
>
|
|
||||||
<RowActions
|
|
||||||
feature={feature}
|
|
||||||
handlers={handlers}
|
|
||||||
isCurrentAutoTask={isCurrentAutoTask}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wrap with animated border for currently running auto task
|
// Wrap with animated border for currently running auto task
|
||||||
if (isCurrentAutoTask) {
|
if (isCurrentAutoTask) {
|
||||||
return (
|
return <div className="animated-border-wrapper-row">{rowContent}</div>;
|
||||||
<div className="animated-border-wrapper-row">
|
|
||||||
{rowContent}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rowContent;
|
return rowContent;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Wand2,
|
Wand2,
|
||||||
Archive,
|
Archive,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
|
GitFork,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -80,8 +81,10 @@ const MenuItem = memo(function MenuItem({
|
|||||||
default: '',
|
default: '',
|
||||||
destructive: 'text-destructive focus:text-destructive focus:bg-destructive/10',
|
destructive: 'text-destructive focus:text-destructive focus:bg-destructive/10',
|
||||||
primary: 'text-primary focus:text-primary focus:bg-primary/10',
|
primary: 'text-primary focus:text-primary focus:bg-primary/10',
|
||||||
success: 'text-[var(--status-success)] focus:text-[var(--status-success)] focus:bg-[var(--status-success)]/10',
|
success:
|
||||||
warning: 'text-[var(--status-waiting)] focus:text-[var(--status-waiting)] focus:bg-[var(--status-waiting)]/10',
|
'text-[var(--status-success)] focus:text-[var(--status-success)] focus:bg-[var(--status-success)]/10',
|
||||||
|
warning:
|
||||||
|
'text-[var(--status-waiting)] focus:text-[var(--status-waiting)] focus:bg-[var(--status-waiting)]/10',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -193,11 +196,12 @@ function getPrimaryAction(
|
|||||||
* - Keyboard accessible (focus, Enter/Space to open)
|
* - Keyboard accessible (focus, Enter/Space to open)
|
||||||
*
|
*
|
||||||
* Actions by status:
|
* Actions by status:
|
||||||
* - Backlog: Edit, Delete, Make (implement), View Plan
|
* - Backlog: Edit, Delete, Make (implement), View Plan, Spawn Sub-Task
|
||||||
* - In Progress: View Logs, Resume, Approve Plan, Manual Verify
|
* - In Progress: View Logs, Resume, Approve Plan, Manual Verify, Edit, Spawn Sub-Task, Delete
|
||||||
* - Waiting Approval: Refine, Verify, View Logs
|
* - Waiting Approval: Refine, Verify, View Logs, View PR, Edit, Spawn Sub-Task, Delete
|
||||||
* - Verified: View Logs, Complete
|
* - Verified: View Logs, View PR, View Branch, Complete, Edit, Spawn Sub-Task, Delete
|
||||||
* - Running (auto task): View Logs, Force Stop, Approve Plan
|
* - Running (auto task): View Logs, Approve Plan, Edit, Spawn Sub-Task, Force Stop
|
||||||
|
* - Pipeline statuses: View Logs, Edit, Spawn Sub-Task, Delete
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```tsx
|
* ```tsx
|
||||||
@@ -246,12 +250,7 @@ export const RowActions = memo(function RowActions({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn('flex items-center gap-1', className)}
|
||||||
'flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200',
|
|
||||||
'focus-within:opacity-100',
|
|
||||||
open && 'opacity-100',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
data-testid={`row-actions-${feature.id}`}
|
data-testid={`row-actions-${feature.id}`}
|
||||||
>
|
>
|
||||||
@@ -262,10 +261,13 @@ export const RowActions = memo(function RowActions({
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-7 w-7',
|
'h-7 w-7',
|
||||||
primaryAction.variant === 'destructive' && 'hover:bg-destructive/10 hover:text-destructive',
|
primaryAction.variant === 'destructive' &&
|
||||||
|
'hover:bg-destructive/10 hover:text-destructive',
|
||||||
primaryAction.variant === 'primary' && 'hover:bg-primary/10 hover:text-primary',
|
primaryAction.variant === 'primary' && 'hover:bg-primary/10 hover:text-primary',
|
||||||
primaryAction.variant === 'success' && 'hover:bg-[var(--status-success)]/10 hover:text-[var(--status-success)]',
|
primaryAction.variant === 'success' &&
|
||||||
primaryAction.variant === 'warning' && 'hover:bg-[var(--status-waiting)]/10 hover:text-[var(--status-waiting)]'
|
'hover:bg-[var(--status-success)]/10 hover:text-[var(--status-success)]',
|
||||||
|
primaryAction.variant === 'warning' &&
|
||||||
|
'hover:bg-[var(--status-waiting)]/10 hover:text-[var(--status-waiting)]'
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -284,7 +286,7 @@ export const RowActions = memo(function RowActions({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
className="h-7 w-7"
|
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||||
data-testid={`row-actions-trigger-${feature.id}`}
|
data-testid={`row-actions-trigger-${feature.id}`}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="w-4 h-4" />
|
<MoreHorizontal className="w-4 h-4" />
|
||||||
@@ -310,6 +312,14 @@ export const RowActions = memo(function RowActions({
|
|||||||
variant="warning"
|
variant="warning"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<MenuItem icon={Edit} label="Edit" onClick={withClose(handlers.onEdit)} />
|
||||||
|
{handlers.onSpawnTask && (
|
||||||
|
<MenuItem
|
||||||
|
icon={GitFork}
|
||||||
|
label="Spawn Sub-Task"
|
||||||
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{handlers.onForceStop && (
|
{handlers.onForceStop && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -327,17 +337,9 @@ export const RowActions = memo(function RowActions({
|
|||||||
{/* Backlog actions */}
|
{/* Backlog actions */}
|
||||||
{!isCurrentAutoTask && feature.status === 'backlog' && (
|
{!isCurrentAutoTask && feature.status === 'backlog' && (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem icon={Edit} label="Edit" onClick={withClose(handlers.onEdit)} />
|
||||||
icon={Edit}
|
|
||||||
label="Edit"
|
|
||||||
onClick={withClose(handlers.onEdit)}
|
|
||||||
/>
|
|
||||||
{feature.planSpec?.content && handlers.onViewPlan && (
|
{feature.planSpec?.content && handlers.onViewPlan && (
|
||||||
<MenuItem
|
<MenuItem icon={Eye} label="View Plan" onClick={withClose(handlers.onViewPlan)} />
|
||||||
icon={Eye}
|
|
||||||
label="View Plan"
|
|
||||||
onClick={withClose(handlers.onViewPlan)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{handlers.onImplement && (
|
{handlers.onImplement && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -347,6 +349,13 @@ export const RowActions = memo(function RowActions({
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{handlers.onSpawnTask && (
|
||||||
|
<MenuItem
|
||||||
|
icon={GitFork}
|
||||||
|
label="Spawn Sub-Task"
|
||||||
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
@@ -391,11 +400,14 @@ export const RowActions = memo(function RowActions({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<MenuItem
|
<MenuItem icon={Edit} label="Edit" onClick={withClose(handlers.onEdit)} />
|
||||||
icon={Edit}
|
{handlers.onSpawnTask && (
|
||||||
label="Edit"
|
<MenuItem
|
||||||
onClick={withClose(handlers.onEdit)}
|
icon={GitFork}
|
||||||
/>
|
label="Spawn Sub-Task"
|
||||||
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
label="Delete"
|
label="Delete"
|
||||||
@@ -416,11 +428,7 @@ export const RowActions = memo(function RowActions({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{handlers.onFollowUp && (
|
{handlers.onFollowUp && (
|
||||||
<MenuItem
|
<MenuItem icon={Wand2} label="Refine" onClick={withClose(handlers.onFollowUp)} />
|
||||||
icon={Wand2}
|
|
||||||
label="Refine"
|
|
||||||
onClick={withClose(handlers.onFollowUp)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{feature.prUrl && (
|
{feature.prUrl && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -438,11 +446,14 @@ export const RowActions = memo(function RowActions({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<MenuItem
|
<MenuItem icon={Edit} label="Edit" onClick={withClose(handlers.onEdit)} />
|
||||||
icon={Edit}
|
{handlers.onSpawnTask && (
|
||||||
label="Edit"
|
<MenuItem
|
||||||
onClick={withClose(handlers.onEdit)}
|
icon={GitFork}
|
||||||
/>
|
label="Spawn Sub-Task"
|
||||||
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
label="Delete"
|
label="Delete"
|
||||||
@@ -486,11 +497,14 @@ export const RowActions = memo(function RowActions({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<MenuItem
|
<MenuItem icon={Edit} label="Edit" onClick={withClose(handlers.onEdit)} />
|
||||||
icon={Edit}
|
{handlers.onSpawnTask && (
|
||||||
label="Edit"
|
<MenuItem
|
||||||
onClick={withClose(handlers.onEdit)}
|
icon={GitFork}
|
||||||
/>
|
label="Spawn Sub-Task"
|
||||||
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
label="Delete"
|
label="Delete"
|
||||||
@@ -501,30 +515,32 @@ export const RowActions = memo(function RowActions({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pipeline status actions (generic fallback) */}
|
{/* Pipeline status actions (generic fallback) */}
|
||||||
{!isCurrentAutoTask &&
|
{!isCurrentAutoTask && feature.status.startsWith('pipeline_') && (
|
||||||
feature.status.startsWith('pipeline_') && (
|
<>
|
||||||
<>
|
{handlers.onViewOutput && (
|
||||||
{handlers.onViewOutput && (
|
|
||||||
<MenuItem
|
|
||||||
icon={FileText}
|
|
||||||
label="View Logs"
|
|
||||||
onClick={withClose(handlers.onViewOutput)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Edit}
|
icon={FileText}
|
||||||
label="Edit"
|
label="View Logs"
|
||||||
onClick={withClose(handlers.onEdit)}
|
onClick={withClose(handlers.onViewOutput)}
|
||||||
/>
|
/>
|
||||||
<DropdownMenuSeparator />
|
)}
|
||||||
|
<MenuItem icon={Edit} label="Edit" onClick={withClose(handlers.onEdit)} />
|
||||||
|
{handlers.onSpawnTask && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Trash2}
|
icon={GitFork}
|
||||||
label="Delete"
|
label="Spawn Sub-Task"
|
||||||
onClick={withClose(handlers.onDelete)}
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
variant="destructive"
|
|
||||||
/>
|
/>
|
||||||
</>
|
)}
|
||||||
)}
|
<DropdownMenuSeparator />
|
||||||
|
<MenuItem
|
||||||
|
icon={Trash2}
|
||||||
|
label="Delete"
|
||||||
|
onClick={withClose(handlers.onDelete)}
|
||||||
|
variant="destructive"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -304,22 +304,22 @@ export function AgentOutputModal({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="w-[60vw] max-w-[60vw] max-h-[80vh] flex flex-col"
|
className="w-full h-full max-w-full max-h-full sm:w-[60vw] sm:max-w-[60vw] sm:max-h-[80vh] sm:h-auto sm:rounded-xl rounded-none flex flex-col"
|
||||||
data-testid="agent-output-modal"
|
data-testid="agent-output-modal"
|
||||||
>
|
>
|
||||||
<DialogHeader className="shrink-0">
|
<DialogHeader className="shrink-0">
|
||||||
<div className="flex items-center justify-between pr-8">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 pr-8">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
{featureStatus !== 'verified' && featureStatus !== 'waiting_approval' && (
|
{featureStatus !== 'verified' && featureStatus !== 'waiting_approval' && (
|
||||||
<Loader2 className="w-5 h-5 text-primary animate-spin" />
|
<Loader2 className="w-5 h-5 text-primary animate-spin" />
|
||||||
)}
|
)}
|
||||||
Agent Output
|
Agent Output
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
<div className="flex items-center gap-1 bg-muted rounded-lg p-1 overflow-x-auto">
|
||||||
{summary && (
|
{summary && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('summary')}
|
onClick={() => setViewMode('summary')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all whitespace-nowrap ${
|
||||||
effectiveViewMode === 'summary'
|
effectiveViewMode === 'summary'
|
||||||
? 'bg-primary/20 text-primary shadow-sm'
|
? 'bg-primary/20 text-primary shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||||
@@ -332,7 +332,7 @@ export function AgentOutputModal({
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('parsed')}
|
onClick={() => setViewMode('parsed')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all whitespace-nowrap ${
|
||||||
effectiveViewMode === 'parsed'
|
effectiveViewMode === 'parsed'
|
||||||
? 'bg-primary/20 text-primary shadow-sm'
|
? 'bg-primary/20 text-primary shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||||
@@ -344,7 +344,7 @@ export function AgentOutputModal({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('changes')}
|
onClick={() => setViewMode('changes')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all whitespace-nowrap ${
|
||||||
effectiveViewMode === 'changes'
|
effectiveViewMode === 'changes'
|
||||||
? 'bg-primary/20 text-primary shadow-sm'
|
? 'bg-primary/20 text-primary shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||||
@@ -356,7 +356,7 @@ export function AgentOutputModal({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('raw')}
|
onClick={() => setViewMode('raw')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all whitespace-nowrap ${
|
||||||
effectiveViewMode === 'raw'
|
effectiveViewMode === 'raw'
|
||||||
? 'bg-primary/20 text-primary shadow-sm'
|
? 'bg-primary/20 text-primary shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||||
@@ -384,7 +384,7 @@ export function AgentOutputModal({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{effectiveViewMode === 'changes' ? (
|
{effectiveViewMode === 'changes' ? (
|
||||||
<div className="flex-1 min-h-[400px] max-h-[60vh] overflow-y-auto scrollbar-visible">
|
<div className="flex-1 min-h-0 sm:min-h-[400px] sm:max-h-[60vh] overflow-y-auto scrollbar-visible">
|
||||||
{projectPath ? (
|
{projectPath ? (
|
||||||
<GitDiffPanel
|
<GitDiffPanel
|
||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
@@ -401,7 +401,7 @@ export function AgentOutputModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : effectiveViewMode === 'summary' && summary ? (
|
) : effectiveViewMode === 'summary' && summary ? (
|
||||||
<div className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 min-h-[400px] max-h-[60vh] scrollbar-visible">
|
<div className="flex-1 min-h-0 sm:min-h-[400px] sm:max-h-[60vh] overflow-y-auto bg-card border border-border/50 rounded-lg p-4 scrollbar-visible">
|
||||||
<Markdown>{summary}</Markdown>
|
<Markdown>{summary}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -409,7 +409,7 @@ export function AgentOutputModal({
|
|||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[400px] max-h-[60vh] scrollbar-visible"
|
className="flex-1 min-h-0 sm:min-h-[400px] sm:max-h-[60vh] overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs scrollbar-visible"
|
||||||
>
|
>
|
||||||
{isLoading && !output ? (
|
{isLoading && !output ? (
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||||
|
|||||||
157
apps/ui/src/components/views/board-view/header-mobile-menu.tsx
Normal file
157
apps/ui/src/components/views/board-view/header-mobile-menu.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { Menu, Bot, Wand2, Settings2, GitBranch, Zap } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface HeaderMobileMenuProps {
|
||||||
|
// Worktree panel visibility
|
||||||
|
isWorktreePanelVisible: boolean;
|
||||||
|
onWorktreePanelToggle: (visible: boolean) => void;
|
||||||
|
// Concurrency control
|
||||||
|
maxConcurrency: number;
|
||||||
|
runningAgentsCount: number;
|
||||||
|
onConcurrencyChange: (value: number) => void;
|
||||||
|
// Auto mode
|
||||||
|
isAutoModeRunning: boolean;
|
||||||
|
onAutoModeToggle: (enabled: boolean) => void;
|
||||||
|
onOpenAutoModeSettings: () => void;
|
||||||
|
// Plan button
|
||||||
|
onOpenPlanDialog: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeaderMobileMenu({
|
||||||
|
isWorktreePanelVisible,
|
||||||
|
onWorktreePanelToggle,
|
||||||
|
maxConcurrency,
|
||||||
|
runningAgentsCount,
|
||||||
|
onConcurrencyChange,
|
||||||
|
isAutoModeRunning,
|
||||||
|
onAutoModeToggle,
|
||||||
|
onOpenAutoModeSettings,
|
||||||
|
onOpenPlanDialog,
|
||||||
|
}: HeaderMobileMenuProps) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
data-testid="header-mobile-menu-trigger"
|
||||||
|
>
|
||||||
|
<Menu className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-64">
|
||||||
|
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
||||||
|
Controls
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
{/* Auto Mode Toggle */}
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between px-2 py-2 cursor-pointer hover:bg-accent rounded-sm"
|
||||||
|
onClick={() => onAutoModeToggle(!isAutoModeRunning)}
|
||||||
|
data-testid="mobile-auto-mode-toggle-container"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Zap
|
||||||
|
className={cn(
|
||||||
|
'w-4 h-4',
|
||||||
|
isAutoModeRunning ? 'text-yellow-500' : 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">Auto Mode</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="mobile-auto-mode-toggle"
|
||||||
|
checked={isAutoModeRunning}
|
||||||
|
onCheckedChange={onAutoModeToggle}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
data-testid="mobile-auto-mode-toggle"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onOpenAutoModeSettings();
|
||||||
|
}}
|
||||||
|
className="p-1 rounded hover:bg-accent/50 transition-colors"
|
||||||
|
title="Auto Mode Settings"
|
||||||
|
data-testid="mobile-auto-mode-settings-button"
|
||||||
|
>
|
||||||
|
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
{/* Worktrees Toggle */}
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between px-2 py-2 cursor-pointer hover:bg-accent rounded-sm"
|
||||||
|
onClick={() => onWorktreePanelToggle(!isWorktreePanelVisible)}
|
||||||
|
data-testid="mobile-worktrees-toggle-container"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">Worktrees</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="mobile-worktrees-toggle"
|
||||||
|
checked={isWorktreePanelVisible}
|
||||||
|
onCheckedChange={onWorktreePanelToggle}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
data-testid="mobile-worktrees-toggle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
{/* Concurrency Control */}
|
||||||
|
<div className="px-2 py-2" data-testid="mobile-concurrency-control">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Bot className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">Max Agents</span>
|
||||||
|
<span
|
||||||
|
className="text-sm text-muted-foreground ml-auto"
|
||||||
|
data-testid="mobile-concurrency-value"
|
||||||
|
>
|
||||||
|
{runningAgentsCount}/{maxConcurrency}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
value={[maxConcurrency]}
|
||||||
|
onValueChange={(value) => onConcurrencyChange(value[0])}
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
step={1}
|
||||||
|
className="w-full"
|
||||||
|
data-testid="mobile-concurrency-slider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
{/* Plan Button */}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={onOpenPlanDialog}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
data-testid="mobile-plan-button"
|
||||||
|
>
|
||||||
|
<Wand2 className="w-4 h-4" />
|
||||||
|
<span>Plan</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export { BranchSwitchDropdown } from './branch-switch-dropdown';
|
export { BranchSwitchDropdown } from './branch-switch-dropdown';
|
||||||
export { WorktreeActionsDropdown } from './worktree-actions-dropdown';
|
export { WorktreeActionsDropdown } from './worktree-actions-dropdown';
|
||||||
|
export { WorktreeMobileDropdown } from './worktree-mobile-dropdown';
|
||||||
export { WorktreeTab } from './worktree-tab';
|
export { WorktreeTab } from './worktree-tab';
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ interface WorktreeActionsDropdownProps {
|
|||||||
isDevServerRunning: boolean;
|
isDevServerRunning: boolean;
|
||||||
devServerInfo?: DevServerInfo;
|
devServerInfo?: DevServerInfo;
|
||||||
gitRepoStatus: GitRepoStatus;
|
gitRepoStatus: GitRepoStatus;
|
||||||
|
/** When true, renders as a standalone button (not attached to another element) */
|
||||||
|
standalone?: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onPull: (worktree: WorktreeInfo) => void;
|
onPull: (worktree: WorktreeInfo) => void;
|
||||||
onPush: (worktree: WorktreeInfo) => void;
|
onPush: (worktree: WorktreeInfo) => void;
|
||||||
@@ -71,6 +73,7 @@ export function WorktreeActionsDropdown({
|
|||||||
isDevServerRunning,
|
isDevServerRunning,
|
||||||
devServerInfo,
|
devServerInfo,
|
||||||
gitRepoStatus,
|
gitRepoStatus,
|
||||||
|
standalone = false,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onPull,
|
onPull,
|
||||||
onPush,
|
onPush,
|
||||||
@@ -115,15 +118,17 @@ export function WorktreeActionsDropdown({
|
|||||||
<DropdownMenu onOpenChange={onOpenChange}>
|
<DropdownMenu onOpenChange={onOpenChange}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant={isSelected ? 'default' : 'outline'}
|
variant={standalone ? 'outline' : isSelected ? 'default' : 'outline'}
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-7 w-7 p-0 rounded-l-none',
|
'h-7 w-7 p-0',
|
||||||
isSelected && 'bg-primary text-primary-foreground',
|
!standalone && 'rounded-l-none',
|
||||||
!isSelected && 'bg-secondary/50 hover:bg-secondary'
|
standalone && 'h-8 w-8 shrink-0',
|
||||||
|
!standalone && isSelected && 'bg-primary text-primary-foreground',
|
||||||
|
!standalone && !isSelected && 'bg-secondary/50 hover:bg-secondary'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="w-3 h-3" />
|
<MoreHorizontal className="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="w-56">
|
<DropdownMenuContent align="start" className="w-56">
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { GitBranch, ChevronDown, Loader2, CircleDot, Check } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { WorktreeInfo } from '../types';
|
||||||
|
|
||||||
|
interface WorktreeMobileDropdownProps {
|
||||||
|
worktrees: WorktreeInfo[];
|
||||||
|
isWorktreeSelected: (worktree: WorktreeInfo) => boolean;
|
||||||
|
hasRunningFeatures: (worktree: WorktreeInfo) => boolean;
|
||||||
|
isActivating: boolean;
|
||||||
|
branchCardCounts?: Record<string, number>;
|
||||||
|
onSelectWorktree: (worktree: WorktreeInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorktreeMobileDropdown({
|
||||||
|
worktrees,
|
||||||
|
isWorktreeSelected,
|
||||||
|
hasRunningFeatures,
|
||||||
|
isActivating,
|
||||||
|
branchCardCounts,
|
||||||
|
onSelectWorktree,
|
||||||
|
}: WorktreeMobileDropdownProps) {
|
||||||
|
// Find the currently selected worktree to display in the trigger
|
||||||
|
const selectedWorktree = worktrees.find((w) => isWorktreeSelected(w));
|
||||||
|
const displayBranch = selectedWorktree?.branch || 'Select branch';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-3 gap-2 font-mono text-xs bg-secondary/50 hover:bg-secondary flex-1 min-w-0"
|
||||||
|
disabled={isActivating}
|
||||||
|
>
|
||||||
|
<GitBranch className="w-3.5 h-3.5 shrink-0" />
|
||||||
|
<span className="truncate">{displayBranch}</span>
|
||||||
|
{isActivating ? (
|
||||||
|
<Loader2 className="w-3 h-3 animate-spin shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-3 h-3 shrink-0 ml-auto" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="w-64 max-h-80 overflow-y-auto">
|
||||||
|
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
||||||
|
Branches & Worktrees
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{worktrees.map((worktree) => {
|
||||||
|
const isSelected = isWorktreeSelected(worktree);
|
||||||
|
const isRunning = hasRunningFeatures(worktree);
|
||||||
|
const cardCount = branchCardCounts?.[worktree.branch];
|
||||||
|
const hasChanges = worktree.hasChanges;
|
||||||
|
const changedFilesCount = worktree.changedFilesCount;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={worktree.path}
|
||||||
|
onClick={() => onSelectWorktree(worktree)}
|
||||||
|
className={cn('flex items-center gap-2 cursor-pointer', isSelected && 'bg-accent')}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
|
{isSelected ? (
|
||||||
|
<Check className="w-3.5 h-3.5 shrink-0 text-primary" />
|
||||||
|
) : (
|
||||||
|
<div className="w-3.5 h-3.5 shrink-0" />
|
||||||
|
)}
|
||||||
|
{isRunning && <Loader2 className="w-3 h-3 animate-spin shrink-0" />}
|
||||||
|
<span className={cn('font-mono text-xs truncate', isSelected && 'font-medium')}>
|
||||||
|
{worktree.branch}
|
||||||
|
</span>
|
||||||
|
{worktree.isMain && (
|
||||||
|
<span className="text-[10px] px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">
|
||||||
|
main
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
|
{cardCount !== undefined && cardCount > 0 && (
|
||||||
|
<span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border">
|
||||||
|
{cardCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{hasChanges && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded border',
|
||||||
|
'bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30'
|
||||||
|
)}
|
||||||
|
title={`${changedFilesCount ?? 'Some'} uncommitted file${changedFilesCount !== 1 ? 's' : ''}`}
|
||||||
|
>
|
||||||
|
<CircleDot className="w-2.5 h-2.5 mr-0.5" />
|
||||||
|
{changedFilesCount ?? '!'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { GitBranch, Plus, RefreshCw } from 'lucide-react';
|
|||||||
import { cn, pathsEqual } from '@/lib/utils';
|
import { cn, pathsEqual } from '@/lib/utils';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||||
|
import { useIsMobile } from '@/hooks/use-media-query';
|
||||||
import type { WorktreePanelProps, WorktreeInfo } from './types';
|
import type { WorktreePanelProps, WorktreeInfo } from './types';
|
||||||
import {
|
import {
|
||||||
useWorktrees,
|
useWorktrees,
|
||||||
@@ -12,7 +13,7 @@ import {
|
|||||||
useWorktreeActions,
|
useWorktreeActions,
|
||||||
useRunningFeatures,
|
useRunningFeatures,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
import { WorktreeTab } from './components';
|
import { WorktreeTab, WorktreeMobileDropdown, WorktreeActionsDropdown } from './components';
|
||||||
|
|
||||||
export function WorktreePanel({
|
export function WorktreePanel({
|
||||||
projectPath,
|
projectPath,
|
||||||
@@ -103,6 +104,8 @@ export function WorktreePanel({
|
|||||||
checkInitScript();
|
checkInitScript();
|
||||||
}, [projectPath]);
|
}, [projectPath]);
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
// Periodic interval check (5 seconds) to detect branch changes on disk
|
// Periodic interval check (5 seconds) to detect branch changes on disk
|
||||||
// Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders
|
// Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -167,6 +170,85 @@ export function WorktreePanel({
|
|||||||
const mainWorktree = worktrees.find((w) => w.isMain);
|
const mainWorktree = worktrees.find((w) => w.isMain);
|
||||||
const nonMainWorktrees = worktrees.filter((w) => !w.isMain);
|
const nonMainWorktrees = worktrees.filter((w) => !w.isMain);
|
||||||
|
|
||||||
|
// Mobile view: single dropdown for all worktrees
|
||||||
|
if (isMobile) {
|
||||||
|
// Find the currently selected worktree for the actions menu
|
||||||
|
const selectedWorktree = worktrees.find((w) => isWorktreeSelected(w)) || mainWorktree;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-border bg-glass/50 backdrop-blur-sm">
|
||||||
|
<WorktreeMobileDropdown
|
||||||
|
worktrees={worktrees}
|
||||||
|
isWorktreeSelected={isWorktreeSelected}
|
||||||
|
hasRunningFeatures={hasRunningFeatures}
|
||||||
|
isActivating={isActivating}
|
||||||
|
branchCardCounts={branchCardCounts}
|
||||||
|
onSelectWorktree={handleSelectWorktree}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Actions menu for the selected worktree */}
|
||||||
|
{selectedWorktree && (
|
||||||
|
<WorktreeActionsDropdown
|
||||||
|
worktree={selectedWorktree}
|
||||||
|
isSelected={true}
|
||||||
|
standalone={true}
|
||||||
|
aheadCount={aheadCount}
|
||||||
|
behindCount={behindCount}
|
||||||
|
isPulling={isPulling}
|
||||||
|
isPushing={isPushing}
|
||||||
|
isStartingDevServer={isStartingDevServer}
|
||||||
|
isDevServerRunning={isDevServerRunning(selectedWorktree)}
|
||||||
|
devServerInfo={getDevServerInfo(selectedWorktree)}
|
||||||
|
gitRepoStatus={gitRepoStatus}
|
||||||
|
onOpenChange={handleActionsDropdownOpenChange(selectedWorktree)}
|
||||||
|
onPull={handlePull}
|
||||||
|
onPush={handlePush}
|
||||||
|
onOpenInEditor={handleOpenInEditor}
|
||||||
|
onCommit={onCommit}
|
||||||
|
onCreatePR={onCreatePR}
|
||||||
|
onAddressPRComments={onAddressPRComments}
|
||||||
|
onResolveConflicts={onResolveConflicts}
|
||||||
|
onDeleteWorktree={onDeleteWorktree}
|
||||||
|
onStartDevServer={handleStartDevServer}
|
||||||
|
onStopDevServer={handleStopDevServer}
|
||||||
|
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{useWorktreesEnabled && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground shrink-0"
|
||||||
|
onClick={onCreateWorktree}
|
||||||
|
title="Create new worktree"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground shrink-0"
|
||||||
|
onClick={async () => {
|
||||||
|
const removedWorktrees = await fetchWorktrees();
|
||||||
|
if (removedWorktrees && removedWorktrees.length > 0 && onRemovedWorktrees) {
|
||||||
|
onRemovedWorktrees(removedWorktrees);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
title="Refresh worktrees"
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn('w-3.5 h-3.5', isLoading && 'animate-spin')} />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desktop view: full tabs layout
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-glass/50 backdrop-blur-sm">
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-glass/50 backdrop-blur-sm">
|
||||||
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
useNodesState,
|
useNodesState,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
ReactFlowProvider,
|
ReactFlowProvider,
|
||||||
|
useReactFlow,
|
||||||
SelectionMode,
|
SelectionMode,
|
||||||
ConnectionMode,
|
ConnectionMode,
|
||||||
Node,
|
Node,
|
||||||
@@ -244,6 +245,61 @@ function GraphCanvasInner({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get fitView from React Flow for orientation change handling
|
||||||
|
const { fitView } = useReactFlow();
|
||||||
|
|
||||||
|
// Handle orientation changes on mobile devices
|
||||||
|
// When rotating from landscape to portrait, the view may incorrectly zoom in
|
||||||
|
// This effect listens for orientation changes and calls fitView to correct the viewport
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// Track the previous orientation to detect changes
|
||||||
|
let previousWidth = window.innerWidth;
|
||||||
|
let previousHeight = window.innerHeight;
|
||||||
|
|
||||||
|
const handleOrientationChange = () => {
|
||||||
|
// Small delay to allow the browser to complete the orientation change
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView({ padding: 0.2, duration: 300 });
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
const currentWidth = window.innerWidth;
|
||||||
|
const currentHeight = window.innerHeight;
|
||||||
|
|
||||||
|
// Detect orientation change by checking if width and height swapped significantly
|
||||||
|
// This happens when device rotates between portrait and landscape
|
||||||
|
const widthDiff = Math.abs(currentWidth - previousHeight);
|
||||||
|
const heightDiff = Math.abs(currentHeight - previousWidth);
|
||||||
|
|
||||||
|
// If the dimensions are close to being swapped (within 100px tolerance)
|
||||||
|
// it's likely an orientation change
|
||||||
|
const isOrientationChange = widthDiff < 100 && heightDiff < 100;
|
||||||
|
|
||||||
|
if (isOrientationChange) {
|
||||||
|
// Delay fitView to allow browser to complete the layout
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView({ padding: 0.2, duration: 300 });
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
previousWidth = currentWidth;
|
||||||
|
previousHeight = currentHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for orientation change event (mobile specific)
|
||||||
|
window.addEventListener('orientationchange', handleOrientationChange);
|
||||||
|
// Also listen for resize as a fallback (some browsers don't fire orientationchange)
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('orientationchange', handleOrientationChange);
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, [fitView]);
|
||||||
|
|
||||||
// MiniMap node color based on status
|
// MiniMap node color based on status
|
||||||
const minimapNodeColor = useCallback((node: Node<TaskNodeData>) => {
|
const minimapNodeColor = useCallback((node: Node<TaskNodeData>) => {
|
||||||
const data = node.data as TaskNodeData | undefined;
|
const data = node.data as TaskNodeData | undefined;
|
||||||
|
|||||||
@@ -386,6 +386,18 @@ export interface GlobalSettings {
|
|||||||
/** How to pass notification data to the command */
|
/** How to pass notification data to the command */
|
||||||
notificationCommandMode: 'args' | 'stdin' | 'env';
|
notificationCommandMode: 'args' | 'stdin' | 'env';
|
||||||
|
|
||||||
|
// ntfy.sh Notifications
|
||||||
|
/** Enable ntfy.sh notifications for task completion */
|
||||||
|
ntfyEnabled: boolean;
|
||||||
|
/** ntfy.sh server URL (default: https://ntfy.sh) */
|
||||||
|
ntfyServerUrl: string;
|
||||||
|
/** ntfy.sh topic to publish notifications to */
|
||||||
|
ntfyTopic: string;
|
||||||
|
/** Optional authentication token for private ntfy servers */
|
||||||
|
ntfyAuthToken?: string;
|
||||||
|
/** Priority for ntfy notifications (1-5, default: 3) */
|
||||||
|
ntfyPriority: 1 | 2 | 3 | 4 | 5;
|
||||||
|
|
||||||
// AI Model Selection (per-phase configuration)
|
// AI Model Selection (per-phase configuration)
|
||||||
/** Phase-specific AI model configuration */
|
/** Phase-specific AI model configuration */
|
||||||
phaseModels: PhaseModelConfig;
|
phaseModels: PhaseModelConfig;
|
||||||
@@ -707,6 +719,11 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
|||||||
muteDoneSound: false,
|
muteDoneSound: false,
|
||||||
notificationCommand: '',
|
notificationCommand: '',
|
||||||
notificationCommandMode: 'args',
|
notificationCommandMode: 'args',
|
||||||
|
ntfyEnabled: false,
|
||||||
|
ntfyServerUrl: 'https://ntfy.sh',
|
||||||
|
ntfyTopic: '',
|
||||||
|
ntfyAuthToken: undefined,
|
||||||
|
ntfyPriority: 3,
|
||||||
phaseModels: DEFAULT_PHASE_MODELS,
|
phaseModels: DEFAULT_PHASE_MODELS,
|
||||||
enhancementModel: 'sonnet',
|
enhancementModel: 'sonnet',
|
||||||
validationModel: 'opus',
|
validationModel: 'opus',
|
||||||
|
|||||||
Reference in New Issue
Block a user