feat(features): add TaskToastManager for consolidated task notifications
- Create task-toast-manager feature with singleton pattern - Show running task list (newest first) when new task starts - Track queued tasks status from ConcurrencyManager - Integrate with BackgroundManager and sisyphus-task tool 🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) assistance
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
MESSAGE_STORAGE,
|
||||
} from "../hook-message-injector"
|
||||
import { subagentSessions } from "../claude-code-session-state"
|
||||
import { getTaskToastManager } from "../task-toast-manager"
|
||||
|
||||
const TASK_TTL_MS = 30 * 60 * 1000
|
||||
|
||||
@@ -122,17 +123,14 @@ export class BackgroundManager {
|
||||
|
||||
log("[background-agent] Launching task:", { taskId: task.id, sessionID, agent: input.agent })
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tuiClient = this.client as any
|
||||
if (tuiClient.tui?.showToast) {
|
||||
tuiClient.tui.showToast({
|
||||
body: {
|
||||
title: "Background Task Started",
|
||||
message: `"${input.description}" running with ${input.agent}`,
|
||||
variant: "info",
|
||||
duration: 3000,
|
||||
},
|
||||
}).catch(() => {})
|
||||
const toastManager = getTaskToastManager()
|
||||
if (toastManager) {
|
||||
toastManager.addTask({
|
||||
id: task.id,
|
||||
description: input.description,
|
||||
agent: input.agent,
|
||||
isBackground: true,
|
||||
})
|
||||
}
|
||||
|
||||
this.client.session.promptAsync({
|
||||
@@ -378,17 +376,13 @@ export class BackgroundManager {
|
||||
|
||||
log("[background-agent] notifyParentSession called for task:", task.id)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tuiClient = this.client as any
|
||||
if (tuiClient.tui?.showToast) {
|
||||
tuiClient.tui.showToast({
|
||||
body: {
|
||||
title: "Background Task Completed",
|
||||
message: `Task "${task.description}" finished in ${duration}.`,
|
||||
variant: "success",
|
||||
duration: 5000,
|
||||
},
|
||||
}).catch(() => {})
|
||||
const toastManager = getTaskToastManager()
|
||||
if (toastManager) {
|
||||
toastManager.showCompletionToast({
|
||||
id: task.id,
|
||||
description: task.description,
|
||||
duration,
|
||||
})
|
||||
}
|
||||
|
||||
const message = `[BACKGROUND TASK COMPLETED] Task "${task.description}" finished in ${duration}. Use background_output with task_id="${task.id}" to get results.`
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { TaskToastManager, getTaskToastManager, initTaskToastManager } from "./manager"
|
||||
export type { TrackedTask, TaskStatus, TaskToastOptions } from "./types"
|
||||
@@ -0,0 +1,190 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import type { TrackedTask, TaskStatus } from "./types"
|
||||
import type { ConcurrencyManager } from "../background-agent/concurrency"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
|
||||
export class TaskToastManager {
|
||||
private tasks: Map<string, TrackedTask> = new Map()
|
||||
private client: OpencodeClient
|
||||
private concurrencyManager?: ConcurrencyManager
|
||||
|
||||
constructor(client: OpencodeClient, concurrencyManager?: ConcurrencyManager) {
|
||||
this.client = client
|
||||
this.concurrencyManager = concurrencyManager
|
||||
}
|
||||
|
||||
setConcurrencyManager(manager: ConcurrencyManager): void {
|
||||
this.concurrencyManager = manager
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new task and show consolidated toast
|
||||
*/
|
||||
addTask(task: {
|
||||
id: string
|
||||
description: string
|
||||
agent: string
|
||||
isBackground: boolean
|
||||
status?: TaskStatus
|
||||
}): void {
|
||||
const trackedTask: TrackedTask = {
|
||||
id: task.id,
|
||||
description: task.description,
|
||||
agent: task.agent,
|
||||
status: task.status ?? "running",
|
||||
startedAt: new Date(),
|
||||
isBackground: task.isBackground,
|
||||
}
|
||||
|
||||
this.tasks.set(task.id, trackedTask)
|
||||
this.showTaskListToast(trackedTask)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update task status
|
||||
*/
|
||||
updateTask(id: string, status: TaskStatus): void {
|
||||
const task = this.tasks.get(id)
|
||||
if (task) {
|
||||
task.status = status
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove completed/error task
|
||||
*/
|
||||
removeTask(id: string): void {
|
||||
this.tasks.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all running tasks (newest first)
|
||||
*/
|
||||
getRunningTasks(): TrackedTask[] {
|
||||
const running = Array.from(this.tasks.values())
|
||||
.filter((t) => t.status === "running")
|
||||
.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime())
|
||||
return running
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all queued tasks
|
||||
*/
|
||||
getQueuedTasks(): TrackedTask[] {
|
||||
return Array.from(this.tasks.values())
|
||||
.filter((t) => t.status === "queued")
|
||||
.sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime())
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration since task started
|
||||
*/
|
||||
private formatDuration(startedAt: Date): string {
|
||||
const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1000)
|
||||
if (seconds < 60) return `${seconds}s`
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
if (minutes < 60) return `${minutes}m ${seconds % 60}s`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
return `${hours}h ${minutes % 60}m`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build task list message
|
||||
*/
|
||||
private buildTaskListMessage(newTask: TrackedTask): string {
|
||||
const running = this.getRunningTasks()
|
||||
const queued = this.getQueuedTasks()
|
||||
|
||||
const lines: string[] = []
|
||||
|
||||
if (running.length > 0) {
|
||||
lines.push(`Running (${running.length}):`)
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (queued.length > 0) {
|
||||
if (lines.length > 0) lines.push("")
|
||||
lines.push(`Queued (${queued.length}):`)
|
||||
for (const task of queued) {
|
||||
const bgIcon = task.isBackground ? "⏳" : "⏸️"
|
||||
lines.push(`${bgIcon} ${task.description} (${task.agent})`)
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Show consolidated toast with all running/queued tasks
|
||||
*/
|
||||
private showTaskListToast(newTask: TrackedTask): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tuiClient = this.client as any
|
||||
if (!tuiClient.tui?.showToast) return
|
||||
|
||||
const message = this.buildTaskListMessage(newTask)
|
||||
const running = this.getRunningTasks()
|
||||
const queued = this.getQueuedTasks()
|
||||
|
||||
const title = newTask.isBackground
|
||||
? `⚡ New Background Task`
|
||||
: `🔄 New Task Executed`
|
||||
|
||||
tuiClient.tui.showToast({
|
||||
body: {
|
||||
title,
|
||||
message: message || `${newTask.description} (${newTask.agent})`,
|
||||
variant: "info",
|
||||
duration: running.length + queued.length > 2 ? 5000 : 3000,
|
||||
},
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Show task completion toast
|
||||
*/
|
||||
showCompletionToast(task: { id: string; description: string; duration: string }): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tuiClient = this.client as any
|
||||
if (!tuiClient.tui?.showToast) return
|
||||
|
||||
this.removeTask(task.id)
|
||||
|
||||
const remaining = this.getRunningTasks()
|
||||
const queued = this.getQueuedTasks()
|
||||
|
||||
let message = `✅ "${task.description}" finished in ${task.duration}`
|
||||
if (remaining.length > 0 || queued.length > 0) {
|
||||
message += `\n\nStill running: ${remaining.length} | Queued: ${queued.length}`
|
||||
}
|
||||
|
||||
tuiClient.tui.showToast({
|
||||
body: {
|
||||
title: "Task Completed",
|
||||
message,
|
||||
variant: "success",
|
||||
duration: 5000,
|
||||
},
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
let instance: TaskToastManager | null = null
|
||||
|
||||
export function getTaskToastManager(): TaskToastManager | null {
|
||||
return instance
|
||||
}
|
||||
|
||||
export function initTaskToastManager(
|
||||
client: OpencodeClient,
|
||||
concurrencyManager?: ConcurrencyManager
|
||||
): TaskToastManager {
|
||||
instance = new TaskToastManager(client, concurrencyManager)
|
||||
return instance
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export type TaskStatus = "running" | "queued" | "completed" | "error"
|
||||
|
||||
export interface TrackedTask {
|
||||
id: string
|
||||
description: string
|
||||
agent: string
|
||||
status: TaskStatus
|
||||
startedAt: Date
|
||||
isBackground: boolean
|
||||
}
|
||||
|
||||
export interface TaskToastOptions {
|
||||
title: string
|
||||
message: string
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
Reference in New Issue
Block a user