241 lines
7.4 KiB
TypeScript
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
|
|
}
|