Browse Source

refactor: extract duplicated code into shared utilities

umi008 1 month ago
parent
commit
b129d87e10

+ 5 - 0
src/config/constants.ts

@@ -89,3 +89,8 @@ export const STABLE_POLLS_THRESHOLD = 3;
 /** Agents that are disabled by default. Users must explicitly enable them
  *  by removing from disabled_agents and configuring an appropriate model. */
 export const DEFAULT_DISABLED_AGENTS: string[] = ['observer'];
+
+// Background job defaults
+export const DEFAULT_MAX_SESSIONS_PER_AGENT = 2;
+export const DEFAULT_READ_CONTEXT_MIN_LINES = 10;
+export const DEFAULT_READ_CONTEXT_MAX_FILES = 8;

+ 23 - 0
src/hooks/command-hook-utils.ts

@@ -0,0 +1,23 @@
+/**
+ * Register a command hook in the OpenCode config if it doesn't already exist.
+ * Returns true if the command was registered, false if it already existed.
+ */
+export function registerCommandHook(
+  opencodeConfig: Record<string, unknown>,
+  commandName: string,
+  template: string,
+  description: string,
+): boolean {
+  const cmdConfig = (opencodeConfig as { command?: Record<string, unknown> })
+    .command;
+  if (cmdConfig?.[commandName]) return false;
+  if (!opencodeConfig.command)
+    (opencodeConfig as Record<string, unknown>).command = {};
+  (
+    (opencodeConfig as Record<string, unknown>).command as Record<
+      string,
+      unknown
+    >
+  )[commandName] = { template, description };
+  return true;
+}

+ 7 - 10
src/hooks/deepwork/index.ts

@@ -1,4 +1,5 @@
 import { createInternalAgentTextPart } from '../../utils';
+import { registerCommandHook } from '../command-hook-utils';
 
 const COMMAND_NAME = 'deepwork';
 
