Skip to content

Conversation

@ramarivera
Copy link

@ramarivera ramarivera commented Jan 4, 2026

Summary

This PR aligns the plugin with OpenCode version 1.0.223.

Key Changes

  • v1.0.223 Alignment:
    • Uses permission.updated (standard broadcast event in v1.0.223) instead of permission.asked.
    • Supports the modern session.status (with type: "idle") event.
    • Downgraded @opencode-ai/plugin devDependency to 1.0.223.
  • Subagent Support: Includes automatic detection of subagent tasks via parentID.
  • Reliability Improvements: Enhanced debouncing for race conditions and session title templating.
  • Enhanced Configuration: Support for JSONC, volume control, and custom notification images.

Documentation & Tests

  • Updated README.md with configuration options and debugging instructions.
  • All unit tests verified passing.

🤖 This content was generated with AI assistance using Gemini 2.0 Pro.

- Replace deprecated permission.updated with permission.asked
- Replace deprecated session.idle with session.status type check
- Update @opencode-ai/plugin to latest (1.0.224)
- Add debug logging support via OPENCODE_NOTIFIER_DEBUG env var
- Bump version to 0.1.8
- Update README with debugging section and technical notes

Event migrations based on OpenCode core source analysis:
- permission.asked: packages/opencode/src/permission/next.ts
- session.status: packages/opencode/src/session/status.ts

Fixes: notifications not triggering on macOS
Related: mohak34#3

Files modified:
- src/index.ts
- package.json
- bun.lock
- README.md
- Log all events to ./opencode_notifier_logs.jsonl
- Log plugin initialization with full config
- Log each event received with full payload
- Log handleEvent calls with config values (messages, sounds, enabled states)
- Helps debug why custom messages/sounds aren't being used

Each log line is a JSON object with timestamp for easy analysis.

Files modified:
- src/index.ts
- Remove all console.log and DEBUG env var usage
- File logging to opencode_notifier_logs.jsonl is sufficient
- console.log breaks because plugins can't write to stdout in OpenCode

Files modified:
- src/index.ts
- Move DEBUG check to module level
- Return early in logEvent if DEBUG is false
- Prevents log file creation unless explicitly debugging
- Avoids performance overhead in production use

Files modified:
- src/index.ts
- Document that logging only happens with DEBUG=true
- Show example JSONL output
- Explain what gets logged (init, events, handleEvent calls)
- Note that no log file is created without DEBUG flag

Files modified:
- README.md
…onl)

- Change opencode_notifier_logs.jsonl -> .opencode_notifier_logs.jsonl
- Hidden file is more appropriate for debug logs
- Matches conventions for hidden config/log files

Files modified:
- src/index.ts
- Add logConfigEvent() helper to avoid code duplication
- Move DEBUG and LOG_FILE to module level
- Cleaner, more maintainable logging in loadConfig()
- Logs: config path, exists check, file read, parse result, errors

Files modified:
- src/config.ts
- Create src/debug-logging.ts with logEvent() and isDebugEnabled()
- Remove duplicated logging code from index.ts and config.ts
- Use 'unknown' instead of 'any' for type safety
- Single source of truth for DEBUG flag and LOG_FILE path

Files modified:
- src/debug-logging.ts (new)
- src/index.ts
- src/config.ts
- Add jsonc-parser@3.3.1 (matches OpenCode core version)
- Add comprehensive logging for parse steps
- Log parsed data type and structure
- Better error handling with type guards

The crash was unrelated to this change - OpenCode logs show
plugin loads successfully without errors.

Files modified:
- src/config.ts
- package.json
- bun.lock
The bundler was trying to inline jsonc-parser but breaking its
internal require() calls to './impl/format' etc. This caused
runtime crashes with MODULE_NOT_FOUND errors.

Solution: Mark both jsonc-parser and node-notifier as --external
so they are loaded from node_modules at runtime.

Files modified:
- package.json
- Replace jsonc-parser with confbox (UnJS, fully bundlable)
- Add volume config parameter (0.0-1.0, default 1.0)
- Implement volume control for macOS (afplay -v) and Linux (paplay, mpv, ffplay)
- Fix error+complete double-sound issue with error debounce (2s window)
- Remove --external flags from build (everything bundles cleanly now)
- Update config interface and helpers (getVolume)

