Browse Source

cleanup: extract shared message types to src/hooks/types.ts

Four files defined identical MessageInfo/MessagePart/MessageWithParts
types for the experimental.chat.messages.transform message shape.
Extract to a shared src/hooks/types.ts with all unioned fields as
optional extras (safe under structural typing).

- filter-available-skills, phase-reminder: replace local interfaces with imports
- image-hook: replace anonymous inline type with import
- task-session-manager: replace ChatMessage/ChatMessagePart interfaces
  with MessageWithParts/MessagePart aliases, then inline those aliases
  per oracle review feedback
- index.ts: replace verbose inline type assertion (10 lines) with
  one-liner using MessageWithParts

Also: keep collapseSystemInPlace extracted (oracle decision). The
mutation contract is subtle (PR #336 history) and the 7 regression
tests are valuable.
adikpb 1 month ago
parent
commit
9825291213

+ 1 - 16
src/hooks/filter-available-skills/index.ts

@@ -6,22 +6,7 @@
 import type { PluginInput } from '@opencode-ai/plugin';
 import { getSkillPermissionsForAgent } from '../../cli/skills';
 import { getAgentOverride, type PluginConfig } from '../../config';
-
-interface MessageInfo {
-  role: string;
-  agent?: string;
-}
-
-interface MessagePart {
-  type: string;
-  text?: string;
-  [key: string]: unknown;
-}
-
-interface MessageWithParts {
-  info: MessageInfo;
-  parts: MessagePart[];
-}
+import type { MessageWithParts } from '../types';
 
 const AVAILABLE_SKILLS_BLOCK_REGEX =
   /<available_skills>\s*([\s\S]*?)\s*<\/available_skills>/g;

+ 1 - 9
src/hooks/image-hook.ts

@@ -9,6 +9,7 @@ import {
   writeFileSync,
 } from 'node:fs';
 import { basename, extname, join } from 'node:path';
+import type { MessageWithParts } from './types';
 
 // Debounce: only run cleanup every 10 minutes per directory
 const lastCleanupByDir = new Map<string, number>();
@@ -23,15 +24,6 @@ interface ImagePart {
   [key: string]: unknown;
 }
 
-interface MessageWithParts {
-  info: { role: string; agent?: string; sessionID?: string };
-  parts: Array<{
-    type: string;
-    text?: string;
-    [key: string]: unknown;
-  }>;
-}
-
 function isImagePart(p: ImagePart): boolean {
   if (p.type === 'image') return true;
   if (p.type === 'file') {

+ 1 - 17
src/hooks/phase-reminder/index.ts

@@ -7,26 +7,10 @@
  */
 import { PHASE_REMINDER_TEXT } from '../../config/constants';
 import { SLIM_INTERNAL_INITIATOR_MARKER } from '../../utils';
+import type { MessageWithParts } from '../types';
 
 export const PHASE_REMINDER = `<internal_reminder>${PHASE_REMINDER_TEXT}</internal_reminder>`;
 
-interface MessageInfo {
-  role: string;
-  agent?: string;
-  sessionID?: string;
-}
-
-interface MessagePart {
-  type: string;
-  text?: string;
-  [key: string]: unknown;
-}
-
-interface MessageWithParts {
-  info: MessageInfo;
-  parts: MessagePart[];
-}
-
 /**
  * Creates the experimental.chat.messages.transform hook for phase reminder injection.
  * This hook runs right before sending to API, so it doesn't affect UI display.

+ 6 - 21
src/hooks/task-session-manager/index.ts

@@ -12,6 +12,7 @@ import {
   SLIM_INTERNAL_INITIATOR_MARKER,
 } from '../../utils';
 import { log } from '../../utils/logger';
+import type { MessagePart, MessageWithParts } from '../types';
 
 interface TaskArgs {
   description?: unknown;
@@ -48,22 +49,6 @@ interface PendingContextFile {
   lastReadAt: number;
 }
 
-interface ChatMessagePart {
-  type: string;
-  text?: string;
-  [key: string]: unknown;
-}
-
-interface ChatMessage {
-  info: {
-    role: string;
-    agent?: string;
-    sessionID?: string;
-    id?: string;
-  };
-  parts: ChatMessagePart[];
-}
-
 const BACKGROUND_JOB_BOARD_SENTINEL = 'SENTINEL: background-job-board-v2';
 const BACKGROUND_COMPLETION_COMPLETED = /^Background task completed: /;
 const BACKGROUND_COMPLETION_FAILED = /^Background task failed: /;
@@ -88,8 +73,8 @@ function djb2Hash(str: string): string {
  * Prefers part.id, then message.info.id + partIndex, then content-derived hash.
  */
 function createOccurrenceId(
-  part: ChatMessagePart,
-  message: ChatMessage,
+  part: MessagePart,
+  message: MessageWithParts,
   partIndex: number,
 ): string {
   // Prefer explicit part.id if available
@@ -314,8 +299,8 @@ export function createTaskSessionManagerHook(
   }
 
   function updateFromInjectedCompletion(
-    part: ChatMessagePart,
-    message: ChatMessage,
+    part: MessagePart,
+    message: MessageWithParts,
     _messageIndex: number,
     partIndex: number,
   ): BackgroundJobRecord | undefined {
@@ -668,7 +653,7 @@ export function createTaskSessionManagerHook(
 
     'experimental.chat.messages.transform': async (
       _input: Record<string, never>,
-      output: { messages: ChatMessage[] },
+      output: { messages: MessageWithParts[] },
     ): Promise<void> => {
       for (const [messageIndex, message] of output.messages.entries()) {
         if (message.info.role !== 'user') continue;

+ 26 - 0
src/hooks/types.ts

@@ -0,0 +1,26 @@
+/**
+ * Shared message type shapes for the OpenCode plugin API's `messages` array.
+ *
+ * These types describe the structure of chat messages passed through
+ * `experimental.chat.messages.transform` and related hooks. All fields
+ * are unioned across the files that previously defined them privately —
+ * optional extras are harmless under structural typing.
+ */
+
+export type MessageInfo = {
+  role: string;
+  agent?: string;
+  sessionID?: string;
+  id?: string;
+};
+
+export type MessagePart = {
+  type: string;
+  text?: string;
+  [key: string]: unknown;
+};
+
+export type MessageWithParts = {
+  info: MessageInfo;
+  parts: MessagePart[];
+};

+ 2 - 12
src/index.ts

@@ -31,6 +31,7 @@ import {
   ForegroundFallbackManager,
 } from './hooks';
 import { processImageAttachments } from './hooks/image-hook';
+import type { MessageWithParts } from './hooks/types';
 import { createInterviewManager } from './interview';
 import { createBuiltinMcps } from './mcp';
 import {
@@ -997,18 +998,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       input: Record<string, never>,
       output: { messages: unknown[] },
     ): Promise<void> => {
-      // Type assertion since we know the structure matches
-      // MessageWithParts[]
-      const typedOutput = output as {
-        messages: Array<{
-          info: { role: string; agent?: string; sessionID?: string };
-          parts: Array<{
-            type: string;
-            text?: string;
-            [key: string]: unknown;
-          }>;
-        }>;
-      };
+      const typedOutput = output as { messages: MessageWithParts[] };
 
       for (const message of typedOutput.messages) {
         if (message.info.role !== 'user') {