diff --git a/cli/src/utils/__tests__/implementor-helpers.test.ts b/cli/src/utils/__tests__/implementor-helpers.test.ts index aada220d51..81b50d4916 100644 --- a/cli/src/utils/__tests__/implementor-helpers.test.ts +++ b/cli/src/utils/__tests__/implementor-helpers.test.ts @@ -44,6 +44,24 @@ describe('extractValueForKey', () => { expect(extractValueForKey(output, 'nonexistent')).toBeNull() }) + test('returns null when key name appears without colon', () => { + const output = 'This mentions file but has no colon' + expect(extractValueForKey(output, 'file')).toBeNull() + }) + + test('extracts value from last line without trailing newline', () => { + const output = 'first: line\nlast: value' + expect(extractValueForKey(output, 'last')).toBe('value') + }) + + test('handles large outputs efficiently without error', () => { + const large = + Array.from({ length: 5000 }, (_, i) => `log_${i}: data`).join('\n') + + '\nfile: src/target.ts' + expect(extractValueForKey(large, 'file')).toBe('src/target.ts') + expect(extractValueForKey(large, 'missing')).toBeNull() + }) + test('handles empty output', () => { expect(extractValueForKey('', 'file')).toBeNull() }) diff --git a/cli/src/utils/implementor-helpers.ts b/cli/src/utils/implementor-helpers.ts index 4fb9fea3de..35a8210bcb 100644 --- a/cli/src/utils/implementor-helpers.ts +++ b/cli/src/utils/implementor-helpers.ts @@ -181,25 +181,44 @@ export function groupConsecutiveToolBlocks( * Supports multi-line values with pipe delimiter. */ export function extractValueForKey(output: string, key: string): string | null { - if (!output) return null - const lines = output.split('\n') - for (let i = 0; i < lines.length; i++) { - const line = lines[i] + if (!output || !output.includes(key + ':')) return null + + let lineStart = 0 + const len = output.length + + while (lineStart < len) { + let lineEnd = output.indexOf('\n', lineStart) + if (lineEnd === -1) lineEnd = len + + const line = output.slice(lineStart, lineEnd) const match = line.match(/^\s*([A-Za-z0-9_]+):\s*(.*)$/) if (match && match[1] === key) { const rest = match[2] if (rest.trim().startsWith('|')) { - const baseIndent = lines[i + 1]?.match(/^\s*/)?.[0].length ?? 0 + let nextStart = lineEnd + 1 + if (nextStart >= len) return '' + + let nextEnd = output.indexOf('\n', nextStart) + if (nextEnd === -1) nextEnd = len + const firstLine = output.slice(nextStart, nextEnd) + const baseIndent = firstLine.match(/^\s*/)?.[0].length ?? 0 + const acc: string[] = [] - for (let j = i + 1; j < lines.length; j++) { - const l = lines[j] + let currStart = nextStart + while (currStart < len) { + let currEnd = output.indexOf('\n', currStart) + if (currEnd === -1) currEnd = len + + const l = output.slice(currStart, currEnd) const indent = l.match(/^\s*/)?.[0].length ?? 0 if (l.trim().length === 0) { acc.push('') - continue + } else { + if (indent < baseIndent) break + acc.push(l.slice(baseIndent)) } - if (indent < baseIndent) break - acc.push(l.slice(baseIndent)) + + currStart = currEnd + 1 } return acc.join('\n') } else { @@ -210,7 +229,10 @@ export function extractValueForKey(output: string, key: string): string | null { return val } } + + lineStart = lineEnd + 1 } + return null }