feat(sisyphus-task): inherit parent model for categories and show fallback warning

- Change model priority: user override > parent model > category default
- Add ModelFallbackInfo to track model resolution type
- Show warning toast when category uses inherited or default model
- Add tests for model fallback info in task toast
This commit is contained in:
Nguyen Khac Trung Kien
2026-01-13 08:58:47 +07:00
parent dbe51e4253
commit 16c38d7230
8 changed files with 130 additions and 9 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
export { TaskToastManager, getTaskToastManager, initTaskToastManager } from "./manager"
export type { TrackedTask, TaskStatus, TaskToastOptions } from "./types"
export type { TrackedTask, TaskStatus, TaskToastOptions, ModelFallbackInfo } from "./types"
@@ -142,4 +142,87 @@ describe("TaskToastManager", () => {
expect(call.body.message).toContain("Running (1):")
})
})
describe("model fallback info in toast message", () => {
test("should display warning when model falls back to default", () => {
// #given - a task with model fallback to default
const task = {
id: "task_1",
description: "Task with default model",
agent: "Sisyphus-Junior",
isBackground: false,
modelInfo: { model: "anthropic/claude-sonnet-4-5", type: "default" as const },
}
// #when - addTask is called
toastManager.addTask(task)
// #then - toast should show warning with model info
expect(mockClient.tui.showToast).toHaveBeenCalled()
const call = mockClient.tui.showToast.mock.calls[0][0]
expect(call.body.message).toContain("⚠️")
expect(call.body.message).toContain("anthropic/claude-sonnet-4-5")
expect(call.body.message).toContain("(default)")
})
test("should display warning when model is inherited from parent", () => {
// #given - a task with inherited model
const task = {
id: "task_2",
description: "Task with inherited model",
agent: "Sisyphus-Junior",
isBackground: false,
modelInfo: { model: "cliproxy/claude-opus-4-5", type: "inherited" as const },
}
// #when - addTask is called
toastManager.addTask(task)
// #then - toast should show warning with inherited model
expect(mockClient.tui.showToast).toHaveBeenCalled()
const call = mockClient.tui.showToast.mock.calls[0][0]
expect(call.body.message).toContain("⚠️")
expect(call.body.message).toContain("cliproxy/claude-opus-4-5")
expect(call.body.message).toContain("(inherited)")
})
test("should not display model info when user-defined", () => {
// #given - a task with user-defined model
const task = {
id: "task_3",
description: "Task with user model",
agent: "Sisyphus-Junior",
isBackground: false,
modelInfo: { model: "my-provider/my-model", type: "user-defined" as const },
}
// #when - addTask is called
toastManager.addTask(task)
// #then - toast should NOT show model warning
expect(mockClient.tui.showToast).toHaveBeenCalled()
const call = mockClient.tui.showToast.mock.calls[0][0]
expect(call.body.message).not.toContain("⚠️ Model:")
expect(call.body.message).not.toContain("(inherited)")
expect(call.body.message).not.toContain("(default)")
})
test("should not display model info when not provided", () => {
// #given - a task without model info
const task = {
id: "task_4",
description: "Task without model info",
agent: "explore",
isBackground: true,
}
// #when - addTask is called
toastManager.addTask(task)
// #then - toast should NOT show model warning
expect(mockClient.tui.showToast).toHaveBeenCalled()
const call = mockClient.tui.showToast.mock.calls[0][0]
expect(call.body.message).not.toContain("⚠️ Model:")
})
})
})
+11 -1
View File
@@ -1,5 +1,5 @@
import type { PluginInput } from "@opencode-ai/plugin"
import type { TrackedTask, TaskStatus } from "./types"
import type { TrackedTask, TaskStatus, ModelFallbackInfo } from "./types"
import type { ConcurrencyManager } from "../background-agent/concurrency"
type OpencodeClient = PluginInput["client"]
@@ -25,6 +25,7 @@ export class TaskToastManager {
isBackground: boolean
status?: TaskStatus
skills?: string[]
modelInfo?: ModelFallbackInfo
}): void {
const trackedTask: TrackedTask = {
id: task.id,
@@ -34,6 +35,7 @@ export class TaskToastManager {
startedAt: new Date(),
isBackground: task.isBackground,
skills: task.skills,
modelInfo: task.modelInfo,
}
this.tasks.set(task.id, trackedTask)
@@ -105,6 +107,14 @@ export class TaskToastManager {
const lines: string[] = []
// Show model fallback warning for the new task if applicable
if (newTask.modelInfo && newTask.modelInfo.type !== "user-defined") {
const icon = "⚠️"
const suffix = newTask.modelInfo.type === "inherited" ? " (inherited)" : " (default)"
lines.push(`${icon} Model: ${newTask.modelInfo.model}${suffix}`)
lines.push("")
}
if (running.length > 0) {
lines.push(`Running (${running.length}):${concurrencyInfo}`)
for (const task of running) {
+6
View File
@@ -1,5 +1,10 @@
export type TaskStatus = "running" | "queued" | "completed" | "error"
export interface ModelFallbackInfo {
model: string
type: "user-defined" | "inherited" | "default"
}
export interface TrackedTask {
id: string
description: string
@@ -8,6 +13,7 @@ export interface TrackedTask {
startedAt: Date
isBackground: boolean
skills?: string[]
modelInfo?: ModelFallbackInfo
}
export interface TaskToastOptions {