Bundle size: 144KB (vs 11KB external, but zero external dependencies needed)
- Add images config section (permission, complete, error)
- Support contentImage on macOS (requires >= 10.9)
- Support icon on Windows and Linux
- Update config interface and getImagePath helper
- Pass image path through notification chain

Images are optional (default: null for all events)
Tested using icon parameter but it doesn't replace Terminal icon.
Only way to replace left icon is to fork terminal-notifier.
contentImage works well as preview image on the right side.
Refactor for testability:
- Export createNotifierPlugin() for config injection
- Export timeProvider for mocking Date.now()
- Add EventWithProperties type to avoid 'any'
- Remove all 'any' types from code

Tests added:
- error-debounce.test.ts: Tests error+complete race condition with fake timers
- notification-parameters.test.ts: Tests correct parameters passed to notify/sound
- invalid-config.test.ts: Tests plugin doesn't crash with edge cases

Coverage:
- Error debounce logic (2s window)
- Multiple errors resetting debounce
- Notification/sound with correct messages, volume, images
- Minimal/null configs don't crash
- Unicode/emoji in messages
- Extreme volume values
- Long messages

All 15 tests passing ✅
The NotifierPlugin was incorrectly wrapped in an extra async arrow function.
Plugin type expects a function that returns Promise<Hooks>, not
async () => Promise<Hooks>.

Changed from:
  export const NotifierPlugin: Plugin = async () => createNotifierPlugin()

To:
  export const NotifierPlugin: Plugin = createNotifierPlugin

This fixes the 'fn3 is not a function' error when OpenCode loads the plugin.
Plugin type signature requires: (input: PluginInput) => Promise<Hooks>

Was: async () => createNotifierPlugin()  // Missing input param
Now: async (_input) => createNotifierPlugin()  // Correct signature
…e crash

