Skip to content
Open
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
27 changes: 27 additions & 0 deletions src/client/SubagentView.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -545,3 +545,30 @@
.jobsPaneError {
color: var(--dsw-alias-state-error-primary);
}

/* "Active only" toolbar row under the main agent card: collapses idle
topology branches so busy trees surface just the running subagents. */
.subagentToolbar {
padding: 2px 10px 4px;
}

.subagentToggle {
border: none;
border-radius: 999px;
background: transparent;
color: var(--dsw-alias-label-secondary);
cursor: pointer;
font: var(--dsw-font-xxxs-11);
padding: 2px 10px;
}

.subagentToggle:hover {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}

.subagentToggleOn,
.subagentToggleOn:hover {
background: var(--dsw-alias-interactive-bg-active);
color: var(--dsw-alias-label-primary);
}
64 changes: 64 additions & 0 deletions src/client/SubagentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
countSubagentDescendants,
isSideThreadSummary,
rootAncestor,
runningVisibilitySet,
} from './subagent-detect.ts'
import { type LastActivity } from '../subagent-activity.ts'
import { SIDE_LABEL_PREFIX } from '../sidechat-core.ts'
Expand All @@ -65,6 +66,26 @@ const ARGS_PREVIEW = 60
const JOB_POLL_MS = 2000
/** How long the kill button stays armed before it needs re-confirming. */
const JOB_KILL_ARM_MS = 3000
/** localStorage key of the "active only" toggle (a pure view preference). */
const ACTIVE_ONLY_KEY = 'dsh-better-sidebar.subagent.activeOnly'

/** Read the persisted "active only" preference; failures fall back to off. */
function readActiveOnlyPref(): boolean {
try {
return globalThis.localStorage?.getItem(ACTIVE_ONLY_KEY) === '1'
} catch {
return false
}
}

/** Persist the "active only" preference; storage failures are non-fatal. */
function writeActiveOnlyPref(value: boolean): void {
try {
globalThis.localStorage?.setItem(ACTIVE_ONLY_KEY, value ? '1' : '0')
} catch {
// Private mode / quota errors must never break the page.
}
}

/** The direct subagent children of one parent (durable `origin` rows;
* Side Chat threads ride the same origin but are tab-strip conversations,
Expand Down Expand Up @@ -240,13 +261,21 @@ interface RowsProps {
currentSessionId: string
/** The batch live-preview map (child id → latest activity). */
live: Readonly<Record<string, LastActivity>>
/**
* Active-only filter (`null` = off): session ids worth showing — running
* nodes plus ancestors with running descendants. Entries outside the set
* (and diagnostics) are hidden; `entry.activity === 'running'` always
* passes so a catalog row never lags its own summary flag.
*/
activeKeepSet: ReadonlySet<string> | null
openChild: (address: SidebarSubagentAddress) => void
refresh: (parentSessionId: string) => void
}

