Skip to content
44 changes: 39 additions & 5 deletions app/src/components/settings/panels/AgentAccessPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const AgentAccessPanel = () => {

const [level, setLevel] = useState<AutonomyLevel>('supervised');
const [workspaceOnly, setWorkspaceOnly] = useState(false);
const [requireTaskPlanApproval, setRequireTaskPlanApproval] = useState(true);
const [trustedRoots, setTrustedRoots] = useState<TrustedRoot[]>([]);
// "Always allow" allowlist — populated by the in-chat "Always allow" button;
// shown here read-only with a Remove action (the re-protect path).
Expand Down Expand Up @@ -78,6 +79,7 @@ const AgentAccessPanel = () => {
if (cancelled) return;
setLevel(resp.result.level);
setWorkspaceOnly(resp.result.workspace_only);
setRequireTaskPlanApproval(resp.result.require_task_plan_approval ?? true);
setTrustedRoots(resp.result.trusted_roots ?? []);
setAutoApprove(resp.result.auto_approve ?? []);
} catch (e) {
Expand All @@ -100,6 +102,7 @@ const AgentAccessPanel = () => {
const persist = async (next: {
level: AutonomyLevel;
workspaceOnly: boolean;
requireTaskPlanApproval: boolean;
trustedRoots: TrustedRoot[];
// Only sent when the allowlist itself is being changed. Omitting it leaves
// the server's `auto_approve` untouched (partial patch) — important so a
Expand All @@ -118,6 +121,7 @@ const AgentAccessPanel = () => {
workspace_only: next.workspaceOnly,
trusted_roots: next.trustedRoots,
allow_tool_install: ALLOW_TOOL_INSTALL,
require_task_plan_approval: next.requireTaskPlanApproval,
...(next.autoApprove !== undefined ? { auto_approve: next.autoApprove } : {}),
});
// Only the most recent persist may write UI state back.
Expand All @@ -137,12 +141,17 @@ const AgentAccessPanel = () => {

const selectTier = (next: AutonomyLevel) => {
setLevel(next);
void persist({ level: next, workspaceOnly, trustedRoots });
void persist({ level: next, workspaceOnly, requireTaskPlanApproval, trustedRoots });
};

const toggleWorkspaceOnly = (next: boolean) => {
setWorkspaceOnly(next);
void persist({ level, workspaceOnly: next, trustedRoots });
void persist({ level, workspaceOnly: next, requireTaskPlanApproval, trustedRoots });
};

const toggleTaskPlanApproval = (next: boolean) => {
setRequireTaskPlanApproval(next);
void persist({ level, workspaceOnly, requireTaskPlanApproval: next, trustedRoots });
};

const addRoot = () => {
Expand All @@ -156,19 +165,25 @@ const AgentAccessPanel = () => {
setTrustedRoots(nextRoots);
setNewRootPath('');
setNewRootAccess('read');
void persist({ level, workspaceOnly, trustedRoots: nextRoots });
void persist({ level, workspaceOnly, requireTaskPlanApproval, trustedRoots: nextRoots });
};

const removeRoot = (path: string) => {
const nextRoots = trustedRoots.filter(r => r.path !== path);
setTrustedRoots(nextRoots);
void persist({ level, workspaceOnly, trustedRoots: nextRoots });
void persist({ level, workspaceOnly, requireTaskPlanApproval, trustedRoots: nextRoots });
};

const removeAutoApprove = (tool: string) => {
const nextList = autoApprove.filter(name => name !== tool);
setAutoApprove(nextList);
void persist({ level, workspaceOnly, trustedRoots, autoApprove: nextList });
void persist({
level,
workspaceOnly,
requireTaskPlanApproval,
trustedRoots,
autoApprove: nextList,
});
};

return (
Expand Down Expand Up @@ -248,6 +263,25 @@ const AgentAccessPanel = () => {
</label>
</section>

<section className="space-y-1">
<label className="flex items-start gap-2 cursor-pointer">
<input
type="checkbox"
className="mt-0.5 cursor-pointer"
checked={requireTaskPlanApproval}
onChange={e => toggleTaskPlanApproval(e.target.checked)}
/>
<span>
<span className="text-sm font-medium text-ink">
{t('settings.agentAccess.requireTaskPlanApproval.label')}
</span>
<span className="block text-xs text-ink-soft">
{t('settings.agentAccess.requireTaskPlanApproval.desc')}
</span>
</span>
</label>
</section>
Comment thread
senamakel marked this conversation as resolved.

{/* Granted folders (trusted roots) — extra read/write reach. */}
<section className="space-y-2">
<h2 className="text-sm font-semibold text-ink">
Expand Down
22 changes: 22 additions & 0 deletions app/src/lib/i18n/chunks/en-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ const en4: TranslationMap = {
'conversations.taskKanban.moveLeft': 'Move left',
'conversations.taskKanban.moveRight': 'Move right',
'conversations.taskKanban.title': 'Tasks',
'conversations.taskKanban.approval.default': 'Default',
'conversations.taskKanban.approval.notRequired': 'Not required',
'conversations.taskKanban.approval.notRequiredBadge': 'no approval',
'conversations.taskKanban.approval.required': 'Required',
'conversations.taskKanban.approval.requiredBadge': 'approval',
'conversations.taskKanban.approval.requiredBeforeExecution': 'Required before execution',
'conversations.taskKanban.briefButton': 'Task brief',
'conversations.taskKanban.briefTitle': 'Task brief',
'conversations.taskKanban.closeBrief': 'Close task brief',
'conversations.taskKanban.field.acceptanceCriteria': 'Acceptance criteria',
'conversations.taskKanban.field.allowedTools': 'Allowed tools',
'conversations.taskKanban.field.approval': 'Approval',
'conversations.taskKanban.field.assignedAgent': 'Assigned agent',
'conversations.taskKanban.field.blocker': 'Blocker',
'conversations.taskKanban.field.evidence': 'Evidence',
'conversations.taskKanban.field.notes': 'Notes',
'conversations.taskKanban.field.objective': 'Objective',
'conversations.taskKanban.field.plan': 'Plan',
'conversations.taskKanban.field.status': 'Status',
'conversations.taskKanban.field.title': 'Title',
'conversations.taskKanban.saveChanges': 'Save changes',
'conversations.taskKanban.updateFailed': 'Could not update task; changes were not saved.',
'conversations.toolTimeline.turn': 'turn',
'conversations.toolTimeline.workerThread': 'worker thread',
'daemon.serviceBlockingGate.body': 'Body',
Expand Down
3 changes: 3 additions & 0 deletions app/src/lib/i18n/chunks/en-5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,9 @@ const en5: TranslationMap = {
'settings.agentAccess.confine.label': 'Confine to workspace',
'settings.agentAccess.confine.desc':
'Restrict the agent to the workspace directory (plus any granted folders), whichever access mode is selected. When off, it can reach anywhere your user can — except the always-blocked credential and system directories.',
'settings.agentAccess.requireTaskPlanApproval.label': 'Require task plan approval',
'settings.agentAccess.requireTaskPlanApproval.desc':
'Pause before an assigned agent executes an agent-authored task brief.',
'settings.agentAccess.grantedFolders': 'Granted folders',
'settings.agentAccess.alwaysAllow': 'Always-allowed tools',
'settings.agentAccess.alwaysAllowDesc':
Expand Down
25 changes: 25 additions & 0 deletions app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2425,6 +2425,28 @@ const en: TranslationMap = {
'conversations.taskKanban.moveLeft': 'Move left',
'conversations.taskKanban.moveRight': 'Move right',
'conversations.taskKanban.title': 'Tasks',
'conversations.taskKanban.approval.default': 'Default',
'conversations.taskKanban.approval.notRequired': 'Not required',
'conversations.taskKanban.approval.notRequiredBadge': 'no approval',
'conversations.taskKanban.approval.required': 'Required',
'conversations.taskKanban.approval.requiredBadge': 'approval',
'conversations.taskKanban.approval.requiredBeforeExecution': 'Required before execution',
'conversations.taskKanban.briefButton': 'Task brief',
'conversations.taskKanban.briefTitle': 'Task brief',
'conversations.taskKanban.closeBrief': 'Close task brief',
'conversations.taskKanban.field.acceptanceCriteria': 'Acceptance criteria',
'conversations.taskKanban.field.allowedTools': 'Allowed tools',
'conversations.taskKanban.field.approval': 'Approval',
'conversations.taskKanban.field.assignedAgent': 'Assigned agent',
'conversations.taskKanban.field.blocker': 'Blocker',
'conversations.taskKanban.field.evidence': 'Evidence',
'conversations.taskKanban.field.notes': 'Notes',
'conversations.taskKanban.field.objective': 'Objective',
'conversations.taskKanban.field.plan': 'Plan',
'conversations.taskKanban.field.status': 'Status',
'conversations.taskKanban.field.title': 'Title',
'conversations.taskKanban.saveChanges': 'Save changes',
'conversations.taskKanban.updateFailed': 'Could not update task; changes were not saved.',
'conversations.toolTimeline.turn': 'turn',
'conversations.toolTimeline.workerThread': 'worker thread',
'daemon.serviceBlockingGate.body':
Expand Down Expand Up @@ -3373,6 +3395,9 @@ const en: TranslationMap = {
'settings.agentAccess.confine.label': 'Confine to workspace',
'settings.agentAccess.confine.desc':
'Restrict the agent to the workspace directory (plus any granted folders), whichever access mode is selected. When off, it can reach anywhere your user can — except the always-blocked credential and system directories.',
'settings.agentAccess.requireTaskPlanApproval.label': 'Require task plan approval',
'settings.agentAccess.requireTaskPlanApproval.desc':
'Pause before an assigned agent executes an agent-authored task brief.',
'settings.agentAccess.grantedFolders': 'Granted folders',
'settings.agentAccess.alwaysAllow': 'Always-allowed tools',
'settings.agentAccess.alwaysAllowDesc':
Expand Down
30 changes: 30 additions & 0 deletions app/src/pages/Conversations.tsx
Comment thread
graycyrus marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,33 @@ const Conversations = ({
}
};

const handleUpdateTaskCard = async (
card: TaskBoardCard,
nextCard: TaskBoardCard
): Promise<void> => {
if (!selectedThreadId || !selectedTaskBoard) return;
const now = new Date().toISOString();
const nextBoard = {
...selectedTaskBoard,
cards: selectedTaskBoard.cards.map(existing =>
existing.id === card.id ? { ...nextCard, updatedAt: now } : existing
),
updatedAt: now,
};
dispatch(setTaskBoardForThread({ threadId: selectedThreadId, board: nextBoard }));
try {
const saved = await threadApi.putTaskBoard(selectedThreadId, nextBoard.cards);
if (!saved) {
throw new Error('Task board update returned no board');
}
dispatch(setTaskBoardForThread({ threadId: selectedThreadId, board: saved }));
} catch (error) {
debug('putTaskBoard failed: %o', error);
setSendAdvisory(t('conversations.taskKanban.updateFailed'));
dispatch(setTaskBoardForThread({ threadId: selectedThreadId, board: selectedTaskBoard }));
}
};

const filteredThreads = useMemo(() => {
// Worker/subagent threads (any thread with `parentThreadId`) are
// surfaced through two intentional paths (issue #1624):
Expand Down Expand Up @@ -1554,6 +1581,9 @@ const Conversations = ({
onMove={(card, status) => {
void handleMoveTaskCard(card, status);
}}
onUpdateCard={(card, nextCard) => {
void handleUpdateTaskCard(card, nextCard);
}}
/>
)}
{visibleMessages.map(msg => (
Expand Down
Loading
Loading