- src/index.ts: Only exports default plugin function (what OpenCode loads)
- src/plugin.ts: Contains all implementation with test exports
- __tests__/*.test.ts: Import from plugin.ts instead of index.ts

Fixes TypeError: fn3 is not a function crash caused by OpenCode's plugin
loader attempting to call non-function exports (timeProvider object) as functions.

Files modified:
- src/index.ts (production entry - clean default export only)
- src/plugin.ts (implementation with test-accessible exports)
- __tests__/error-debounce.test.ts (import from plugin.ts)
- __tests__/invalid-config.test.ts (import from plugin.ts)
- __tests__/notification-parameters.test.ts (import from plugin.ts, fix typo)

All 15 tests passing. OpenCode starts successfully.
…ation)

- Add bidirectional debounce logic to handle both error→idle and idle→error sequences
- Reduce debounce window from 2000ms to 150ms (cancellation events fire quickly)
- Track both lastErrorTime and lastIdleTime with -1 sentinel values
- Skip idle if error happened within 150ms (original behavior)
- Skip error if idle happened within 150ms (NEW - fixes cancellation double-notification)

This fixes the issue where cancelling mid-flight causes both completion and
error notifications. When user cancels, OpenCode fires idle→error in quick
succession, so we now debounce in both directions.

Test updates:
- Add cancellation scenario tests (idle before error)
- Update debounce timing from 2s to 150ms across all tests
- Add fake timers to notification-parameters.test.ts to control timing

Files modified:
- src/plugin.ts (bidirectional debounce logic)
- __tests__/error-debounce.test.ts (new cancellation tests, updated timing)
- __tests__/notification-parameters.test.ts (fake timers for deterministic tests)

All 17 tests passing.
- Add 50ms delay before sending idle notifications to detect cancellation
- If error arrives during delay, skip idle notification entirely
- Result: When user cancels mid-flight, NO notifications are sent
- Reasoning: User already knows they cancelled, no feedback needed

Implementation:
- Idle event records timestamp immediately for error debouncing
- Waits 50ms before sending notification
- Checks if error arrived during wait - if so, skips notification
- Error event still skips if idle just happened (within 150ms)

User experience:
- Normal completion: Idle notification after 50ms delay
- Normal error: Error notification immediately
- Cancellation: No notifications (both events debounced out)

Test updates:
- Updated log message assertions to match new wording
- Tests still use fake timers and pass

All 17 tests passing.
- Replace timing-based cancellation detection with explicit cancellation flag
- When idle event delays 50ms, it stores a cancellation callback
- When error arrives, it calls the callback to cancel idle notification
- Result: Both notifications properly skipped on cancellation

Previous approach failed because:
- Plugin event handlers run concurrently (not sequentially)
- Error handler checked lastIdleTime before idle handler finished waiting
- This caused error to skip but idle still sent notification

New approach:
- Idle: Sets pendingIdleNotification flag, waits 50ms, checks flag before sending
- Error: Checks pendingIdleNotification flag, calls cancel callback if present
- Guarantees: Both notifications skipped when cancellation detected

All 17 tests passing.
- Add 'subagent' as a fourth event type alongside permission/complete/error
- Detect subagent sessions by checking session.parentID via OpenCode client
- Use subagent event config when session has parentID, complete otherwise
- Subagent event disabled by default (sound: false, notification: false)

Implementation:
- Pass PluginInput to plugin instance to access OpenCode client
- Fetch session info on idle events to check for parentID
- Route to 'subagent' event handler if parentID exists
- Graceful fallback to 'complete' if session lookup fails

Config structure:
- events.subagent: { sound, notification } (default: both false)
- messages.subagent: Custom message for subagent completions
- sounds.subagent: Custom sound path (optional)
- images.subagent: Custom notification icon (optional)

User experience:
- Main session completions: Use 'complete' event config
- Subagent task completions: Use 'subagent' event config (disabled by default)
- User can enable subagent notifications independently from main completions

Example config:
  "subagent": {
    "sound": false,
    "notification": false
  }

All 17 tests passing.
Add 6 new tests for subagent vs main session detection:

1. Main session (no parentID) → uses 'complete' event
2. Subagent session (has parentID) → uses 'subagent' event
3. Subagent disabled in config → no notification sent
4. Session lookup fails → fallback to 'complete' event
5. No pluginInput provided → default to 'complete' event
6. Multiple subagent completions → each handled independently

Test coverage:
- Mocks OpenCode client session.get() API
- Tests both enabled and disabled subagent configs
- Verifies correct event type routing (complete vs subagent)
- Validates proper notification parameters (message, sound, image)
- Tests error handling and fallback behavior

All 23 tests passing (17 original + 6 new).
- Sort imports (external before internal, types before values)
- Replace 'as any' with proper 'EventWithProperties' type
- Export EventWithProperties type from plugin for test usage

Fixes:
- Import order violations
- 7 'Unexpected any' type errors

All 23 tests still passing.
ramarivera and others added 5 commits January 4, 2026 02:15
- __tests__/fixtures/integration_logs.jsonl
- __tests__/integration-logs.test.ts

Verifies permission/subagent/complete/error notifications from real OpenCode 223 logs.
Confirms debounce race conditions handled correctly.
- Track lastIdleNotificationTime instead of lastIdleTime for error debouncing
- Error notifications are now skipped when they occur within 150ms AFTER
  the idle notification was sent, not when the idle event was received
- Add comprehensive test for user abort scenario (idle at t=0,
  notification at t=168, error at t=182)
- Fixes race condition where both complete and error notifications
  were sent when only idle should trigger

🤖 *This content was generated with AI assistance using Claude Sonnet 4.5.*
- __tests__/integration-logs.test.ts
- src/config.ts
- src/plugin.ts
- src/sound.ts
- tsconfig.json

Switch integration tests from Bun vitest API to Jest and clean up
redundant comments throughout the codebase.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- __mocks__/confbox.ts
- __tests__/fixtures/integration_logs.jsonl
- __tests__/integration-logs.test.ts
- jest.config.js

Add confbox mock to handle ESM-only dependency, fix integration test
to properly mock pluginInput for session lookup, fix malformed JSON
and session IDs in fixture file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@mohak34
Copy link
Owner

mohak34 commented Jan 4, 2026

Hey, thanks for this! I like the logging and testing improvements here.

Just pushed v0.1.9 with a fix for the notification issues. Once I've confirmed that's working well, I'll come back to this PR to implement the better logging and error handling. This should help debug any future issues.

Will follow up soon!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants