YeonGyu-Kim 49c933961e fix(background-cancel): skip notification when user explicitly cancels tasks
- Add skipNotification option to cancelTask method
- Apply skipNotification to background_cancel tool
- Prevents unwanted notifications when user cancels via tool
2026-02-03 16:56:40 +09:00

168 lines
4.5 KiB
TypeScript

import type { ContextCollector } from "./collector"
import type { Message, Part } from "@opencode-ai/sdk"
import { log } from "../../shared"
import { getMainSessionID } from "../claude-code-session-state"
interface OutputPart {
type: string
text?: string
[key: string]: unknown
}
interface InjectionResult {
injected: boolean
contextLength: number
}
export function injectPendingContext(
collector: ContextCollector,
sessionID: string,
parts: OutputPart[]
): InjectionResult {
if (!collector.hasPending(sessionID)) {
return { injected: false, contextLength: 0 }
}
const textPartIndex = parts.findIndex((p) => p.type === "text" && p.text !== undefined)
if (textPartIndex === -1) {
return { injected: false, contextLength: 0 }
}
const pending = collector.consume(sessionID)
const originalText = parts[textPartIndex].text ?? ""
parts[textPartIndex].text = `${pending.merged}\n\n---\n\n${originalText}`
return {
injected: true,
contextLength: pending.merged.length,
}
}
interface ChatMessageInput {
sessionID: string
agent?: string
model?: { providerID: string; modelID: string }
messageID?: string
}
interface ChatMessageOutput {
message: Record<string, unknown>
parts: OutputPart[]
}
export function createContextInjectorHook(collector: ContextCollector) {
return {
"chat.message": async (
input: ChatMessageInput,
output: ChatMessageOutput
): Promise<void> => {
const result = injectPendingContext(collector, input.sessionID, output.parts)
if (result.injected) {
log("[context-injector] Injected pending context via chat.message", {
sessionID: input.sessionID,
contextLength: result.contextLength,
})
}
},
}
}
interface MessageWithParts {
info: Message
parts: Part[]
}
type MessagesTransformHook = {
"experimental.chat.messages.transform"?: (
input: Record<string, never>,
output: { messages: MessageWithParts[] }
) => Promise<void>
}
export function createContextInjectorMessagesTransformHook(
collector: ContextCollector
): MessagesTransformHook {
return {
"experimental.chat.messages.transform": async (_input, output) => {
const { messages } = output
log("[DEBUG] experimental.chat.messages.transform called", {
messageCount: messages.length,
})
if (messages.length === 0) {
return
}
let lastUserMessageIndex = -1
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].info.role === "user") {
lastUserMessageIndex = i
break
}
}
if (lastUserMessageIndex === -1) {
log("[DEBUG] No user message found in messages")
return
}
const lastUserMessage = messages[lastUserMessageIndex]
// Try message.info.sessionID first, fallback to mainSessionID
const messageSessionID = (lastUserMessage.info as unknown as { sessionID?: string }).sessionID
const sessionID = messageSessionID ?? getMainSessionID()
log("[DEBUG] Extracted sessionID", {
messageSessionID,
mainSessionID: getMainSessionID(),
sessionID,
infoKeys: Object.keys(lastUserMessage.info),
})
if (!sessionID) {
log("[DEBUG] sessionID is undefined (both message.info and mainSessionID are empty)")
return
}
const hasPending = collector.hasPending(sessionID)
log("[DEBUG] Checking hasPending", {
sessionID,
hasPending,
})
if (!hasPending) {
return
}
const pending = collector.consume(sessionID)
if (!pending.hasContent) {
return
}
const textPartIndex = lastUserMessage.parts.findIndex(
(p) => p.type === "text" && (p as { text?: string }).text
)
if (textPartIndex === -1) {
log("[context-injector] No text part found in last user message, skipping injection", {
sessionID,
partsCount: lastUserMessage.parts.length,
})
return
}
// synthetic part pattern (minimal fields)
const syntheticPart = {
id: `synthetic_hook_${Date.now()}`,
messageID: lastUserMessage.info.id,
sessionID: (lastUserMessage.info as { sessionID?: string }).sessionID ?? "",
type: "text" as const,
text: pending.merged,
synthetic: true, // hidden in UI
}
lastUserMessage.parts.splice(textPartIndex, 0, syntheticPart as Part)
log("[context-injector] Inserted synthetic part with hook content", {
sessionID,
contentLength: pending.merged.length,
})
},
}
}