Skip to content

Nudge never fires in interactive TUI sessions despite E2E passing #4

@tbrandenburg

Description

@tbrandenburg

Summary

The plugin works in E2E tests but never sends a nudge in real interactive (TUI) sessions. The root cause is a combination of two issues: lastUserMessage is never populated in TUI sessions, and the two-phase idle fallback requires a second session.idle event that OpenCode never emits.

Root Cause Analysis

1. chat.message hook does not fire for TUI-originated user messages

index.ts registers a chat.message hook to record lastUserMessage:

"chat.message": (messageInput) => {
  handleUserMessage({ sessionID: messageInput.sessionID })
  return Promise.resolve()
},

In practice, when a user types into the TUI and presses Enter, OpenCode does not invoke the plugin's chat.message hook. As a result, lastUserMessage stays at 0 for the entire interactive session.

2. The two-phase fallback requires a second session.idle event that never arrives

Because lastUserMessage === 0, idle-handler.ts falls into the two-phase branch:

} else {
  if (state.lastIdleSeen === 0) {
    state.lastIdleSeen = now   // records first idle
    return                     // waits for a second one
  }
  if (now - state.lastIdleSeen < getIdleThreshold()) return
}

This requires OpenCode to emit a second session.idle event separated by at least IDLE_THRESHOLD (default 5 min) from the first. OpenCode only fires session.idle once per idle transition — no second event ever arrives, so the nudge is never sent.

3. Why E2E passes

The E2E test sets OPENCODE_IDLE_THRESHOLD_MS=0. With a zero threshold, the check now - state.lastIdleSeen < getIdleThreshold() is always false, so the first idle event immediately passes through — regardless of lastUserMessage being unset. The bug is masked entirely by the test's environment override.

Reproduction

  1. Build and install the plugin normally (make install && make build-plugin)
  2. Start an interactive opencode session (TUI)
  3. Send a message and wait — the AI responds and the session goes idle
  4. Wait 5+ minutes
  5. Observe: no nudge is ever injected

Expected Behaviour

After the configured idle threshold elapses following the AI's response, the plugin injects the continuation prompt.

Proposed Fix

Replace the chat.message hook approach with listening to session.status events (already flowing through the existing event handler). A session.status event with status.type === "running" fires reliably every time the AI starts processing a new user message — making it a correct proxy for "user sent a message":

if (event.type === "session.status" && event.properties.status.type === "running") {
  handleUserMessage({ sessionID: event.properties.sessionID })
}

This removes the dependency on chat.message firing and makes the single-phase path (lastUserMessage > 0) work correctly in all contexts including the TUI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions