feat(todo-continuation-enforcer): implement countdown toast notification
Implement countdown toast feature showing visual feedback before todo continuation: - Changed from 5-second timeout to interval-based countdown - Shows toast every second: "Resuming in 5s...", "Resuming in 4s...", etc. - Toast duration set to 900ms to prevent overlap - Countdown cancels on user message, session error, or session deletion 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -61,12 +61,20 @@ function detectInterrupt(error: unknown): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const COUNTDOWN_SECONDS = 5
|
||||||
|
const TOAST_DURATION_MS = 900 // Slightly less than 1s so toasts don't overlap
|
||||||
|
|
||||||
|
interface CountdownState {
|
||||||
|
secondsRemaining: number
|
||||||
|
intervalId: ReturnType<typeof setInterval>
|
||||||
|
}
|
||||||
|
|
||||||
export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuationEnforcer {
|
export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuationEnforcer {
|
||||||
const remindedSessions = new Set<string>()
|
const remindedSessions = new Set<string>()
|
||||||
const interruptedSessions = new Set<string>()
|
const interruptedSessions = new Set<string>()
|
||||||
const errorSessions = new Set<string>()
|
const errorSessions = new Set<string>()
|
||||||
const recoveringSessions = new Set<string>()
|
const recoveringSessions = new Set<string>()
|
||||||
const pendingTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
const pendingCountdowns = new Map<string, CountdownState>()
|
||||||
|
|
||||||
const markRecovering = (sessionID: string): void => {
|
const markRecovering = (sessionID: string): void => {
|
||||||
recoveringSessions.add(sessionID)
|
recoveringSessions.add(sessionID)
|
||||||
@@ -89,11 +97,10 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
|
|||||||
}
|
}
|
||||||
log(`[${HOOK_NAME}] session.error received`, { sessionID, isInterrupt, error: props?.error })
|
log(`[${HOOK_NAME}] session.error received`, { sessionID, isInterrupt, error: props?.error })
|
||||||
|
|
||||||
// Cancel pending continuation if error occurs
|
const countdown = pendingCountdowns.get(sessionID)
|
||||||
const timer = pendingTimers.get(sessionID)
|
if (countdown) {
|
||||||
if (timer) {
|
clearInterval(countdown.intervalId)
|
||||||
clearTimeout(timer)
|
pendingCountdowns.delete(sessionID)
|
||||||
pendingTimers.delete(sessionID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -105,17 +112,27 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
|
|||||||
|
|
||||||
log(`[${HOOK_NAME}] session.idle received`, { sessionID })
|
log(`[${HOOK_NAME}] session.idle received`, { sessionID })
|
||||||
|
|
||||||
// Cancel any existing timer to debounce
|
const existingCountdown = pendingCountdowns.get(sessionID)
|
||||||
const existingTimer = pendingTimers.get(sessionID)
|
if (existingCountdown) {
|
||||||
if (existingTimer) {
|
clearInterval(existingCountdown.intervalId)
|
||||||
clearTimeout(existingTimer)
|
pendingCountdowns.delete(sessionID)
|
||||||
log(`[${HOOK_NAME}] Cancelled existing timer`, { sessionID })
|
log(`[${HOOK_NAME}] Cancelled existing countdown`, { sessionID })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule continuation check
|
const showCountdownToast = async (seconds: number): Promise<void> => {
|
||||||
const timer = setTimeout(async () => {
|
await ctx.client.tui.showToast({
|
||||||
pendingTimers.delete(sessionID)
|
body: {
|
||||||
log(`[${HOOK_NAME}] Timer fired, checking conditions`, { sessionID })
|
title: "Todo Continuation",
|
||||||
|
message: `Resuming in ${seconds}s...`,
|
||||||
|
variant: "warning" as const,
|
||||||
|
duration: TOAST_DURATION_MS,
|
||||||
|
},
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const executeAfterCountdown = async (): Promise<void> => {
|
||||||
|
pendingCountdowns.delete(sessionID)
|
||||||
|
log(`[${HOOK_NAME}] Countdown finished, checking conditions`, { sessionID })
|
||||||
|
|
||||||
// Check if session is in recovery mode - if so, skip entirely without clearing state
|
// Check if session is in recovery mode - if so, skip entirely without clearing state
|
||||||
if (recoveringSessions.has(sessionID)) {
|
if (recoveringSessions.has(sessionID)) {
|
||||||
@@ -206,9 +223,32 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
|
|||||||
log(`[${HOOK_NAME}] Prompt injection failed`, { sessionID, error: String(err) })
|
log(`[${HOOK_NAME}] Prompt injection failed`, { sessionID, error: String(err) })
|
||||||
remindedSessions.delete(sessionID)
|
remindedSessions.delete(sessionID)
|
||||||
}
|
}
|
||||||
}, 5000)
|
}
|
||||||
|
|
||||||
pendingTimers.set(sessionID, timer)
|
let secondsRemaining = COUNTDOWN_SECONDS
|
||||||
|
showCountdownToast(secondsRemaining).catch(() => {})
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
secondsRemaining--
|
||||||
|
|
||||||
|
if (secondsRemaining <= 0) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
pendingCountdowns.delete(sessionID)
|
||||||
|
executeAfterCountdown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const countdown = pendingCountdowns.get(sessionID)
|
||||||
|
if (!countdown) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
countdown.secondsRemaining = secondsRemaining
|
||||||
|
showCountdownToast(secondsRemaining).catch(() => {})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
pendingCountdowns.set(sessionID, { secondsRemaining, intervalId })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "message.updated") {
|
if (event.type === "message.updated") {
|
||||||
@@ -217,12 +257,11 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
|
|||||||
log(`[${HOOK_NAME}] message.updated received`, { sessionID, role: info?.role })
|
log(`[${HOOK_NAME}] message.updated received`, { sessionID, role: info?.role })
|
||||||
|
|
||||||
if (sessionID && info?.role === "user") {
|
if (sessionID && info?.role === "user") {
|
||||||
// Cancel pending continuation on user interaction (real user input)
|
const countdown = pendingCountdowns.get(sessionID)
|
||||||
const timer = pendingTimers.get(sessionID)
|
if (countdown) {
|
||||||
if (timer) {
|
clearInterval(countdown.intervalId)
|
||||||
clearTimeout(timer)
|
pendingCountdowns.delete(sessionID)
|
||||||
pendingTimers.delete(sessionID)
|
log(`[${HOOK_NAME}] Cancelled countdown on user message`, { sessionID })
|
||||||
log(`[${HOOK_NAME}] Cancelled pending timer on user message`, { sessionID })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,11 +280,10 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
|
|||||||
errorSessions.delete(sessionInfo.id)
|
errorSessions.delete(sessionInfo.id)
|
||||||
recoveringSessions.delete(sessionInfo.id)
|
recoveringSessions.delete(sessionInfo.id)
|
||||||
|
|
||||||
// Cancel pending continuation
|
const countdown = pendingCountdowns.get(sessionInfo.id)
|
||||||
const timer = pendingTimers.get(sessionInfo.id)
|
if (countdown) {
|
||||||
if (timer) {
|
clearInterval(countdown.intervalId)
|
||||||
clearTimeout(timer)
|
pendingCountdowns.delete(sessionInfo.id)
|
||||||
pendingTimers.delete(sessionInfo.id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user