Files
oh-my-openagent/src/features/task-toast-manager/manager.ts

241 lines
7.4 KiB
TypeScript

import type { PluginInput } from "@opencode-ai/plugin"
import type { TrackedTask, TaskStatus, ModelFallbackInfo } 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
}
addTask(task: {
id: string
sessionID?: string
description: string
agent: string
isBackground: boolean
status?: TaskStatus
category?: string
skills?: string[]
modelInfo?: ModelFallbackInfo
}): void {
const trackedTask: TrackedTask = {
id: task.id,
sessionID: task.sessionID,
description: task.description,
agent: task.agent,
status: task.status ?? "running",
startedAt: new Date(),
isBackground: task.isBackground,
category: task.category,
skills: task.skills,
modelInfo: task.modelInfo,
}
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
}
}
/**
* Update model info for a task by session ID
*/
updateTaskModelBySession(sessionID: string, modelInfo: ModelFallbackInfo): void {
if (!sessionID) return
const task = Array.from(this.tasks.values()).find((t) => t.sessionID === sessionID)
if (!task) return
if (task.modelInfo?.model === modelInfo.model && task.modelInfo?.type === modelInfo.type) return
task.modelInfo = modelInfo
this.showTaskListToast(task)
}
/**
* 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`
}
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[] = []
const isFallback = newTask.modelInfo && (
newTask.modelInfo.type === "inherited" ||
newTask.modelInfo.type === "system-default" ||
newTask.modelInfo.type === "runtime-fallback"
)
if (isFallback) {
const suffixMap: Record<"inherited" | "system-default" | "runtime-fallback", string> = {
inherited: " (inherited from parent)",
"system-default": " (system default fallback)",
"runtime-fallback": " (runtime fallback)",
}
const suffix = suffixMap[newTask.modelInfo!.type as "inherited" | "system-default" | "runtime-fallback"]
lines.push(`[FALLBACK] Model: ${newTask.modelInfo!.model}${suffix}`)
lines.push("")
}
if (running.length > 0) {
lines.push(`Running (${running.length}):${concurrencyInfo}`)
for (const task of running) {
const duration = this.formatDuration(task.startedAt)
const bgIcon = task.isBackground ? "[BG]" : "[RUN]"
const isNew = task.id === newTask.id ? " ← NEW" : ""
const categoryInfo = task.category ? `/${task.category}` : ""
const skillsInfo = task.skills?.length ? ` [${task.skills.join(", ")}]` : ""
lines.push(`${bgIcon} ${task.description} (${task.agent}${categoryInfo})${skillsInfo} - ${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 ? "[Q]" : "[W]"
const categoryInfo = task.category ? `/${task.category}` : ""
const skillsInfo = task.skills?.length ? ` [${task.skills.join(", ")}]` : ""
const isNew = task.id === newTask.id ? " ← NEW" : ""
lines.push(`${bgIcon} ${task.description} (${task.agent}${categoryInfo})${skillsInfo} - Queued${isNew}`)
}
}
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
}
export function _resetTaskToastManagerForTesting(): void {
instance = null
}