From 1fe6c7e508c9c3cfc1bbb254c11028b4843f5119 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 8 Jan 2026 11:26:27 +0900 Subject: [PATCH] feat(task-toast): display skills and concurrency info in toast - Add skills field to TrackedTask and LaunchInput types - Show skills in task list message as [skill1, skill2] - Add concurrency slot info [running/limit] in Running header - Pass skills from sisyphus_task to toast manager (sync & background) - Add unit tests for new toast features --- src/features/background-agent/manager.ts | 1 + src/features/background-agent/types.ts | 1 + .../task-toast-manager/manager.test.ts | 145 ++++++++++++++++++ src/features/task-toast-manager/manager.ts | 27 ++-- src/features/task-toast-manager/types.ts | 1 + src/tools/sisyphus-task/tools.ts | 2 + 6 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 src/features/task-toast-manager/manager.test.ts diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index f697a96fc..be52b08d6 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -132,6 +132,7 @@ export class BackgroundManager { description: input.description, agent: input.agent, isBackground: true, + skills: input.skills, }) } diff --git a/src/features/background-agent/types.ts b/src/features/background-agent/types.ts index 11e95c680..661afa6cd 100644 --- a/src/features/background-agent/types.ts +++ b/src/features/background-agent/types.ts @@ -40,6 +40,7 @@ export interface LaunchInput { parentMessageID: string parentModel?: { providerID: string; modelID: string } model?: { providerID: string; modelID: string } + skills?: string[] } export interface ResumeInput { diff --git a/src/features/task-toast-manager/manager.test.ts b/src/features/task-toast-manager/manager.test.ts new file mode 100644 index 000000000..1e813ba85 --- /dev/null +++ b/src/features/task-toast-manager/manager.test.ts @@ -0,0 +1,145 @@ +import { describe, test, expect, beforeEach, mock } from "bun:test" +import { TaskToastManager } from "./manager" +import type { ConcurrencyManager } from "../background-agent/concurrency" + +describe("TaskToastManager", () => { + let mockClient: { + tui: { + showToast: ReturnType + } + } + let toastManager: TaskToastManager + let mockConcurrencyManager: ConcurrencyManager + + beforeEach(() => { + mockClient = { + tui: { + showToast: mock(() => Promise.resolve()), + }, + } + mockConcurrencyManager = { + getConcurrencyLimit: mock(() => 5), + } as unknown as ConcurrencyManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + toastManager = new TaskToastManager(mockClient as any, mockConcurrencyManager) + }) + + describe("skills in toast message", () => { + test("should display skills when provided", () => { + // #given - a task with skills + const task = { + id: "task_1", + description: "Test task", + agent: "Sisyphus-Junior", + isBackground: true, + skills: ["playwright", "git-master"], + } + + // #when - addTask is called + toastManager.addTask(task) + + // #then - toast message should include skills + expect(mockClient.tui.showToast).toHaveBeenCalled() + const call = mockClient.tui.showToast.mock.calls[0][0] + expect(call.body.message).toContain("playwright") + expect(call.body.message).toContain("git-master") + }) + + test("should not display skills section when no skills provided", () => { + // #given - a task without skills + const task = { + id: "task_2", + description: "Test task without skills", + agent: "explore", + isBackground: true, + } + + // #when - addTask is called + toastManager.addTask(task) + + // #then - toast message should not include skills prefix + expect(mockClient.tui.showToast).toHaveBeenCalled() + const call = mockClient.tui.showToast.mock.calls[0][0] + expect(call.body.message).not.toContain("Skills:") + }) + }) + + describe("concurrency info in toast message", () => { + test("should display concurrency status in toast", () => { + // #given - multiple running tasks + toastManager.addTask({ + id: "task_1", + description: "First task", + agent: "explore", + isBackground: true, + }) + toastManager.addTask({ + id: "task_2", + description: "Second task", + agent: "librarian", + isBackground: true, + }) + + // #when - third task is added + toastManager.addTask({ + id: "task_3", + description: "Third task", + agent: "explore", + isBackground: true, + }) + + // #then - toast should show concurrency info + expect(mockClient.tui.showToast).toHaveBeenCalledTimes(3) + const lastCall = mockClient.tui.showToast.mock.calls[2][0] + // Should show "Running (3):" header + expect(lastCall.body.message).toContain("Running (3):") + }) + + test("should display concurrency limit info when available", () => { + // #given - a concurrency manager with known limit + const mockConcurrencyWithCounts = { + getConcurrencyLimit: mock(() => 5), + getRunningCount: mock(() => 2), + getQueuedCount: mock(() => 1), + } as unknown as ConcurrencyManager + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const managerWithConcurrency = new TaskToastManager(mockClient as any, mockConcurrencyWithCounts) + + // #when - a task is added + managerWithConcurrency.addTask({ + id: "task_1", + description: "Test task", + agent: "explore", + isBackground: true, + }) + + // #then - toast should show concurrency status like "2/5 slots" + expect(mockClient.tui.showToast).toHaveBeenCalled() + const call = mockClient.tui.showToast.mock.calls[0][0] + expect(call.body.message).toMatch(/\d+\/\d+/) + }) + }) + + describe("combined skills and concurrency display", () => { + test("should display both skills and concurrency info together", () => { + // #given - a task with skills and concurrency manager + const task = { + id: "task_1", + description: "Full info task", + agent: "Sisyphus-Junior", + isBackground: true, + skills: ["frontend-ui-ux"], + } + + // #when - addTask is called + toastManager.addTask(task) + + // #then - toast should include both skills and task count + expect(mockClient.tui.showToast).toHaveBeenCalled() + const call = mockClient.tui.showToast.mock.calls[0][0] + expect(call.body.message).toContain("frontend-ui-ux") + expect(call.body.message).toContain("Running (1):") + }) + }) +}) diff --git a/src/features/task-toast-manager/manager.ts b/src/features/task-toast-manager/manager.ts index 127916974..66a03b2a2 100644 --- a/src/features/task-toast-manager/manager.ts +++ b/src/features/task-toast-manager/manager.ts @@ -18,15 +18,13 @@ export class TaskToastManager { this.concurrencyManager = manager } - /** - * Register a new task and show consolidated toast - */ addTask(task: { id: string description: string agent: string isBackground: boolean status?: TaskStatus + skills?: string[] }): void { const trackedTask: TrackedTask = { id: task.id, @@ -35,6 +33,7 @@ export class TaskToastManager { status: task.status ?? "running", startedAt: new Date(), isBackground: task.isBackground, + skills: task.skills, } this.tasks.set(task.id, trackedTask) @@ -89,22 +88,31 @@ export class TaskToastManager { return `${hours}h ${minutes % 60}m` } - /** - * Build task list message - */ + private getConcurrencyInfo(): string { + if (!this.concurrencyManager) return "" + const running = this.getRunningTasks() + const queued = this.getQueuedTasks() + const total = running.length + queued.length + const limit = this.concurrencyManager.getConcurrencyLimit("default") + if (limit === Infinity) return "" + return ` [${total}/${limit}]` + } + private buildTaskListMessage(newTask: TrackedTask): string { const running = this.getRunningTasks() const queued = this.getQueuedTasks() + const concurrencyInfo = this.getConcurrencyInfo() const lines: string[] = [] if (running.length > 0) { - lines.push(`Running (${running.length}):`) + lines.push(`Running (${running.length}):${concurrencyInfo}`) for (const task of running) { const duration = this.formatDuration(task.startedAt) const bgIcon = task.isBackground ? "⚑" : "πŸ”„" const isNew = task.id === newTask.id ? " ← NEW" : "" - lines.push(`${bgIcon} ${task.description} (${task.agent}) - ${duration}${isNew}`) + const skillsInfo = task.skills?.length ? ` [${task.skills.join(", ")}]` : "" + lines.push(`${bgIcon} ${task.description} (${task.agent})${skillsInfo} - ${duration}${isNew}`) } } @@ -113,7 +121,8 @@ export class TaskToastManager { lines.push(`Queued (${queued.length}):`) for (const task of queued) { const bgIcon = task.isBackground ? "⏳" : "⏸️" - lines.push(`${bgIcon} ${task.description} (${task.agent})`) + const skillsInfo = task.skills?.length ? ` [${task.skills.join(", ")}]` : "" + lines.push(`${bgIcon} ${task.description} (${task.agent})${skillsInfo}`) } } diff --git a/src/features/task-toast-manager/types.ts b/src/features/task-toast-manager/types.ts index 2e6ba01b3..de4aca0a0 100644 --- a/src/features/task-toast-manager/types.ts +++ b/src/features/task-toast-manager/types.ts @@ -7,6 +7,7 @@ export interface TrackedTask { status: TaskStatus startedAt: Date isBackground: boolean + skills?: string[] } export interface TaskToastOptions { diff --git a/src/tools/sisyphus-task/tools.ts b/src/tools/sisyphus-task/tools.ts index 36ddce336..c79999da1 100644 --- a/src/tools/sisyphus-task/tools.ts +++ b/src/tools/sisyphus-task/tools.ts @@ -221,6 +221,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.` parentMessageID: ctx.messageID, parentModel, model: categoryModel, + skills: args.skills, }) ctx.metadata?.({ @@ -268,6 +269,7 @@ System notifies on completion. Use \`background_output\` with task_id="${task.id description: args.description, agent: agentToUse, isBackground: false, + skills: args.skills, }) }