/** Render one topology level; branches are always expanded (lazy catalogs). */
function CatalogRows({
parentSessionId, catalog, catalogs, byId, level, currentSessionId, live,
activeKeepSet,
openChild, refresh,
}: RowsProps) {
const emptyLoading = catalog?.state === 'loading' && catalog.entries.length === 0
Expand All @@ -255,6 +284,10 @@ function CatalogRows({
// strip owns them). Legacy threads created before the descriptor fix still
// arrive as corrupt diagnostics; they are recognized by summary title.
const visibleEntries = (catalog?.entries ?? []).filter((entry) => {
if (activeKeepSet !== null && !(activeKeepSet.has(entry.id)
|| (entry.kind === 'child' && entry.activity === 'running'))) {
return false
}
if (entry.kind === 'child') return !(entry.label?.startsWith(SIDE_LABEL_PREFIX) ?? false)
return !(byId[entry.id]?.displayTitle.startsWith(SIDE_LABEL_PREFIX) ?? false)
})
Expand Down Expand Up @@ -361,6 +394,7 @@ function CatalogRows({
level={level + 1}
currentSessionId={currentSessionId}
live={live}
activeKeepSet={activeKeepSet}
openChild={openChild}
refresh={refresh}
/>
Expand Down Expand Up @@ -664,6 +698,22 @@ export function SubagentView(props: {
const rootSummary = rootId === undefined ? undefined : byId[rootId]
const live = useSubagentLive(rootId, active)

// "Active only" view mode: hide idle topology branches so a busy tree
// surfaces just the running subagents (and the ancestors that own them).
// A pure view preference — persisted client-side, defaulting to off.
const [activeOnly, setActiveOnly] = useState(readActiveOnlyPref)
const toggleActiveOnly = useCallback(() => {
setActiveOnly(value => {
const next = !value
writeActiveOnlyPref(next)
return next
})
}, [])
const activeKeepSet = useMemo(
() => (rootId === undefined || !activeOnly ? null : runningVisibilitySet(byId, rootId)),
[byId, rootId, activeOnly],
)

/** Catalog owners currently consuming live membership updates. */
const observedRef = useRef(new Set<string>())

Expand Down Expand Up @@ -844,6 +894,19 @@ export function SubagentView(props: {
</span>
</div>
)}
{rootId !== undefined && totals.count > 0 && (
<div className={css.subagentToolbar}>
<button
type="button"
aria-pressed={activeOnly}
title={t('subagentActiveOnlyTitle')}
className={clsx(css.subagentToggle, activeOnly && css.subagentToggleOn)}
onClick={toggleActiveOnly}
>
{activeOnly ? `✓ ${t('subagentActiveOnly')}` : t('subagentActiveOnly')}
</button>
</div>
)}
{rootId !== undefined && (
<div className={css.subagentChildren} role="group" aria-busy={summaryBackedLoading || undefined}>
{summaryBackedLoading && (
Expand All @@ -858,6 +921,7 @@ export function SubagentView(props: {
level={1}
currentSessionId={sessionId}
live={live}
activeKeepSet={activeKeepSet}
openChild={openChild}
refresh={refresh}
/>
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ export const ar: Record<string, string> = {
subagent: 'المهام',
openSubagent: 'المهام',
subagentMainAgent: 'الوكيل الرئيسي',
subagentActiveOnly: "للنشطين فقط",
subagentActiveOnlyTitle: "طيّ الوكلاء الفرعيين الخاملين؛ إظهار الوكلاء قيد التشغيل وسلسلة آبائهم فقط",
subagentEmpty: 'لا وكلاء فرعيون',
subagentEmptyDesc: 'الوكلاء الفرعيون المُنتَجون تحت الوكيل الرئيسي سيظهرون هنا',
subagentRunning: 'قيد التشغيل',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ export const de: Record<string, string> = {
subagent: 'Aufgaben',
openSubagent: 'Aufgaben',
subagentMainAgent: 'Hauptagent',
subagentActiveOnly: "Nur aktive",
subagentActiveOnlyTitle: "Untätige Subagenten einklappen; nur laufende und deren Vorfahren anzeigen",
subagentEmpty: 'Keine Subagenten',
subagentEmptyDesc: 'Subagenten, die unter dem Hauptagenten erzeugt werden, erscheinen hier',
subagentRunning: 'Läuft',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ export const fr: Record<string, string> = {
subagent: 'Gestion des tâches',
openSubagent: 'Gestion des tâches',
subagentMainAgent: 'Agent principal',
subagentActiveOnly: "Actifs uniquement",
subagentActiveOnlyTitle: "Replier les sous-agents inactifs ; garder les actifs et leurs ancêtres",
subagentEmpty: 'Aucun sous-agent pour l’instant',
subagentEmptyDesc: 'Les sous-agents dérivés de l’agent principal actuel s’afficheront ici',
subagentRunning: 'En cours',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ export const hi: Record<string, string> = {
subagent: 'कार्य',
openSubagent: 'कार्य',
subagentMainAgent: 'मुख्य एजेंट',
subagentActiveOnly: "केवल सक्रिय",
subagentActiveOnlyTitle: "निष्क्रिय सब-एजेंट समेटें; केवल चल रहे और उनके पूर्वज दिखाएँ",
subagentEmpty: 'कोई सबएजेंट नहीं',
subagentEmptyDesc: 'मुख्य एजेंट के अंतर्गत बने सबएजेंट यहाँ दिखेंगे',
subagentRunning: 'चल रहा',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ export const id: Record<string, string> = {
subagent: 'Tasks',
openSubagent: 'Tasks',
subagentMainAgent: 'Agen utama',
subagentActiveOnly: "Hanya aktif",
subagentActiveOnlyTitle: "Ciutkan subagen menganggur; tampilkan yang sedang berjalan dan leluhurnya",
subagentEmpty: 'Tidak ada subagen',
subagentEmptyDesc: 'Subagen yang dibangkitkan di bawah agen utama akan muncul di sini',
subagentRunning: 'Berjalan',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ export const it: Record<string, string> = {
subagent: 'Attività',
openSubagent: 'Attività',
subagentMainAgent: 'Agente principale',
subagentActiveOnly: "Solo attivi",
subagentActiveOnlyTitle: "Comprimi i sotto-agenti inattivi; mostra solo quelli attivi e i loro antenati",
subagentEmpty: 'Nessun sottoagente',
subagentEmptyDesc: 'I sottoagenti generati dall’agente principale compariranno qui',
subagentRunning: 'In esecuzione',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ export const ja: Record<string, string> = {
subagent: 'タスク管理',
openSubagent: 'タスク管理',
subagentMainAgent: 'メインエージェント',
subagentActiveOnly: 'アクティブのみ',
subagentActiveOnlyTitle: 'アイドル中のサブエージェントを折りたたみ、実行中のものとその親チェーンのみを表示',
subagentEmpty: 'サブエージェントなし',
subagentEmptyDesc: 'メインエージェントが派生したサブエージェントはここに表示されます',
subagentRunning: '実行中',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ export const ko: Record<string, string> = {
subagent: '작업 관리',
openSubagent: '작업 관리',
subagentMainAgent: '주 에이전트',
subagentActiveOnly: "활성만",
subagentActiveOnlyTitle: "유휴 상태의 하위 에이전트는 접고, 실행 중인 것과 그 조상 체인만 표시",
subagentEmpty: '서브 에이전트 없음',
subagentEmptyDesc: '현재 주 에이전트에서 파생된 서브 에이전트가 여기에 표시됩니다',
subagentRunning: '실행 중',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ export const nl: Record<string, string> = {
subagent: 'Taken',
openSubagent: 'Taken',
subagentMainAgent: 'Hoofdagent',
subagentActiveOnly: "Alleen actief",
subagentActiveOnlyTitle: "Inactieve subagenten inklappen; alleen draaiende en hun voorouders tonen",
subagentEmpty: 'Geen subagents',
subagentEmptyDesc: 'Subagents voortgebracht door de hoofdagent verschijnen hier',
subagentRunning: 'Actief',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ export const pl: Record<string, string> = {
subagent: 'Zadania',
openSubagent: 'Zadania',
subagentMainAgent: 'Agent główny',
subagentActiveOnly: "Tylko aktywne",
subagentActiveOnlyTitle: "Zwiń bezczynne podagenty; pokazuj tylko działające i ich przodków",
subagentEmpty: 'Brak podagentów',
subagentEmptyDesc: 'Podagenci powołani przez agenta głównego pojawią się tutaj',
subagentRunning: 'Działa',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ export const pt: Record<string, string> = {
subagent: 'Tarefas',
openSubagent: 'Tarefas',
subagentMainAgent: 'Agente principal',
subagentActiveOnly: "Somente ativos",
subagentActiveOnlyTitle: "Recolher subagentes ociosos; mostrar apenas os ativos e seus ancestrais",
subagentEmpty: 'Nenhum subagente',
subagentEmptyDesc: 'Subagentes gerados sob o agente principal aparecerão aqui',
subagentRunning: 'Executando',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ export const ru: Record<string, string> = {
subagent: 'Задачи',
openSubagent: 'Задачи',
subagentMainAgent: 'Главный агент',
subagentActiveOnly: "Только активные",
subagentActiveOnlyTitle: "Свернуть простаивающие субагенты; показывать только выполняющиеся и их предков",
subagentEmpty: 'Нет субагентов',
subagentEmptyDesc: 'Субагенты, порождённые главным агентом, появятся здесь',
subagentRunning: 'Выполняется',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-sv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ export const sv: Record<string, string> = {
subagent: 'Uppgifter',
openSubagent: 'Uppgifter',
subagentMainAgent: 'Huvudagent',
subagentActiveOnly: "Endast aktiva",
subagentActiveOnlyTitle: "Fäll ihop inaktiva underagenter; visa endast körande och deras förfäder",
subagentEmpty: 'Inga subagenter',
subagentEmptyDesc: 'Subagenter skapade under huvudagenten visas här',
subagentRunning: 'Kör',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-th.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ export const th: Record<string, string> = {
subagent: 'งาน',
openSubagent: 'งาน',
subagentMainAgent: 'ตัวแทนหลัก',
subagentActiveOnly: "เฉพาะที่กำลังทำงาน",
subagentActiveOnlyTitle: "ย่อซับเอเจนต์ที่ว่างงาน แสดงเฉพาะตัวที่กำลังทำงานและบรรพบุรุษของมัน",
subagentEmpty: 'ไม่มีตัวแทนย่อย',
subagentEmptyDesc: 'ตัวแทนย่อยที่สร้างภายใต้ตัวแทนหลักจะปรากฏที่นี่',
subagentRunning: 'กำลังทำงาน',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ export const tr: Record<string, string> = {
subagent: 'Görevler',
openSubagent: 'Görevler',
subagentMainAgent: 'Ana aracı',
subagentActiveOnly: "Sadece aktif",
subagentActiveOnlyTitle: "Boşta bekleyen alt ajanları daralt; yalnızca çalışanlar ve üst zincirlerini göster",
subagentEmpty: 'Alt aracı yok',
subagentEmptyDesc: 'Ana aracının altında doğan alt aracılar burada görünür',
subagentRunning: 'Çalışıyor',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ export const vi: Record<string, string> = {
subagent: 'Quản lý tác vụ',
openSubagent: 'Quản lý tác vụ',
subagentMainAgent: 'Tác nhân chính',
subagentActiveOnly: "Chỉ mục đang chạy",
subagentActiveOnlyTitle: "Thu gọn subagent nhàn rỗi; chỉ hiện mục đang chạy và chuỗi cha của nó",
subagentEmpty: 'Không có tác nhân con',
subagentEmptyDesc: 'Tác nhân con do tác nhân chính tạo ra sẽ hiển thị ở đây',
subagentRunning: 'Đang chạy',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-zh-HK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ export const zhHK: Record<string, string> = {
subagent: '任務管理',
openSubagent: '任務管理',
subagentMainAgent: '主代理',
subagentActiveOnly: "只看活躍",
subagentActiveOnlyTitle: "收起空閒子代理,僅顯示執行中的子代理及其父鏈",
subagentEmpty: '暫無子代理',
subagentEmptyDesc: '目前主代理派生的子代理將顯示在這裡',
subagentRunning: '執行中',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-zh-MO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ export const zhMO: Record<string, string> = {
subagent: '任務管理',
openSubagent: '任務管理',
subagentMainAgent: '主代理',
subagentActiveOnly: "只看活躍",
subagentActiveOnlyTitle: "收起空閒子代理,僅顯示執行中的子代理及其父鏈",
subagentEmpty: '暫無子代理',
subagentEmptyDesc: '目前主代理派生的子代理將顯示在這裡',
subagentRunning: '執行中',
Expand Down
2 changes: 2 additions & 0 deletions src/client/locales-zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ export const zhTW: Record<string, string> = {
subagent: '任務管理',
openSubagent: '任務管理',
subagentMainAgent: '主代理',
subagentActiveOnly: "只看活躍",
subagentActiveOnlyTitle: "摺疊閒置子代理,僅顯示執行中的子代理及其父鏈",
subagentEmpty: '暫無子代理',
subagentEmptyDesc: '目前主代理派生的子代理將顯示在這裡',
subagentRunning: '執行中',
Expand Down
4 changes: 4 additions & 0 deletions src/client/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ export const zh = {
subagent: '任务管理',
openSubagent: '任务管理',
subagentMainAgent: '主代理',
subagentActiveOnly: '只看活跃',
subagentActiveOnlyTitle: '收起空闲子代理,仅显示运行中的子代理及其父链',
subagentEmpty: '暂无子代理',
subagentEmptyDesc: '当前主代理派生的子代理将显示在这里',
subagentRunning: '运行中',
Expand Down Expand Up @@ -627,6 +629,8 @@ export const en: Record<keyof typeof zh, string> = {
subagent: 'Tasks',
openSubagent: 'Tasks',
subagentMainAgent: 'Main agent',
subagentActiveOnly: 'Active only',
subagentActiveOnlyTitle: 'Collapse idle subagents; keep running ones and their ancestors',
subagentEmpty: 'No subagents',
subagentEmptyDesc: 'Subagents spawned under the main agent will appear here',
subagentRunning: 'Running',
Expand Down
41 changes: 41 additions & 0 deletions src/client/subagent-detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,44 @@ export function countSubagentDescendants(
}
return totals
}

/**
* The session ids worth showing under "active only" mode: every running
* subagent plus each ancestor whose subtree contains one (hiding an idle
* parent must never orphan its running descendants). The root itself joins
* only when its subtree runs — the page renders it separately regardless.
* Cycles fail soft (same discipline as {@link countSubagentDescendants}).
*/
export function runningVisibilitySet(
byId: SidebarSessionList['byId'],
rootId: string,
): Set<string> {
const keep = new Set<string>()
const childrenOf = new Map<string, string[]>()
for (const summary of Object.values(byId)) {
if (summary.origin !== 'subagent' || isSideThreadSummary(summary)
|| summary.parentId === undefined) continue
const list = childrenOf.get(summary.parentId)
if (list) list.push(summary.id)
else childrenOf.set(summary.parentId, [summary.id])
}
const visit = (id: string): boolean => {
const seen = new Set<string>()
const walk = (current: string): boolean => {
if (seen.has(current)) return false
seen.add(current)
let subtreeRuns = false
for (const childId of childrenOf.get(current) ?? []) {
if (walk(childId)) subtreeRuns = true
}
if (subtreeRuns || byId[current]?.running === true) {
keep.add(current)
return true
}
return false
}
return walk(id)
}
visit(rootId)
return keep
}
Loading