@mohak34/opencode-notifier 0.1.22-beta.0 → 0.1.22-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +112 -74
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3894,9 +3894,6 @@ function getMessage(config, event) {
3894
3894
  function getSoundPath(config, event) {
3895
3895
  return config.sounds[event];
3896
3896
  }
3897
- function getSoundVolume(config, event) {
3898
- return config.volumes[event];
3899
- }
3900
3897
  function getIconPath(config) {
3901
3898
  if (!config.showIcon) {
3902
3899
  return;
@@ -3911,16 +3908,6 @@ function getIconPath(config) {
3911
3908
  } catch {}
3912
3909
  return;
3913
3910
  }
3914
- function interpolateMessage(message, context) {
3915
- let result = message;
3916
- const sessionTitle = context.sessionTitle || "";
3917
- result = result.replaceAll("{sessionTitle}", sessionTitle);
3918
- const projectName = context.projectName || "";
3919
- result = result.replaceAll("{projectName}", projectName);
3920
- result = result.replace(/\s*[:\-|]\s*$/, "").trim();
3921
- result = result.replace(/\s{2,}/g, " ");
3922
- return result;
3923
- }
3924
3911
 
3925
3912
  // src/notify.ts
3926
3913
  var import_node_notifier = __toESM(require_node_notifier(), 1);
@@ -4127,12 +4114,26 @@ function runCommand2(config, event, message, sessionTitle, projectName) {
4127
4114
  }
4128
4115
 
4129
4116
  // src/index.ts
4130
- var recentErrors = new Map;
4117
+ var IDLE_COMPLETE_DELAY_MS = 350;
4118
+ var pendingIdleTimers = new Map;
4119
+ var sessionIdleSequence = new Map;
4120
+ var sessionErrorSuppressionAt = new Map;
4121
+ var sessionLastBusyAt = new Map;
4131
4122
  setInterval(() => {
4132
4123
  const cutoff = Date.now() - 5 * 60 * 1000;
4133
- for (const [sessionID, timestamp] of recentErrors) {
4124
+ for (const [sessionID] of sessionIdleSequence) {
4125
+ if (!pendingIdleTimers.has(sessionID)) {
4126
+ sessionIdleSequence.delete(sessionID);
4127
+ }
4128
+ }
4129
+ for (const [sessionID, timestamp] of sessionErrorSuppressionAt) {
4134
4130
  if (timestamp < cutoff) {
4135
- recentErrors.delete(sessionID);
4131
+ sessionErrorSuppressionAt.delete(sessionID);
4132
+ }
4133
+ }
4134
+ for (const [sessionID, timestamp] of sessionLastBusyAt) {
4135
+ if (timestamp < cutoff) {
4136
+ sessionLastBusyAt.delete(sessionID);
4136
4137
  }
4137
4138
  }
4138
4139
  }, 5 * 60 * 1000);
@@ -4142,13 +4143,9 @@ function getNotificationTitle(config, projectName) {
4142
4143
  }
4143
4144
  return "OpenCode";
4144
4145
  }
4145
- async function handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle) {
4146
+ async function handleEvent(config, eventType, projectName, elapsedSeconds) {
4146
4147
  const promises = [];
4147
- const rawMessage = getMessage(config, eventType);
4148
- const message = interpolateMessage(rawMessage, {
4149
- sessionTitle: config.showSessionTitle ? sessionTitle : null,
4150
- projectName
4151
- });
4148
+ const message = getMessage(config, eventType);
4152
4149
  if (isEventNotificationEnabled(config, eventType)) {
4153
4150
  const title = getNotificationTitle(config, projectName);
4154
4151
  const iconPath = getIconPath(config);
@@ -4156,13 +4153,12 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds, sessi
4156
4153
  }
4157
4154
  if (isEventSoundEnabled(config, eventType)) {
4158
4155
  const customSoundPath = getSoundPath(config, eventType);
4159
- const soundVolume = getSoundVolume(config, eventType);
4160
- promises.push(playSound(eventType, customSoundPath, soundVolume));
4156
+ promises.push(playSound(eventType, customSoundPath, 1));
4161
4157
  }
4162
4158
  const minDuration = config.command?.minDuration;
4163
4159
  const shouldSkipCommand = typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
4164
4160
  if (!shouldSkipCommand) {
4165
- runCommand2(config, eventType, message, sessionTitle, projectName);
4161
+ runCommand2(config, eventType, message);
4166
4162
  }
4167
4163
  await Promise.allSettled(promises);
4168
4164
  }
@@ -4173,6 +4169,50 @@ function getSessionIDFromEvent(event) {
4173
4169
  }
4174
4170
  return null;
4175
4171
  }
4172
+ function clearPendingIdleTimer(sessionID) {
4173
+ const timer = pendingIdleTimers.get(sessionID);
4174
+ if (!timer) {
4175
+ return;
4176
+ }
4177
+ clearTimeout(timer);
4178
+ pendingIdleTimers.delete(sessionID);
4179
+ }
4180
+ function bumpSessionIdleSequence(sessionID) {
4181
+ const nextSequence = (sessionIdleSequence.get(sessionID) ?? 0) + 1;
4182
+ sessionIdleSequence.set(sessionID, nextSequence);
4183
+ return nextSequence;
4184
+ }
4185
+ function hasCurrentSessionIdleSequence(sessionID, sequence) {
4186
+ return sessionIdleSequence.get(sessionID) === sequence;
4187
+ }
4188
+ function markSessionError(sessionID) {
4189
+ if (!sessionID) {
4190
+ return;
4191
+ }
4192
+ sessionErrorSuppressionAt.set(sessionID, Date.now());
4193
+ bumpSessionIdleSequence(sessionID);
4194
+ clearPendingIdleTimer(sessionID);
4195
+ }
4196
+ function markSessionBusy(sessionID) {
4197
+ const now = Date.now();
4198
+ sessionLastBusyAt.set(sessionID, now);
4199
+ sessionErrorSuppressionAt.delete(sessionID);
4200
+ bumpSessionIdleSequence(sessionID);
4201
+ clearPendingIdleTimer(sessionID);
4202
+ }
4203
+ function shouldSuppressSessionIdle(sessionID) {
4204
+ const errorAt = sessionErrorSuppressionAt.get(sessionID);
4205
+ if (errorAt === undefined) {
4206
+ return false;
4207
+ }
4208
+ const busyAt = sessionLastBusyAt.get(sessionID);
4209
+ if (typeof busyAt === "number" && busyAt > errorAt) {
4210
+ sessionErrorSuppressionAt.delete(sessionID);
4211
+ return false;
4212
+ }
4213
+ sessionErrorSuppressionAt.delete(sessionID);
4214
+ return true;
4215
+ }
4176
4216
  async function getElapsedSinceLastPrompt(client, sessionID, nowMs = Date.now()) {
4177
4217
  try {
4178
4218
  const response = await client.session.messages({ path: { id: sessionID } });
@@ -4192,33 +4232,55 @@ async function getElapsedSinceLastPrompt(client, sessionID, nowMs = Date.now())
4192
4232
  } catch {}
4193
4233
  return null;
4194
4234
  }
4195
- async function getSessionInfo(client, sessionID) {
4235
+ async function isChildSession(client, sessionID) {
4196
4236
  try {
4197
4237
  const response = await client.session.get({ path: { id: sessionID } });
4198
- return {
4199
- isChild: !!response.data?.parentID,
4200
- title: response.data?.title ?? null
4201
- };
4238
+ const parentID = response.data?.parentID;
4239
+ return !!parentID;
4202
4240
  } catch {
4203
- return { isChild: false, title: null };
4241
+ return false;
4242
+ }
4243
+ }
4244
+ async function processSessionIdle(client, config, projectName, event, sessionID, sequence, idleReceivedAtMs) {
4245
+ if (!hasCurrentSessionIdleSequence(sessionID, sequence)) {
4246
+ return;
4204
4247
  }
4248
+ if (shouldSuppressSessionIdle(sessionID)) {
4249
+ return;
4250
+ }
4251
+ const isChild = await isChildSession(client, sessionID);
4252
+ if (!hasCurrentSessionIdleSequence(sessionID, sequence)) {
4253
+ return;
4254
+ }
4255
+ if (!isChild) {
4256
+ await handleEventWithElapsedTime(client, config, "complete", projectName, event, idleReceivedAtMs);
4257
+ return;
4258
+ }
4259
+ await handleEventWithElapsedTime(client, config, "subagent_complete", projectName, event, idleReceivedAtMs);
4260
+ }
4261
+ function scheduleSessionIdle(client, config, projectName, event, sessionID) {
4262
+ clearPendingIdleTimer(sessionID);
4263
+ const sequence = bumpSessionIdleSequence(sessionID);
4264
+ const idleReceivedAtMs = Date.now();
4265
+ const timer = setTimeout(() => {
4266
+ pendingIdleTimers.delete(sessionID);
4267
+ processSessionIdle(client, config, projectName, event, sessionID, sequence, idleReceivedAtMs).catch(() => {
4268
+ return;
4269
+ });
4270
+ }, IDLE_COMPLETE_DELAY_MS);
4271
+ pendingIdleTimers.set(sessionID, timer);
4205
4272
  }
4206
- async function handleEventWithElapsedTime(client, config, eventType, projectName, event, preloadedSessionTitle) {
4207
- const sessionID = getSessionIDFromEvent(event);
4273
+ async function handleEventWithElapsedTime(client, config, eventType, projectName, event, elapsedReferenceNowMs) {
4274
+ const minDuration = config.command?.minDuration;
4275
+ const shouldLookupElapsed = !!config.command?.enabled && typeof config.command?.path === "string" && config.command.path.length > 0 && typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0;
4208
4276
  let elapsedSeconds = null;
4209
- let sessionTitle = preloadedSessionTitle ?? null;
4210
- if (sessionID) {
4211
- const minDuration = config.command?.minDuration;
4212
- const shouldLookupElapsed = !!config.command?.enabled && typeof config.command?.path === "string" && config.command.path.length > 0 && typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0;
4213
- if (shouldLookupElapsed) {
4214
- elapsedSeconds = await getElapsedSinceLastPrompt(client, sessionID);
4215
- }
4216
- if (!sessionTitle && config.showSessionTitle) {
4217
- const info = await getSessionInfo(client, sessionID);
4218
- sessionTitle = info.title;
4277
+ if (shouldLookupElapsed) {
4278
+ const sessionID = getSessionIDFromEvent(event);
4279
+ if (sessionID) {
4280
+ elapsedSeconds = await getElapsedSinceLastPrompt(client, sessionID, elapsedReferenceNowMs);
4219
4281
  }
4220
4282
  }
4221
- await handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle);
4283
+ await handleEvent(config, eventType, projectName, elapsedSeconds);
4222
4284
  }
4223
4285
  var NotifierPlugin = async ({ client, directory }) => {
4224
4286
  const config = loadConfig();
@@ -4234,41 +4296,17 @@ var NotifierPlugin = async ({ client, directory }) => {
4234
4296
  if (event.type === "session.idle") {
4235
4297
  const sessionID = getSessionIDFromEvent(event);
4236
4298
  if (sessionID) {
4237
- const errorTime = recentErrors.get(sessionID);
4238
- if (errorTime && Date.now() - errorTime < 500) {
4239
- recentErrors.delete(sessionID);
4240
- const sessionInfo = await getSessionInfo(client, sessionID);
4241
- await handleEventWithElapsedTime(client, config, "interrupted", projectName, event, sessionInfo.title);
4242
- } else {
4243
- const sessionInfo = await getSessionInfo(client, sessionID);
4244
- if (!sessionInfo.isChild) {
4245
- await handleEventWithElapsedTime(client, config, "complete", projectName, event, sessionInfo.title);
4246
- } else {
4247
- await handleEventWithElapsedTime(client, config, "subagent_complete", projectName, event, sessionInfo.title);
4248
- }
4249
- }
4299
+ scheduleSessionIdle(client, config, projectName, event, sessionID);
4250
4300
  } else {
4251
4301
  await handleEventWithElapsedTime(client, config, "complete", projectName, event);
4252
4302
  }
4253
4303
  }
4304
+ if (event.type === "session.status" && event.properties.status.type === "busy") {
4305
+ markSessionBusy(event.properties.sessionID);
4306
+ }
4254
4307
  if (event.type === "session.error") {
4255
- const sessionID = getSessionIDFromEvent(event);
4256
- if (sessionID) {
4257
- recentErrors.set(sessionID, Date.now());
4258
- let sessionTitle = null;
4259
- if (config.showSessionTitle) {
4260
- const info = await getSessionInfo(client, sessionID);
4261
- sessionTitle = info.title;
4262
- }
4263
- setTimeout(() => {
4264
- if (recentErrors.has(sessionID)) {
4265
- recentErrors.delete(sessionID);
4266
- handleEventWithElapsedTime(client, config, "error", projectName, event, sessionTitle);
4267
- }
4268
- }, 100);
4269
- } else {
4270
- await handleEventWithElapsedTime(client, config, "error", projectName, event);
4271
- }
4308
+ markSessionError(getSessionIDFromEvent(event));
4309
+ await handleEventWithElapsedTime(client, config, "error", projectName, event);
4272
4310
  }
4273
4311
  },
4274
4312
  "permission.ask": async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohak34/opencode-notifier",
3
- "version": "0.1.22-beta.0",
3
+ "version": "0.1.22-beta.2",
4
4
  "description": "OpenCode plugin that sends system notifications and plays sounds when permission is needed, generation completes, or errors occur",
5
5
  "author": "mohak34",
6
6
  "license": "MIT",