feat(keyword-detector): show toast notification when ultrawork mode is activated (#393)

* feat(keyword-detector): show toast notification when ultrawork mode is activated

When users trigger ultrawork mode (via 'ultrawork' or 'ulw' keywords), a toast
notification now appears to confirm the mode is active. The notification is
shown once per session to avoid spamming.

Changes:
- Add detectKeywordsWithType() to identify which keyword type triggered
- Show 'Ultrawork Mode Activated' toast with success variant
- Track notified sessions to prevent duplicate toasts

Closes #392

* fix(keyword-detector): fix index bug in detectKeywordsWithType and add error logging

- Fix P1: detectKeywordsWithType now maps before filtering to preserve
  original KEYWORD_DETECTORS indices. Previously, the index used was from
  the filtered array, causing incorrect type assignment (e.g., 'search'
  match would incorrectly return 'ultrawork' type).

- Fix P2: Replace silent .catch(() => {}) with proper error logging using
  the log function for easier debugging when toast notifications fail.

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
Sisyphus
2026-01-01 20:58:02 +09:00
committed by GitHub
parent 1c55385cb5
commit f66c886e0d
3 changed files with 39 additions and 4 deletions

View File

@@ -4,6 +4,11 @@ import {
INLINE_CODE_PATTERN,
} from "./constants"
export interface DetectedKeyword {
type: "ultrawork" | "search" | "analyze"
message: string
}
export function removeCodeBlocks(text: string): string {
return text.replace(CODE_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "")
}
@@ -15,6 +20,18 @@ export function detectKeywords(text: string): string[] {
).map(({ message }) => message)
}
export function detectKeywordsWithType(text: string): DetectedKeyword[] {
const textWithoutCode = removeCodeBlocks(text)
const types: Array<"ultrawork" | "search" | "analyze"> = ["ultrawork", "search", "analyze"]
return KEYWORD_DETECTORS.map(({ pattern, message }, index) => ({
matches: pattern.test(textWithoutCode),
type: types[index],
message,
}))
.filter((result) => result.matches)
.map(({ type, message }) => ({ type, message }))
}
export function extractPromptText(
parts: Array<{ type: string; text?: string }>
): string {

View File

@@ -1,4 +1,5 @@
import { detectKeywords, extractPromptText } from "./detector"
import type { PluginInput } from "@opencode-ai/plugin"
import { detectKeywords, detectKeywordsWithType, extractPromptText } from "./detector"
import { log } from "../../shared"
import { injectHookMessage } from "../../features/hook-message-injector"
@@ -7,8 +8,9 @@ export * from "./constants"
export * from "./types"
const sessionFirstMessageProcessed = new Set<string>()
const sessionUltraworkNotified = new Set<string>()
export function createKeywordDetectorHook() {
export function createKeywordDetectorHook(ctx: PluginInput) {
return {
"chat.message": async (
input: {
@@ -26,12 +28,28 @@ export function createKeywordDetectorHook() {
sessionFirstMessageProcessed.add(input.sessionID)
const promptText = extractPromptText(output.parts)
const messages = detectKeywords(promptText)
const detectedKeywords = detectKeywordsWithType(promptText)
const messages = detectedKeywords.map((k) => k.message)
if (messages.length === 0) {
return
}
const hasUltrawork = detectedKeywords.some((k) => k.type === "ultrawork")
if (hasUltrawork && !sessionUltraworkNotified.has(input.sessionID)) {
sessionUltraworkNotified.add(input.sessionID)
log(`[keyword-detector] Ultrawork mode activated`, { sessionID: input.sessionID })
ctx.client.tui.showToast({
body: {
title: "Ultrawork Mode Activated",
message: "Maximum precision engaged. All agents at your disposal.",
variant: "success" as const,
duration: 3000,
},
}).catch((err) => log(`[keyword-detector] Failed to show toast`, { error: err, sessionID: input.sessionID }))
}
const context = messages.join("\n")
// First message: transform parts directly (for title generation compatibility)

View File

@@ -237,7 +237,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
})
: null;
const keywordDetector = isHookEnabled("keyword-detector")
? createKeywordDetectorHook()
? createKeywordDetectorHook(ctx)
: null;
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
? createAgentUsageReminderHook(ctx)