import * as vscode from "vscode"; import { listLocatedTasks } from "@/commands/listLocatedTasks"; import { resolveTaskLocations } from "@/commands/resolveTaskLocations"; import { saveTaskDescription } from "@/commands/saveTaskDescription"; import { buildDashboardView } from "@/dashboard/buildDashboardView"; import { TaskFilter } from "@/dashboard/filterTasks"; import { GroupBy } from "@/dashboard/groupBy"; import { parseDashboardInboundMessage, type DashboardInboundMessage, } from "@/dashboard/messages"; import { DashboardOutboundMessage } from "@/dashboard/outboundMessages"; import { appError, AppErrorVariant } from "@/error"; import { LocatedTask } from "@/model/taskLocation"; import { IConfigProvider, ITaskRepository } from "@/ports"; import { presentError } from "@/ui/presentError"; import { createNonce, getTaskDashboardHtml } from "./taskDashboardHtml"; export type TaskDashboardDeps = { repo: ITaskRepository; config: IConfigProvider; /** Called after dashboard mutates tasks (e.g. save description). */ onTasksMutated?: () => void; }; export type TaskDashboardShowOptions = { /** Pre-select this task after load (e.g. open from tree). */ selectedId?: string; }; /** * Host adapter for the Task Dashboard webview. * Owns panel lifecycle and message wiring; business rules stay in use-cases. */ export class TaskDashboardPanel { static #current: TaskDashboardPanel | undefined; static show( deps: TaskDashboardDeps, options: TaskDashboardShowOptions = {}, ): void { if (TaskDashboardPanel.#current) { const current = TaskDashboardPanel.#current; if (options.selectedId !== undefined) { current.#selectedId = options.selectedId; } current.#panel.reveal(vscode.ViewColumn.One); void current.reloadTasks(); return; } const panel = vscode.window.createWebviewPanel( "xboctFlow.dashboard", "XBOCT flow", vscode.ViewColumn.One, { enableScripts: true, retainContextWhenHidden: true, }, ); TaskDashboardPanel.#current = new TaskDashboardPanel( panel, deps, options.selectedId, ); } static refreshIfOpen(): void { void TaskDashboardPanel.#current?.reloadTasks(); } readonly #panel: vscode.WebviewPanel; readonly #deps: TaskDashboardDeps; #tasks: LocatedTask[] = []; #filter: TaskFilter = {}; #groupBy: GroupBy = GroupBy.NONE; #selectedId: string | undefined; #disposed = false; constructor( panel: vscode.WebviewPanel, deps: TaskDashboardDeps, selectedId?: string, ) { this.#panel = panel; this.#deps = deps; this.#selectedId = selectedId; const nonce = createNonce(); this.#panel.webview.html = getTaskDashboardHtml(this.#panel.webview, nonce); this.#panel.webview.onDidReceiveMessage((raw: unknown) => { void this.#onMessage(raw); }); this.#panel.onDidDispose(() => { this.#disposed = true; if (TaskDashboardPanel.#current === this) { TaskDashboardPanel.#current = undefined; } }); } async reloadTasks(): Promise { if (this.#disposed) return; const locations = resolveTaskLocations(this.#deps.config); const result = await listLocatedTasks(this.#deps.repo, locations); if (result.isErr()) { this.#post({ type: "error", error: result.error }); if (result.error.variant === AppErrorVariant.NO_FOLDER) { presentError(result.error); } return; } this.#tasks = result.value; if ( this.#selectedId && !this.#tasks.some((item) => item.task.id === this.#selectedId) ) { this.#selectedId = undefined; } this.#postState(); } async #onMessage(raw: unknown): Promise { const parsed = parseDashboardInboundMessage(raw); if (parsed.isErr()) { this.#post({ type: "error", error: parsed.error }); return; } await this.#handleInbound(parsed.value); } async #handleInbound(message: DashboardInboundMessage): Promise { switch (message.type) { case "ready": case "refresh": await this.reloadTasks(); return; case "selectTask": this.#selectedId = message.id; this.#postState(); return; case "setFilter": this.#filter = message.filter; this.#postState(); return; case "setGroupBy": this.#groupBy = message.groupBy; this.#postState(); return; case "saveDescription": await this.#saveDescription(message.id, message.description); return; case "createTask": await vscode.commands.executeCommand("xboctFlow.createTask"); await this.reloadTasks(); return; } } async #saveDescription(id: string, description: string): Promise { const located = this.#tasks.find((item) => item.task.id === id); if (!located) { this.#post({ type: "error", error: appError(AppErrorVariant.NOT_FOUND, { id }), }); return; } const result = await saveTaskDescription(this.#deps.repo, { folderPath: located.location.folderPath, id, description, }); if (result.isErr()) { this.#post({ type: "error", error: result.error }); return; } const saved = result.value; this.#tasks = this.#tasks.map((item) => item.task.id === saved.id ? { task: saved, location: item.location } : item, ); this.#selectedId = saved.id; this.#post({ type: "saved", task: saved }); this.#postState(); this.#deps.onTasksMutated?.(); } #postState(): void { const plainTasks = this.#tasks.map((item) => item.task); const view = buildDashboardView(plainTasks, { filter: this.#filter, groupBy: this.#groupBy, selectedId: this.#selectedId, }); this.#post({ type: "state", filter: this.#filter, groupBy: this.#groupBy, selectedId: this.#selectedId, groups: view.groups, selected: view.selected ?? null, }); } #post(message: DashboardOutboundMessage): void { if (this.#disposed) return; void this.#panel.webview.postMessage(message); } }