Skip to content

Commit 27e8c7d

Browse files
feat(config): add custom trigger pattern support for watch mode
1 parent 9268c5b commit 27e8c7d

File tree

3 files changed

+137
-23
lines changed

3 files changed

+137
-23
lines changed

codex-cli/src/utils/config.ts

+24
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ export type StoredConfig = {
105105
/** Disable server-side response storage (send full transcript each request) */
106106
disableResponseStorage?: boolean;
107107
providers?: Record<string, { name: string; baseURL: string; envKey: string }>;
108+
/** Configuration for watch mode */
109+
watchMode?: {
110+
/**
111+
* Custom trigger pattern. Default is '/(?:\/\/|#|--|;|\'|%|REM)\s*(.*?)(?:,\s*)?AI[!?]/i'.
112+
* Must be a valid regular expression string, with the first capture group containing the instruction.
113+
* Examples:
114+
* - '/(?:\/\/|#)\s*AI:(TODO|FIXME)\s+(.*)/i' to match "// AI:TODO fix this" or "# AI:FIXME handle errors"
115+
* - '/(?:\/\/|#)\s*codex[!?]\s+(.*)/i' to match "// codex! fix this" or "# codex? what does this do"
116+
*/
117+
triggerPattern?: string;
118+
};
108119
history?: {
109120
maxSize?: number;
110121
saveHistory?: boolean;
@@ -142,6 +153,17 @@ export type AppConfig = {
142153
/** Enable the "flex-mode" processing mode for supported models (o3, o4-mini) */
143154
flexMode?: boolean;
144155
providers?: Record<string, { name: string; baseURL: string; envKey: string }>;
156+
/** Configuration for watch mode */
157+
watchMode?: {
158+
/**
159+
* Custom trigger pattern. Default is '/(?:\/\/|#|--|;|\'|%|REM)\s*(.*?)(?:,\s*)?AI[!?]/i'.
160+
* Must be a valid regular expression string, with the first capture group containing the instruction.
161+
* Examples:
162+
* - '/(?:\/\/|#)\s*AI:(TODO|FIXME)\s+(.*)/i' to match "// AI:TODO fix this" or "# AI:FIXME handle errors"
163+
* - '/(?:\/\/|#)\s*codex[!?]\s+(.*)/i' to match "// codex! fix this" or "# codex? what does this do"
164+
*/
165+
triggerPattern?: string;
166+
};
145167
history?: {
146168
maxSize: number;
147169
saveHistory: boolean;
@@ -334,6 +356,7 @@ export const loadConfig = (
334356
notify: storedConfig.notify === true,
335357
approvalMode: storedConfig.approvalMode,
336358
disableResponseStorage: storedConfig.disableResponseStorage ?? false,
359+
watchMode: storedConfig.watchMode,
337360
};
338361

339362
// -----------------------------------------------------------------------
@@ -448,6 +471,7 @@ export const saveConfig = (
448471
provider: config.provider,
449472
providers: config.providers,
450473
approvalMode: config.approvalMode,
474+
watchMode: config.watchMode,
451475
};
452476

453477
// Add history settings if they exist

codex-cli/src/utils/watch-mode-utils.ts

+79-23
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,78 @@
1+
import { loadConfig } from "./config";
2+
13
/**
2-
* Pattern to match various "AI!" style trigger comments with possible instructions
3-
* Supports multiple single-line programming language comment styles:
4-
* - Double slash comment (C, C++, JavaScript, TypeScript, Java, etc.)
5-
* - Hash comment (Python, Ruby, Perl, Shell scripts, YAML, etc.)
6-
* - Double dash comment (SQL, Haskell, Lua)
7-
* - Semicolon comment (Lisp, Clojure, Assembly)
8-
* - Single quote comment (VB, VBA)
9-
* - Percent comment (LaTeX, Matlab, Erlang)
10-
* - REM comment (Batch files)
11-
*
4+
* Custom trigger patterns for watch mode
5+
*
6+
* Users can define their own trigger patterns in config.json:
7+
* ```json
8+
* {
9+
* "watchMode": {
10+
* "triggerPattern": "/(?:\\/\\/|#)\\s*AI:(TODO|FIXME)\\s+(.*)/i"
11+
* }
12+
* }
13+
* ```
14+
*
15+
* The pattern MUST include at least one capture group that will contain the instruction.
16+
*
1217
* Examples:
18+
*
19+
* Default pattern (single-line comments ending with AI! or AI?):
1320
* - "// what does this function do, AI?"
1421
* - "# Fix this code, AI!"
1522
* - "-- Optimize this query, AI!"
23+
*
24+
* Custom pattern for task management:
25+
* - "// AI:TODO fix this bug"
26+
* - "// AI:FIXME handle error case"
27+
* - "# AI:BUG this crashes with null input"
28+
*
29+
* Custom pattern with different keyword:
30+
* - "// codex! fix this"
31+
* - "# codex? what does this function do"
1632
*/
1733

18-
export const TRIGGER_PATTERN =
19-
/(?:\/\/|#|--|;|'|%|REM)\s*(.*?)(?:,\s*)?AI[!?]/i;
34+
// Default trigger pattern
35+
const DEFAULT_TRIGGER_PATTERN = '/(?:\\/\\/|#|--|;|\'|%|REM)\\s*(.*?)(?:,\\s*)?AI[!?]/i';
36+
37+
/**
38+
* Get the configured trigger pattern from config.json or use the default
39+
* @returns A RegExp object for the trigger pattern
40+
*/
41+
export function getTriggerPattern(): RegExp {
42+
const config = loadConfig();
43+
44+
// Get the pattern string from config or use default
45+
const patternString = config.watchMode?.triggerPattern || DEFAULT_TRIGGER_PATTERN;
46+
47+
try {
48+
// Parse the regex from the string - first remove enclosing slashes and extract flags
49+
const match = patternString.match(/^\/(.*)\/([gimuy]*)$/);
50+
51+
if (match) {
52+
const [, pattern, flags] = match;
53+
return new RegExp(pattern, flags);
54+
} else {
55+
// If not in /pattern/flags format, try to use directly as a RegExp
56+
return new RegExp(patternString, 'i');
57+
}
58+
} catch (error) {
59+
console.warn(`Invalid trigger pattern in config: ${patternString}. Using default.`);
60+
// Parse default pattern
61+
const match = DEFAULT_TRIGGER_PATTERN.match(/^\/(.*)\/([gimuy]*)$/);
62+
const [, pattern, flags] = match!;
63+
return new RegExp(pattern, flags);
64+
}
65+
}
2066

2167
/**
22-
* Function to find all AI trigger matches in a file content
68+
* Function to find all trigger matches in a file content
69+
* Uses the configured trigger pattern from config.json
2370
*/
2471
export function findAllTriggers(content: string): Array<RegExpMatchArray> {
2572
const matches: Array<RegExpMatchArray> = [];
26-
const regex = new RegExp(TRIGGER_PATTERN, "g");
73+
const pattern = getTriggerPattern();
74+
// We need to ensure the global flag is set for .exec() to work properly
75+
const regex = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
2776

2877
let match;
2978
while ((match = regex.exec(content)) != null) {
@@ -58,15 +107,22 @@ export function extractContextAroundTrigger(
58107
// Join the context lines back together
59108
const context = contextLines.join("\n");
60109

61-
// Extract the instruction from the capture groups for different comment styles
62-
// There are multiple capture groups for different comment syntaxes
63-
// Find the first non-undefined capture group
64-
let instruction =
65-
Array.from(
66-
{ length: triggerMatch.length - 1 },
67-
(_, i) => triggerMatch[i + 1],
68-
).find((group) => group !== undefined) || "fix or improve this code";
69-
110+
// Get instruction from capture groups
111+
// For custom patterns, check all capture groups and use the last non-empty one
112+
// This allows patterns like /AI:(TODO|FIXME)\s+(.*)/ where we want the second group
113+
// For the default pattern, it will be the first capture group
114+
const captureGroups = Array.from(
115+
{ length: triggerMatch.length - 1 },
116+
(_, i) => triggerMatch[i + 1]
117+
).filter(group => group !== undefined);
118+
119+
// Use the last non-empty capture group as the instruction
120+
// For simple patterns with one capture, this will be that capture
121+
// For patterns with multiple captures (like task types), this will be the actual instruction
122+
let instruction = captureGroups.length > 0
123+
? captureGroups[captureGroups.length - 1]
124+
: "fix or improve this code";
125+
70126
// Remove any comment prefixes that might have been captured
71127
instruction = instruction.replace(/^(?:\/\/|#|--|;|'|%|REM)\s*/, "");
72128

codex-cli/tests/watch-mode-trigger-extraction.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { describe, it, expect } from "vitest";
55
import {
66
findAllTriggers,
77
extractContextAroundTrigger,
8+
getTriggerPattern
89
} from "../src/utils/watch-mode-utils";
910

1011
// For testing, we'll use a larger context size
@@ -127,6 +128,39 @@ describe("Watch mode trigger pattern matching", () => {
127128
expect(matches[0]![1]).toBe("Optimize this query");
128129
});
129130

131+
132+
it("should handle custom trigger patterns", () => {
133+
// Create custom patterns for testing
134+
const customPatternString = '/(?:\\/\\/|#)\\s*AI:(TODO|FIXME)\\s+(.*)/i';
135+
const match = customPatternString.match(/^\/(.*)\/([gimuy]*)$/);
136+
const [, pattern, flags] = match!;
137+
const customPattern = new RegExp(pattern, flags + 'g');
138+
139+
const content = `
140+
function testFunction() {
141+
// This is a normal comment
142+
// AI:TODO Fix this bug
143+
return 1 + 1;
144+
}
145+
146+
function anotherFunction() {
147+
# AI:FIXME Handle null input
148+
return x * 2;
149+
}
150+
`;
151+
152+
const matches: Array<RegExpMatchArray> = [];
153+
let matchResult;
154+
while ((matchResult = customPattern.exec(content)) != null) {
155+
matches.push(matchResult);
156+
}
157+
158+
expect(matches.length).toBe(2);
159+
expect(matches[0]![0]).toContain("AI:TODO Fix this bug");
160+
expect(matches[0]![2]).toBe("Fix this bug");
161+
expect(matches[1]![0]).toContain("AI:FIXME Handle null input");
162+
expect(matches[1]![2]).toBe("Handle null input");
163+
});
130164
});
131165

132166
describe("Context extraction around AI triggers", () => {

0 commit comments

Comments
 (0)