gadget/gadget-code/frontend/src/components/WorkspaceModeIndicator.tsx
Rob Colbert 5c56f95cd6 Implement workspace mode switching with validation
- Add isProcessingWorkOrder flag to track Agent work order processing
- Update onRequestWorkspaceMode with mode transition matrix validation
  - Idle → User/Agent: Always allowed
  - User → Agent: Always allowed (file editor checks for future)
  - Agent → User: Only if !isProcessingWorkOrder
  - All other transitions: Rejected with reason
- Extend RequestWorkspaceModeCallback with optional reason parameter
- Update frontend socket client to capture rejection reason
- Update handleWorkspaceModeChange to display rejection reason in toast
- Update WorkspaceModeIndicator to allow mode transitions per matrix
- Fix FilesPanel RW/RO indicator swap bug
- Document mode transition matrix and behavior in workspace-management.md
2026-05-02 18:13:31 -04:00

102 lines
2.9 KiB
TypeScript

import { WorkspaceMode } from '../lib/types';
interface WorkspaceModeIndicatorProps {
mode: WorkspaceMode;
onChange?: (mode: WorkspaceMode) => void;
disabled?: boolean;
}
const MODE_LABELS: Record<WorkspaceMode, string> = {
[WorkspaceMode.Idle]: 'I',
[WorkspaceMode.Syncing]: 'S',
[WorkspaceMode.User]: 'U',
[WorkspaceMode.Agent]: 'A',
};
const MODE_COLORS: Record<WorkspaceMode, string> = {
[WorkspaceMode.Idle]: 'text-gray-500 border-gray-500',
[WorkspaceMode.Syncing]: 'text-yellow-500 border-yellow-500',
[WorkspaceMode.User]: 'text-blue-400 border-blue-400',
[WorkspaceMode.Agent]: 'text-green-500 border-green-500',
};
export default function WorkspaceModeIndicator({
mode,
onChange,
disabled = false,
}: WorkspaceModeIndicatorProps) {
const modes = [
WorkspaceMode.Idle,
WorkspaceMode.Syncing,
WorkspaceMode.User,
WorkspaceMode.Agent,
];
const handleClick = (m: WorkspaceMode) => {
if (disabled || m === WorkspaceMode.Syncing) return;
if (m === mode) return;
switch (mode) {
case WorkspaceMode.Idle:
if (m === WorkspaceMode.User || m === WorkspaceMode.Agent) {
onChange?.(m);
}
break;
case WorkspaceMode.User:
if (m === WorkspaceMode.Agent) {
onChange?.(m);
}
break;
case WorkspaceMode.Agent:
if (m === WorkspaceMode.User) {
onChange?.(m);
}
break;
}
};
return (
<div className="flex items-center gap-1">
{modes.map((m) => {
const isActive = m === mode;
const isClickable =
!disabled && m !== WorkspaceMode.Syncing && m !== mode;
const colorClass = MODE_COLORS[m];
const inactiveClass = isActive
? `border-2 ${colorClass} strobe shadow-[inset_0_1px_3px_rgba(0,0,0,0.5)]`
: `border border-border-default`;
return (
<button
key={m}
onClick={() => handleClick(m)}
disabled={!isClickable}
title={
m === WorkspaceMode.Idle
? 'Idle'
: m === WorkspaceMode.Syncing
? 'Syncing'
: m === WorkspaceMode.User
? 'User Mode'
: 'Agent Mode'
}
className={`
w-7 h-7 flex items-center justify-center
font-mono font-bold text-sm rounded
transition-all duration-150 select-none
${inactiveClass}
${isActive ? `active:text-${colorClass.split('-')[1]}` : 'text-text-muted'}
${isClickable ? 'hover:border-border-highlight hover:text-text-secondary cursor-pointer' : ''}
${!isClickable && !isActive ? 'cursor-default' : ''}
${isActive ? 'translate-y-0.5 shadow-inner' : 'translate-y-0'}
`}
>
{MODE_LABELS[m]}
</button>
);
})}
</div>
);
}