Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions client/src/components/cos/TaskAddForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const readTaskDescriptionDraft = (defaultApp) => {
};
};

export default function TaskAddForm({ providers, apps, onTaskAdded, compact = false, defaultExpanded = false, defaultApp = '' }) {
export default function TaskAddForm({ providers, providersLoaded = true, apps, onTaskAdded, compact = false, defaultExpanded = false, defaultApp = '' }) {
const [initialDraft] = useState(() => readTaskDescriptionDraft(defaultApp));
const [newTask, setNewTask] = useState(() => {
return {
Expand Down Expand Up @@ -180,12 +180,16 @@ export default function TaskAddForm({ providers, apps, onTaskAdded, compact = fa

// If the pinned provider isn't a valid coding option (e.g. a saved template
// pinned an `api` provider that's now filtered out of the dropdown), reset to
// "Auto" so the visible select and the submitted value can't diverge.
// "Auto" so the visible select and the submitted value can't diverge. Gated on
// `providersLoaded`: mid-fetch, `enabledProviders` is always empty, so without
// the gate this would wipe out a legitimately pinned provider (a draft/template
// restored before the list has arrived) before it ever gets a chance to match.
useEffect(() => {
if (!providersLoaded) return;
if (newTask.provider && !enabledProviders.some(p => p.id === newTask.provider)) {
setNewTask(t => ({ ...t, provider: '', model: '', effort: '', temperature: '', thinking: '' }));
}
}, [enabledProviders, newTask.provider]);
}, [enabledProviders, newTask.provider, providersLoaded]);

// Check if selected app has JIRA configured
const selectedApp = useMemo(() =>
Expand Down Expand Up @@ -948,9 +952,12 @@ export default function TaskAddForm({ providers, apps, onTaskAdded, compact = fa
value={newTask.provider}
onChange={e => setNewTask(t => ({ ...t, provider: e.target.value, model: '', effort: '', temperature: '', thinking: '' }))}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded-lg text-white text-sm min-h-[44px]"
disabled={!providersLoaded}
>
<option value="">Auto (default)</option>
{enabledProviders.map(p => (
{providersLoaded
? <option value="">Auto (default)</option>
: <option value="">Loading providers…</option>}
{providersLoaded && enabledProviders.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
Expand Down
4 changes: 3 additions & 1 deletion client/src/components/cos/tabs/AgentsTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const needsAgentFeedback = (agent) => {
return !isSystemAgent && isManualUserAgent && !agent.feedback?.rating;
};

export default function AgentsTab({ agents, onRefresh, liveOutputs, providers, apps }) {
export default function AgentsTab({ agents, onRefresh, liveOutputs, providers, providersLoaded, apps }) {
const [searchParams, setSearchParams] = useSearchParams();
const [resumingAgent, setResumingAgent] = useState(null);
const [relaunchingAgent, setRelaunchingAgent] = useState(null);
Expand Down Expand Up @@ -435,6 +435,7 @@ export default function AgentsTab({ agents, onRefresh, liveOutputs, providers, a
<RelaunchAgentModal
agent={relaunchingAgent}
providers={providers}
providersLoaded={providersLoaded}
apps={apps}
onDone={onRefresh}
onClose={() => setRelaunchingAgent(null)}
Expand All @@ -447,6 +448,7 @@ export default function AgentsTab({ agents, onRefresh, liveOutputs, providers, a
agent={resumingAgent}
taskType={resumingAgent.taskId?.startsWith('sys-') || resumingAgent.metadata?.taskType === 'internal' ? 'internal' : 'user'}
providers={providers}
providersLoaded={providersLoaded}
apps={apps}
onSubmit={handleResumeSubmit}
onClose={() => setResumingAgent(null)}
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/cos/tabs/RelaunchAgentModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const RELAUNCH_MESSAGES = {
* because it is mounted from two places (the agent card and the in-progress task
* card) and the server's mode enum should have exactly one client reader.
*/
export default function RelaunchAgentModal({ agent, providers, apps, onDone, onClose }) {
export default function RelaunchAgentModal({ agent, providers, providersLoaded = true, apps, onDone, onClose }) {
const currentProvider = agent?.metadata?.providerId || agent?.metadata?.provider || '';
const taskDescription = agent?.metadata?.taskDescription || agent?.taskId || 'Current task';

Expand Down Expand Up @@ -163,6 +163,7 @@ export default function RelaunchAgentModal({ agent, providers, apps, onDone, onC
emptyModelOption="Default model"
alwaysShowModel
highlightToolUse
loading={!providersLoaded}
/>

<FormField label="Additional Instructions (optional)">
Expand Down
15 changes: 10 additions & 5 deletions client/src/components/cos/tabs/ResumeAgentModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FormField } from '../../ui/FormField';
import EffortSelect from '../EffortSelect';
import { effectiveModelFor, effortAwareModelOptions, effortSurvivingModel, seedModelEffort } from '../../../utils/providers';

export default function ResumeAgentModal({ agent, taskType = 'user', providers, apps, onSubmit, onClose }) {
export default function ResumeAgentModal({ agent, taskType = 'user', providers, providersLoaded = true, apps, onSubmit, onClose }) {
// A paused agent resumes IN PLACE: its own task is requeued on the worktree its
// run left behind. Everything else (a completed/failed run, whose task is long
// settled) can only be continued by queueing a new task.
Expand Down Expand Up @@ -232,9 +232,14 @@ export default function ResumeAgentModal({ agent, taskType = 'user', providers,
value={formData.provider}
onChange={e => setFormData({ ...formData, provider: e.target.value, model: '', effort: '' })}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded-lg text-white text-sm focus:border-port-accent focus:outline-hidden"
disabled={!providersLoaded}
>
<option value="">Auto (default)</option>
{providers?.filter(p => p.enabled).map(p => (
{/* Mid-fetch, `providers` is empty — say so instead of rendering a
picker whose only option looks like a broken control. */}
{providersLoaded
? <option value="">Auto (default)</option>
: <option value="">Loading providers…</option>}
{providersLoaded && providers?.filter(p => p.enabled).map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
Expand All @@ -250,9 +255,9 @@ export default function ResumeAgentModal({ agent, taskType = 'user', providers,
effort: effortSurvivingModel(selectedProvider, e.target.value, d.effort),
}))}
className="w-full px-3 py-2 bg-port-bg border border-port-border rounded-lg text-white text-sm focus:border-port-accent focus:outline-hidden"
disabled={!formData.provider}
disabled={!providersLoaded || !formData.provider}
>
<option value="">{formData.provider ? 'Select model...' : 'Select provider first'}</option>
<option value="">{!providersLoaded ? 'Loading providers…' : formData.provider ? 'Select model...' : 'Select provider first'}</option>
{availableModels.map(m => (
<option key={m} value={m}>{m.replace('claude-', '').replace(/-\d+$/, '')}</option>
))}
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/cos/tabs/SortableTaskItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useSortable } from '@dnd-kit/sortable';
import { dndTransformToCss } from '../../../lib/dndTransform';
import TaskItem from './TaskItem';

export default function SortableTaskItem({ task, selected = false, onRefresh, providers, durations, apps, instances }) {
export default function SortableTaskItem({ task, selected = false, onRefresh, providers, providersLoaded, durations, apps, instances }) {
const [isEditing, setIsEditing] = useState(false);
const {
attributes,
Expand All @@ -28,6 +28,7 @@ export default function SortableTaskItem({ task, selected = false, onRefresh, pr
selected={selected}
onRefresh={onRefresh}
providers={providers}
providersLoaded={providersLoaded}
durations={durations}
apps={apps}
instances={instances}
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/cos/tabs/TaskItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ function getSuccessRateStyle(rate) {
return { bg: 'bg-port-error/15', text: 'text-port-error', label: 'low' };
}

export default function TaskItem({ task, agent = null, isSystem, spawning = false, selected = false, onRefresh, onTaskUnblocked, providers, durations, dragHandleProps, apps, instances = null, onEditingChange }) {
export default function TaskItem({ task, agent = null, isSystem, spawning = false, selected = false, onRefresh, onTaskUnblocked, providers, providersLoaded, durations, dragHandleProps, apps, instances = null, onEditingChange }) {
// System tasks are persisted in COS-TASKS.md. Every task
// mutation must name that source; otherwise the API's user-queue default
// searches TASKS.md and reports the system task as missing.
Expand Down Expand Up @@ -831,6 +831,7 @@ export default function TaskItem({ task, agent = null, isSystem, spawning = fals
<RelaunchAgentModal
agent={agent}
providers={providers}
providersLoaded={providersLoaded}
apps={apps}
onDone={onRefresh}
onClose={() => setRelaunching(false)}
Expand Down
20 changes: 10 additions & 10 deletions client/src/components/cos/tabs/TasksTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function SectionGlyph({ status }) {
return <MicroGlyph variant={spec.variant} state={spec.state} animated={spec.animated} size={13} />;
}

export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, onTaskUnblocked, providers, apps }) {
export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, onTaskUnblocked, providers, providersLoaded, apps }) {
const [searchParams] = useSearchParams();
const [userTasksLocal, setUserTasksLocal] = useState([]);
const [durations, setDurations] = useState(null);
Expand Down Expand Up @@ -231,7 +231,7 @@ export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, o
</div>

{/* Add Task Form */}
<TaskAddForm providers={providers} apps={apps} onTaskAdded={handleTaskAdded} />
<TaskAddForm providers={providers} providersLoaded={providersLoaded} apps={apps} onTaskAdded={handleTaskAdded} />

{/* User Tasks Sections */}
{pendingUserTasksLocal.length === 0 && activeUserTasksLocal.length === 0 && blockedUserTasksLocal.length === 0 && completedUserTasksLocal.length === 0 ? (
Expand Down Expand Up @@ -262,7 +262,7 @@ export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, o
>
<div className="space-y-1.5">
{pendingUserTasksLocal.map(task => (
<SortableTaskItem key={task.id} task={task} selected={isTaskSelected(task, 'user')} onRefresh={onRefresh} providers={providers} durations={durations} apps={apps} instances={assignableInstances} />
<SortableTaskItem key={task.id} task={task} selected={isTaskSelected(task, 'user')} onRefresh={onRefresh} providers={providers} providersLoaded={providersLoaded} durations={durations} apps={apps} instances={assignableInstances} />
))}
</div>
</SortableContext>
Expand All @@ -282,7 +282,7 @@ export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, o
</div>
<div className="p-2 space-y-1.5">
{activeUserTasksLocal.map(task => (
<TaskItem key={task.id} task={task} agent={runningAgentByTaskId.get(task.id)} spawning={isSpawning(task)} selected={isTaskSelected(task, 'user')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} durations={durations} apps={apps} instances={assignableInstances} />
<TaskItem key={task.id} task={task} agent={runningAgentByTaskId.get(task.id)} spawning={isSpawning(task)} selected={isTaskSelected(task, 'user')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} providersLoaded={providersLoaded} durations={durations} apps={apps} instances={assignableInstances} />
))}
</div>
</div>
Expand All @@ -299,7 +299,7 @@ export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, o
</div>
<div className="p-2 space-y-1.5">
{blockedUserTasksLocal.map(task => (
<TaskItem key={task.id} task={task} selected={isTaskSelected(task, 'user')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} durations={durations} apps={apps} instances={assignableInstances} />
<TaskItem key={task.id} task={task} selected={isTaskSelected(task, 'user')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} providersLoaded={providersLoaded} durations={durations} apps={apps} instances={assignableInstances} />
))}
</div>
</div>
Expand All @@ -322,7 +322,7 @@ export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, o
{showCompletedUserTasks && (
<div className="p-2 space-y-1.5">
{completedUserTasksLocal.map(task => (
<TaskItem key={task.id} task={task} selected={isTaskSelected(task, 'user')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} durations={durations} apps={apps} instances={assignableInstances} />
<TaskItem key={task.id} task={task} selected={isTaskSelected(task, 'user')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} providersLoaded={providersLoaded} durations={durations} apps={apps} instances={assignableInstances} />
))}
</div>
)}
Expand Down Expand Up @@ -355,7 +355,7 @@ export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, o
</div>
<div className="p-2 space-y-1.5">
{pendingSystemTasks.map(task => (
<TaskItem key={task.id} task={task} isSystem selected={isTaskSelected(task, 'internal')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} durations={durations} apps={apps} instances={assignableInstances} />
<TaskItem key={task.id} task={task} isSystem selected={isTaskSelected(task, 'internal')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} providersLoaded={providersLoaded} durations={durations} apps={apps} instances={assignableInstances} />
))}
</div>
</div>
Expand All @@ -372,7 +372,7 @@ export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, o
</div>
<div className="p-2 space-y-1.5">
{activeSystemTasks.map(task => (
<TaskItem key={task.id} task={task} isSystem agent={runningAgentByTaskId.get(task.id)} spawning={isSpawning(task)} selected={isTaskSelected(task, 'internal')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} durations={durations} apps={apps} instances={assignableInstances} />
<TaskItem key={task.id} task={task} isSystem agent={runningAgentByTaskId.get(task.id)} spawning={isSpawning(task)} selected={isTaskSelected(task, 'internal')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} providersLoaded={providersLoaded} durations={durations} apps={apps} instances={assignableInstances} />
))}
</div>
</div>
Expand All @@ -389,7 +389,7 @@ export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, o
</div>
<div className="p-2 space-y-1.5">
{blockedSystemTasks.map(task => (
<TaskItem key={task.id} task={task} isSystem selected={isTaskSelected(task, 'internal')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} durations={durations} apps={apps} instances={assignableInstances} />
<TaskItem key={task.id} task={task} isSystem selected={isTaskSelected(task, 'internal')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} providersLoaded={providersLoaded} durations={durations} apps={apps} instances={assignableInstances} />
))}
</div>
</div>
Expand All @@ -412,7 +412,7 @@ export default function TasksTab({ tasks, agents = [], onRefresh, onTaskAdded, o
{showCompletedSystemTasks && (
<div className="p-2 space-y-1.5">
{completedSystemTasks.map(task => (
<TaskItem key={task.id} task={task} isSystem selected={isTaskSelected(task, 'internal')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} durations={durations} apps={apps} instances={assignableInstances} />
<TaskItem key={task.id} task={task} isSystem selected={isTaskSelected(task, 'internal')} onRefresh={onRefresh} onTaskUnblocked={onTaskUnblocked} providers={providers} providersLoaded={providersLoaded} durations={durations} apps={apps} instances={assignableInstances} />
))}
</div>
)}
Expand Down
Loading