Browse Source

Share reminder transform helpers

DanMaly 2 weeks ago
parent
commit
fcd106c4a6
4 changed files with 38 additions and 45 deletions
  1. 13 27
      src/hooks/phase-reminder/index.ts
  2. 12 18
      src/hooks/post-file-tool-nudge/index.ts
  3. 12 0
      src/hooks/types.ts
  4. 1 0
      src/index.ts

+ 13 - 27
src/hooks/phase-reminder/index.ts

@@ -8,12 +8,20 @@
 import { PHASE_REMINDER } from '../../config/constants';
 import { isInternalInitiatorPart } from '../../utils';
 import { isRecord } from '../../utils/guards';
-import { isUserMessageWithParts } from '../types';
+import { findLatestUserMessage, type MessagePart } from '../types';
 
 export { PHASE_REMINDER };
 
 export const PHASE_REMINDER_METADATA_KEY = 'oh-my-opencode-slim.phaseReminder';
 
+export function hasPhaseReminder(part: MessagePart): boolean {
+  return (
+    part.synthetic === true &&
+    isRecord(part.metadata) &&
+    part.metadata[PHASE_REMINDER_METADATA_KEY] === true
+  );
+}
+
 interface PhaseReminderOptions {
   shouldInject?: (sessionID: string) => boolean;
 }
@@ -31,24 +39,8 @@ export function createPhaseReminderHook(options: PhaseReminderOptions = {}) {
     ): Promise<void> => {
       const messages = Array.isArray(output.messages) ? output.messages : [];
 
-      if (messages.length === 0) {
-        return;
-      }
-
-      let lastUserMessageIndex = -1;
-      for (let i = messages.length - 1; i >= 0; i--) {
-        if (isUserMessageWithParts(messages[i])) {
-          lastUserMessageIndex = i;
-          break;
-        }
-      }
-
-      if (lastUserMessageIndex === -1) {
-        return;
-      }
-
-      const lastUserMessage = messages[lastUserMessageIndex];
-      if (!isUserMessageWithParts(lastUserMessage)) {
+      const lastUserMessage = findLatestUserMessage(messages);
+      if (!lastUserMessage) {
         return;
       }
 
@@ -73,17 +65,11 @@ export function createPhaseReminderHook(options: PhaseReminderOptions = {}) {
       if (isInternalInitiatorPart(originalPart)) {
         return;
       }
-      if (
-        lastUserMessage.parts.some(
-          (part) =>
-            part.synthetic === true &&
-            isRecord(part.metadata) &&
-            part.metadata[PHASE_REMINDER_METADATA_KEY] === true,
-        )
-      ) {
+      if (lastUserMessage.parts.some(hasPhaseReminder)) {
         return;
       }
 
+      // post-file-tool-nudge must run first so its tagged part deduplicates.
       // Append reminder as a new, separate message part instead of mutating
       // the user-authored text. This prevents the reminder from leaking into
       // the UI display and chat history (issue #448).

+ 12 - 18
src/hooks/post-file-tool-nudge/index.ts

@@ -8,10 +8,16 @@
 
 import { PHASE_REMINDER } from '../../config/constants';
 import { isInternalInitiatorPart } from '../../utils';
-import { isRecord } from '../../utils/guards';
-import { PHASE_REMINDER_METADATA_KEY } from '../phase-reminder';
+import {
+  hasPhaseReminder,
+  PHASE_REMINDER_METADATA_KEY,
+} from '../phase-reminder';
 import type { SessionLifecycle } from '../session-lifecycle';
-import { isUserMessageWithParts, type MessageWithParts } from '../types';
+import {
+  findLatestUserMessage,
+  isUserMessageWithParts,
+  type MessageWithParts,
+} from '../types';
 
 const FILE_TOOLS = new Set(['Read', 'read', 'Write', 'write']);
 
@@ -46,26 +52,13 @@ export function createPostFileToolNudgeHook(
       }
 
       const messages = Array.isArray(output.messages) ? output.messages : [];
-      let lastUserMessage: unknown;
-      for (let index = messages.length - 1; index >= 0; index--) {
-        if (isUserMessageWithParts(messages[index])) {
-          lastUserMessage = messages[index];
-          break;
-        }
-      }
-
-      const eligible = getEligibleMessage(lastUserMessage);
+      const eligible = getEligibleMessage(findLatestUserMessage(messages));
       if (!eligible) {
         return;
       }
       const { message, sessionID } = eligible;
 
-      const hasReminder = message.parts.some(
-        (part) =>
-          part.synthetic === true &&
-          isRecord(part.metadata) &&
-          part.metadata[PHASE_REMINDER_METADATA_KEY] === true,
-      );
+      const hasReminder = message.parts.some(hasPhaseReminder);
       if (!coordinator.consumePending(sessionID)) {
         return;
       }
@@ -75,6 +68,7 @@ export function createPostFileToolNudgeHook(
       ) {
         return;
       }
+      // This transform must run before phase-reminder so this metadata deduplicates.
       message.parts.push({
         type: 'text',
         synthetic: true,

+ 12 - 0
src/hooks/types.ts

@@ -46,3 +46,15 @@ export function isUserMessageWithParts(
 ): message is MessageWithParts {
   return isMessageWithParts(message) && message.info.role === 'user';
 }
+
+export function findLatestUserMessage(
+  messages: unknown[],
+): MessageWithParts | undefined {
+  for (let index = messages.length - 1; index >= 0; index--) {
+    const message = messages[index];
+    if (isUserMessageWithParts(message)) {
+      return message;
+    }
+  }
+  return undefined;
+}

+ 1 - 0
src/index.ts

@@ -1195,6 +1195,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         log,
       });
 
+      // Post-file metadata must be added before phase-reminder deduplicates it.
       await postFileToolNudge['experimental.chat.messages.transform'](
         input as never,
         typedOutput as never,