feat: task_list priority filter, session delete confirm, spawn_agent tool restrictions
- task_list: add priority param ('low'/'normal'/'high') alongside existing status filter
- Session delete: inline confirm row (Delete / Cancel) instead of immediate delete
- spawn_agent: allow_tools and deny_tools per-call params; role config remains ceiling;
deny_tools falls back to confirm_deny gate when no explicit tool_list is set
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -693,19 +693,53 @@
|
|||||||
editBtn.onclick = enterEditMode;
|
editBtn.onclick = enterEditMode;
|
||||||
|
|
||||||
// ── Delete ───────────────────────────────────────────────
|
// ── Delete ───────────────────────────────────────────────
|
||||||
delBtn.addEventListener('click', async (e) => {
|
delBtn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
await fetch(`/sessions/${s.session_id}?${_fileParams}`, { method: 'DELETE' });
|
|
||||||
if (sessionId === s.session_id) {
|
// Swap row content for inline confirm
|
||||||
sessionId = null;
|
editBtn.hidden = true;
|
||||||
clear_stored_session();
|
bodyEl.hidden = true;
|
||||||
currentHistory = [];
|
delBtn.hidden = true;
|
||||||
messagesEl.innerHTML = '';
|
|
||||||
sessionEl.textContent = '';
|
const confirmRow = document.createElement('div');
|
||||||
showToast('Session deleted');
|
confirmRow.className = 'session-confirm-row';
|
||||||
|
confirmRow.innerHTML =
|
||||||
|
'<span class="session-confirm-label">Delete this session?</span>';
|
||||||
|
|
||||||
|
const yesBtn = document.createElement('button');
|
||||||
|
yesBtn.className = 'session-confirm-yes';
|
||||||
|
yesBtn.textContent = 'Delete';
|
||||||
|
|
||||||
|
const noBtn = document.createElement('button');
|
||||||
|
noBtn.className = 'session-confirm-no';
|
||||||
|
noBtn.textContent = 'Cancel';
|
||||||
|
|
||||||
|
confirmRow.append(yesBtn, noBtn);
|
||||||
|
item.appendChild(confirmRow);
|
||||||
|
|
||||||
|
function cancelConfirm() {
|
||||||
|
confirmRow.remove();
|
||||||
|
editBtn.hidden = false;
|
||||||
|
bodyEl.hidden = false;
|
||||||
|
delBtn.hidden = false;
|
||||||
}
|
}
|
||||||
const res = await fetch(`/sessions?${_fileParams}`);
|
|
||||||
renderPanel((await res.json()).sessions);
|
noBtn.addEventListener('click', (e) => { e.stopPropagation(); cancelConfirm(); });
|
||||||
|
|
||||||
|
yesBtn.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await fetch(`/sessions/${s.session_id}?${_fileParams}`, { method: 'DELETE' });
|
||||||
|
if (sessionId === s.session_id) {
|
||||||
|
sessionId = null;
|
||||||
|
clear_stored_session();
|
||||||
|
currentHistory = [];
|
||||||
|
messagesEl.innerHTML = '';
|
||||||
|
sessionEl.textContent = '';
|
||||||
|
showToast('Session deleted');
|
||||||
|
}
|
||||||
|
const res = await fetch(`/sessions?${_fileParams}`);
|
||||||
|
renderPanel((await res.json()).sessions);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
sessionsPanel.appendChild(item);
|
sessionsPanel.appendChild(item);
|
||||||
|
|||||||
@@ -372,6 +372,35 @@
|
|||||||
}
|
}
|
||||||
.session-save-btn:hover { opacity: 0.75; }
|
.session-save-btn:hover { opacity: 0.75; }
|
||||||
|
|
||||||
|
.session-confirm-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.session-confirm-label {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #e06c75;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.session-confirm-yes, .session-confirm-no {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.session-confirm-yes { border-color: #e06c75; color: #e06c75; }
|
||||||
|
.session-confirm-no { border-color: var(--muted); color: var(--muted); }
|
||||||
|
.session-confirm-yes:hover, .session-confirm-no:hover { opacity: 0.75; }
|
||||||
|
|
||||||
.session-rename-input {
|
.session-rename-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ async def spawn_agent(
|
|||||||
tier: int = 1,
|
tier: int = 1,
|
||||||
timeout: int = 120,
|
timeout: int = 120,
|
||||||
max_rounds: int | None = None,
|
max_rounds: int | None = None,
|
||||||
|
allow_tools: list[str] | None = None,
|
||||||
|
deny_tools: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Spawn a sub-agent to complete a task synchronously.
|
Spawn a sub-agent to complete a task synchronously.
|
||||||
@@ -91,6 +93,21 @@ async def spawn_agent(
|
|||||||
confirm_allow = set(policy.get("allow", []))
|
confirm_allow = set(policy.get("allow", []))
|
||||||
confirm_deny = set(policy.get("deny", []))
|
confirm_deny = set(policy.get("deny", []))
|
||||||
|
|
||||||
|
# Per-call tool restrictions — role config remains the authoritative ceiling
|
||||||
|
if allow_tools is not None:
|
||||||
|
if tool_list is not None:
|
||||||
|
tool_list = [t for t in tool_list if t in allow_tools]
|
||||||
|
else:
|
||||||
|
tool_list = list(allow_tools)
|
||||||
|
|
||||||
|
if deny_tools is not None:
|
||||||
|
deny_set = set(deny_tools)
|
||||||
|
if tool_list is not None:
|
||||||
|
tool_list = [t for t in tool_list if t not in deny_set]
|
||||||
|
else:
|
||||||
|
# tool_list is unrestricted — block via confirm_deny so the gate fires
|
||||||
|
confirm_deny = confirm_deny | deny_set
|
||||||
|
|
||||||
if max_rounds is not None:
|
if max_rounds is not None:
|
||||||
model_cfg = dict(model_cfg)
|
model_cfg = dict(model_cfg)
|
||||||
model_cfg["max_rounds"] = max_rounds
|
model_cfg["max_rounds"] = max_rounds
|
||||||
@@ -198,6 +215,25 @@ DECLARATIONS = [
|
|||||||
type=types.Type.INTEGER,
|
type=types.Type.INTEGER,
|
||||||
description="Override max tool-loop iterations for this call.",
|
description="Override max tool-loop iterations for this call.",
|
||||||
),
|
),
|
||||||
|
"allow_tools": types.Schema(
|
||||||
|
type=types.Type.ARRAY,
|
||||||
|
items=types.Schema(type=types.Type.STRING),
|
||||||
|
description=(
|
||||||
|
"Restrict the sub-agent to only these tools. "
|
||||||
|
"Intersected with the role's tool set — cannot grant more than the role allows. "
|
||||||
|
"Omit to give the sub-agent the role's full tool set. "
|
||||||
|
"Example: ['web_search', 'web_read'] for a pure research agent."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"deny_tools": types.Schema(
|
||||||
|
type=types.Type.ARRAY,
|
||||||
|
items=types.Schema(type=types.Type.STRING),
|
||||||
|
description=(
|
||||||
|
"Block these tools from the sub-agent regardless of role config. "
|
||||||
|
"Use to prevent destructive operations in sensitive sub-tasks. "
|
||||||
|
"Example: ['shell_exec', 'file_write', 'cortex_restart']."
|
||||||
|
),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
required=["task"],
|
required=["task"],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -60,13 +60,15 @@ def _format_task(t: dict) -> str:
|
|||||||
# Sync implementations — called via asyncio.to_thread
|
# Sync implementations — called via asyncio.to_thread
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _task_list(status: str | None) -> str:
|
def _task_list(status: str | None, priority: str | None) -> str:
|
||||||
tasks = _load()
|
tasks = _load()
|
||||||
if status:
|
if status:
|
||||||
tasks = [t for t in tasks if t["status"] == status]
|
tasks = [t for t in tasks if t["status"] == status]
|
||||||
|
if priority:
|
||||||
|
tasks = [t for t in tasks if t.get("priority") == priority]
|
||||||
if not tasks:
|
if not tasks:
|
||||||
label = f"No {status} tasks." if status else "No tasks yet."
|
filters = " ".join(f for f in [status, priority] if f)
|
||||||
return label
|
return f"No {filters} tasks." if filters else "No tasks yet."
|
||||||
lines = [f"Tasks ({len(tasks)}):\n"]
|
lines = [f"Tasks ({len(tasks)}):\n"]
|
||||||
for t in tasks:
|
for t in tasks:
|
||||||
lines.append(_format_task(t))
|
lines.append(_format_task(t))
|
||||||
@@ -118,8 +120,8 @@ def _task_complete(task_id: str) -> str:
|
|||||||
# Async wrappers
|
# Async wrappers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async def task_list(status: str | None = None) -> str:
|
async def task_list(status: str | None = None, priority: str | None = None) -> str:
|
||||||
return await asyncio.to_thread(_task_list, status)
|
return await asyncio.to_thread(_task_list, status, priority)
|
||||||
|
|
||||||
|
|
||||||
async def task_create(title: str, description: str | None = None,
|
async def task_create(title: str, description: str | None = None,
|
||||||
@@ -148,6 +150,7 @@ DECLARATIONS = [
|
|||||||
type=types.Type.OBJECT,
|
type=types.Type.OBJECT,
|
||||||
properties={
|
properties={
|
||||||
"status": types.Schema(type=types.Type.STRING, description="Filter by status: 'todo', 'in_progress', or 'done'. Omit to list all."),
|
"status": types.Schema(type=types.Type.STRING, description="Filter by status: 'todo', 'in_progress', or 'done'. Omit to list all."),
|
||||||
|
"priority": types.Schema(type=types.Type.STRING, description="Filter by priority: 'low', 'normal', or 'high'. Omit to list all priorities."),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user