@@ -30,16 +31,12 @@ export function createDeepworkCommandHook(): {
 } {
   return {
     registerCommand: (opencodeConfig) => {
-      const commandConfig = opencodeConfig.command as
-        | Record<string, unknown>
-        | undefined;
-      if (commandConfig?.[COMMAND_NAME]) return;
-      if (!opencodeConfig.command) opencodeConfig.command = {};
-      (opencodeConfig.command as Record<string, unknown>)[COMMAND_NAME] = {
-        template: 'Start a deepwork session for a complex coding task',
-        description:
-          'Use the deepwork workflow for heavy multi-phase coding work',
-      };
+      registerCommandHook(
+        opencodeConfig,
+        COMMAND_NAME,
+        'Start a deepwork session for a complex coding task',
+        'Use the deepwork workflow for heavy multi-phase coding work',
+      );
     },
 
     handleCommandExecuteBefore: async (input, output) => {

+ 7 - 8
src/hooks/loop-command/index.ts

@@ -1,4 +1,5 @@
 import { createInternalAgentTextPart } from '../../utils';
+import { registerCommandHook } from '../command-hook-utils';
 
 const COMMAND_NAME = 'loop';
 
@@ -52,14 +53,12 @@ export function createLoopCommandHook(): {
 } {
   return {
     registerCommand: (opencodeConfig) => {
-      const cfg = opencodeConfig.command as Record<string, unknown> | undefined;
-      if (cfg?.[COMMAND_NAME]) return;
-      if (!opencodeConfig.command) opencodeConfig.command = {};
-      (opencodeConfig.command as Record<string, unknown>)[COMMAND_NAME] = {
-        template: 'Run an automated execute-verify loop',
-        description:
-          'Dispatch fixer, verify, iterate with file-based history on disk.',
-      };
+      registerCommandHook(
+        opencodeConfig,
+        COMMAND_NAME,
+        'Run an automated execute-verify loop',
+        'Dispatch fixer, verify, iterate with file-based history on disk.',
+      );
     },
 
     handleCommandExecuteBefore: async (input, output) => {

+ 8 - 14
src/hooks/reflect/index.ts

@@ -1,3 +1,5 @@
+import { registerCommandHook } from '../command-hook-utils';
+
 const COMMAND_NAME = 'reflect';
 
 function activationPrompt(
@@ -55,20 +57,12 @@ export function createReflectCommandHook(): {
 
   return {
     registerCommand: (opencodeConfig) => {
-      const commandConfig = opencodeConfig.command as
-        | Record<string, unknown>
-        | undefined;
-      if (commandConfig?.[COMMAND_NAME]) {
-        shouldHandleCommand = false;
-        return;
-      }
-      if (!opencodeConfig.command) opencodeConfig.command = {};
-      (opencodeConfig.command as Record<string, unknown>)[COMMAND_NAME] = {
-        template: 'Review repeated work and suggest workflow improvements',
-        description:
-          'Use reflect to learn from repeated workflows and suggest reusable improvements',
-      };
-      shouldHandleCommand = true;
+      shouldHandleCommand = registerCommandHook(
+        opencodeConfig,
+        COMMAND_NAME,
+        'Review repeated work and suggest workflow improvements',
+        'Use reflect to learn from repeated workflows and suggest reusable improvements',
+      );
     },
 
     handleCommandExecuteBefore: async (input, output) => {

+ 24 - 7
src/index.ts

@@ -10,7 +10,12 @@ import {
   type MultiplexerConfig,
 } from './config';
 import { parseList } from './config/agent-mcps';
-import { AGENT_ALIASES } from './config/constants';
+import {
+  AGENT_ALIASES,
+  DEFAULT_MAX_SESSIONS_PER_AGENT,
+  DEFAULT_READ_CONTEXT_MAX_FILES,
+  DEFAULT_READ_CONTEXT_MIN_LINES,
+} from './config/constants';
 import {
   getActiveRuntimePreset,
   getPreviousRuntimePreset,
@@ -257,9 +262,15 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         : {};
     webfetch = createWebfetchTool(ctx);
     backgroundJobBoard = new BackgroundJobBoard({
-      maxReusablePerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
-      readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
-      readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8,
+      maxReusablePerAgent:
+        config.backgroundJobs?.maxSessionsPerAgent ??
+        DEFAULT_MAX_SESSIONS_PER_AGENT,
+      readContextMinLines:
+        config.backgroundJobs?.readContextMinLines ??
+        DEFAULT_READ_CONTEXT_MIN_LINES,
+      readContextMaxFiles:
+        config.backgroundJobs?.readContextMaxFiles ??
+        DEFAULT_READ_CONTEXT_MAX_FILES,
     });
 
     // Initialize coordinator as the sole writer to the board
@@ -307,9 +318,15 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
     reflectCommandHook = createReflectCommandHook();
     loopCommandHook = createLoopCommandHook();
     taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
-      maxSessionsPerAgent: config.backgroundJobs?.maxSessionsPerAgent ?? 2,
-      readContextMinLines: config.backgroundJobs?.readContextMinLines ?? 10,
-      readContextMaxFiles: config.backgroundJobs?.readContextMaxFiles ?? 8,
+      maxSessionsPerAgent:
+        config.backgroundJobs?.maxSessionsPerAgent ??
+        DEFAULT_MAX_SESSIONS_PER_AGENT,
+      readContextMinLines:
+        config.backgroundJobs?.readContextMinLines ??
+        DEFAULT_READ_CONTEXT_MIN_LINES,
+      readContextMaxFiles:
+        config.backgroundJobs?.readContextMaxFiles ??
+        DEFAULT_READ_CONTEXT_MAX_FILES,
       backgroundJobBoard: backgroundJobCoordinator,
       shouldManageSession: (sessionID) =>
         sessionAgentMap.get(sessionID) === 'orchestrator',

+ 2 - 14
src/interview/document.ts

@@ -1,6 +1,7 @@
 import * as fsSync from 'node:fs';
 import * as fs from 'node:fs/promises';
 import * as path from 'node:path';
+import { parseFrontmatter as sharedParseFrontmatter } from '../utils/frontmatter';
 import type {
   InterviewAnswer,
   InterviewQuestion,
@@ -182,20 +183,7 @@ export function buildInterviewDocument(
 }
 
 /** Parse frontmatter from a .md file. Returns null if no frontmatter. */
-export function parseFrontmatter(
-  content: string,
-): Record<string, string> | null {
-  const match = content.match(/^---\n([\s\S]*?)\n---\n/);
-  if (!match) return null;
-  const result: Record<string, string> = {};
-  for (const line of match[1].split('\n')) {
-    const colonIdx = line.indexOf(':');
-    if (colonIdx > 0) {
-      result[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
-    }
-  }
-  return result;
-}
+export const parseFrontmatter = sharedParseFrontmatter;
 
 export async function ensureInterviewFile(
   record: InterviewRecord,

+ 1 - 9
src/interview/ui.ts

@@ -1,3 +1,4 @@
+import { escapeHtml } from '../utils/escape-html';
 import type { InterviewFileItem, InterviewListItem } from './types';
 
 interface DashboardInterviewItem extends InterviewListItem {
@@ -11,15 +12,6 @@ interface DashboardInterviewItem extends InterviewListItem {
 const BRAND_LOGO_URL =
   'https://ohmyopencodeslim.com/android-chrome-512x512.png';
 
-export function escapeHtml(value: string): string {
-  return value
-    .replaceAll('&', '&amp;')
-    .replaceAll('<', '&lt;')
-    .replaceAll('>', '&gt;')
-    .replaceAll('"', '&quot;')
-    .replaceAll("'", '&#39;');
-}
-
 // ─── Shared client-side helpers ────────────────────────────────────
 
 function clipboardHelperJs(): string {

+ 6 - 23
src/tools/smartfetch/utils.ts

@@ -1,7 +1,11 @@
 import { Readability } from '@mozilla/readability';
 import TurndownService from 'turndown';
+import { escapeHtml } from '../../utils/escape-html';
+import { parseFrontmatter } from '../../utils/frontmatter';
 import type { CachedFetch, ExtractedContent } from './types';
 
+export { escapeHtml, parseFrontmatter };
+
 let jsdomPromise: Promise<typeof import('jsdom')> | undefined;
 
 async function getJSDOM() {
@@ -182,15 +186,6 @@ export function cleanFetchedText(input: string) {
   return trimBlankRuns(input);
 }
 
-export function escapeHtml(input: string) {
-  return input
-    .replace(/&/g, '&amp;')
-    .replace(/</g, '&lt;')
-    .replace(/>/g, '&gt;')
-    .replace(/"/g, '&quot;')
-    .replace(/'/g, '&#39;');
-}
-
 export function withTruncationMarker(
   content: string,
   format: 'text' | 'markdown' | 'html',
@@ -346,21 +341,9 @@ export async function extractFromHtml(
   };
 }
 
-function parseFrontmatterBlock(content: string) {
-  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
-  if (!match) return undefined;
-  const result: Record<string, string> = {};
-  for (const line of match[1].split(/\r?\n/)) {
-    const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.+?)\s*$/);
-    if (!kv) continue;
-    result[kv[1]] = kv[2].replace(/^(['"])(.*)\1$/, '$2');
-  }
-  return result;
-}
-
 export function inferCanonicalUrlFromText(content: string, finalUrl: string) {
-  const frontmatter = parseFrontmatterBlock(content);
-  const raw = frontmatter?.url;
+  const frontmatterData = parseFrontmatter(content);
+  const raw = frontmatterData?.url;
   if (!raw) return undefined;
   try {
     return new URL(raw, finalUrl).toString();

+ 12 - 4
src/utils/background-job-board.ts

@@ -1,4 +1,9 @@
-import { formatSystemReminder } from '../config/constants';
+import {
+  DEFAULT_MAX_SESSIONS_PER_AGENT,
+  DEFAULT_READ_CONTEXT_MAX_FILES,
+  DEFAULT_READ_CONTEXT_MIN_LINES,
+  formatSystemReminder,
+} from '../config/constants';
 import type { BackgroundJobStore } from './background-job-store';
 import { parseTaskStatusOutput, type TaskOutputState } from './task';
 
@@ -92,9 +97,12 @@ export class BackgroundJobBoard implements BackgroundJobStore {
   private readonly readContextMaxFiles: number;
 
   constructor(options: BackgroundJobBoardOptions = {}) {
-    this.maxReusablePerAgent = options.maxReusablePerAgent ?? 2;
-    this.readContextMinLines = options.readContextMinLines ?? 10;
-    this.readContextMaxFiles = options.readContextMaxFiles ?? 8;
+    this.maxReusablePerAgent =
+      options.maxReusablePerAgent ?? DEFAULT_MAX_SESSIONS_PER_AGENT;
+    this.readContextMinLines =
+      options.readContextMinLines ?? DEFAULT_READ_CONTEXT_MIN_LINES;
+    this.readContextMaxFiles =
+      options.readContextMaxFiles ?? DEFAULT_READ_CONTEXT_MAX_FILES;
   }
 
   addTerminalStateListener(listener: TerminalStateListener): void {

+ 8 - 0
src/utils/escape-html.ts

@@ -0,0 +1,8 @@
+export function escapeHtml(value: string): string {
+  return value
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');
+}

+ 18 - 0
src/utils/frontmatter.ts

@@ -0,0 +1,18 @@
+/**
+ * Parse `---` delimited frontmatter from a string.
+ * Handles both `\n` and `\r\n` line endings.
+ * Returns null if no frontmatter is found.
+ */
+export function parseFrontmatter(
+  content: string,
+): Record<string, string> | null {
+  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
+  if (!match) return null;
+  const result: Record<string, string> = {};
+  for (const line of match[1].split(/\r?\n/)) {
+    const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.+?)\s*$/);
+    if (!kv) continue;
+    result[kv[1]] = kv[2].replace(/^(['"])(.*)\1$/, '$2');
+  }
+  return result;
+}