diff --git a/.github/workflows/verify-pr.yaml b/.github/workflows/verify-pr.yaml index 192f4b5..f48a61e 100644 --- a/.github/workflows/verify-pr.yaml +++ b/.github/workflows/verify-pr.yaml @@ -36,6 +36,22 @@ jobs: - name: Check format run: npm run format + typecheck: + name: Typecheck + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: '.nvmrc' + cache: npm + - name: Install dependencies + run: npm ci + - name: Check types + run: npm run typecheck + build: name: Build runs-on: ubuntu-latest diff --git a/agent/runAgentForOnePass.ts b/agent/runAgentForOnePass.ts index 6542f05..33f0ee6 100644 --- a/agent/runAgentForOnePass.ts +++ b/agent/runAgentForOnePass.ts @@ -212,11 +212,15 @@ export async function runAgentForOnePass( hasUnknownPrices: sessionStats.hasUnknownPrices, }); + // `_maxTurns` is `number | null` upstream; null means "unset", in which case we + // keep the configured limit rather than clobbering it (a null here would make + // the `currentTurn + 1 >= maxTurns` check below fire immediately). + const nextMaxTurns = stream.state._maxTurns ?? trackedState.maxTurns; if ( - trackedState.currentTurn !== stream.state._currentTurn || trackedState.maxTurns !== stream.state._maxTurns + trackedState.currentTurn !== stream.state._currentTurn || trackedState.maxTurns !== nextMaxTurns ) { trackedState.currentTurn = stream.state._currentTurn; - trackedState.maxTurns = stream.state._maxTurns; + trackedState.maxTurns = nextMaxTurns; emitToListeners('SettingsUpdated', undefined); if (trackedState.currentTurn + 1 >= trackedState.maxTurns) { await pushNewItemsIntoSession(); @@ -318,7 +322,7 @@ export async function runAgentForOnePass( }); trackedState.currentTurn = stream.state._currentTurn; - trackedState.maxTurns = stream.state._maxTurns; + trackedState.maxTurns = stream.state._maxTurns ?? trackedState.maxTurns; emitToListeners('SettingsUpdated', undefined); if (stream.interruptions?.length) { diff --git a/ink/components/BlinkingTextInput.tsx b/ink/components/BlinkingTextInput.tsx index fc459e7..09d7f2a 100644 --- a/ink/components/BlinkingTextInput.tsx +++ b/ink/components/BlinkingTextInput.tsx @@ -182,7 +182,7 @@ export function BlinkingTextInput({ return ( - {displayContent} + {displayContent} ); } @@ -216,7 +216,7 @@ export function BlinkingTextInput({ result.push( - {lineContent} + {lineContent} , ); if (lineIndex < lines.length - 1) { diff --git a/ink/configurationWizard/ApiKeyStep.test.tsx b/ink/configurationWizard/ApiKeyStep.test.tsx index ecb106e..7ffb85c 100644 --- a/ink/configurationWizard/ApiKeyStep.test.tsx +++ b/ink/configurationWizard/ApiKeyStep.test.tsx @@ -20,7 +20,9 @@ describe('ApiKeyStep', () => { it('renders API key prompt for OpenAI', () => { const onConfirm = vi.fn(); const onBack = vi.fn(); - const { lastFrame } = render(); + const { lastFrame } = render( + , + ); expect(lastFrame()).toContain('Can you provide us with your OpenAI API key?'); expect(lastFrame()).toContain('Get your key at: https://platform.openai.com/api-keys'); @@ -29,7 +31,7 @@ describe('ApiKeyStep', () => { it('calls onBack when ESC is pressed', () => { const onConfirm = vi.fn(); const onBack = vi.fn(); - render(); + render(); const inputHandler = (useInput as any).mock.calls[0][0]; inputHandler('', { escape: true }); diff --git a/ink/configurationWizard/ApiUrlStep.test.tsx b/ink/configurationWizard/ApiUrlStep.test.tsx index 4684cca..f29c286 100644 --- a/ink/configurationWizard/ApiUrlStep.test.tsx +++ b/ink/configurationWizard/ApiUrlStep.test.tsx @@ -20,7 +20,9 @@ describe('ApiUrlStep', () => { it('renders API URL prompt for Ollama', () => { const onConfirm = vi.fn(); const onBack = vi.fn(); - const { lastFrame } = render(); + const { lastFrame } = render( + , + ); expect(lastFrame()).toContain('Where are you hosting Ollama?'); }); @@ -28,7 +30,7 @@ describe('ApiUrlStep', () => { it('calls onBack when ESC is pressed', () => { const onConfirm = vi.fn(); const onBack = vi.fn(); - render(); + render(); const inputHandler = (useInput as any).mock.calls[0][0]; inputHandler('', { escape: true }); diff --git a/ink/configurationWizard/ModelSelectionStep.test.tsx b/ink/configurationWizard/ModelSelectionStep.test.tsx index 4cb2dd3..d179234 100644 --- a/ink/configurationWizard/ModelSelectionStep.test.tsx +++ b/ink/configurationWizard/ModelSelectionStep.test.tsx @@ -24,6 +24,7 @@ describe('ModelSelectionStep', () => { , @@ -42,6 +43,7 @@ describe('ModelSelectionStep', () => { , diff --git a/ink/configurationWizard/ProviderStep.test.tsx b/ink/configurationWizard/ProviderStep.test.tsx index d46c856..d7a5062 100644 --- a/ink/configurationWizard/ProviderStep.test.tsx +++ b/ink/configurationWizard/ProviderStep.test.tsx @@ -10,7 +10,8 @@ describe('ProviderStep', () => { it('renders provider selection message', () => { const onConfirm = vi.fn(); - const { lastFrame } = render(); + const onExit = vi.fn(); + const { lastFrame } = render(); expect(lastFrame()).toContain('What model provider would you like to use today?'); }); diff --git a/package.json b/package.json index ab7d734..0ce4bfa 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "format:fix": "dprint fmt", "format:staged": "dprint check --staged --allow-no-files", "format-and-lint:fix": "npm run format:fix && npm run lint:fix", + "typecheck": "tsc --noEmit", "test": "vitest --run", "test:coverage": "vitest run --coverage", "test:watch": "vitest", diff --git a/tools/browser/browserClickTool.ts b/tools/browser/browserClickTool.ts index 47b9079..fd7c932 100644 --- a/tools/browser/browserClickTool.ts +++ b/tools/browser/browserClickTool.ts @@ -11,7 +11,9 @@ const ToolParameters = z.object({ export async function execute({ selector, button, clickCount }: z.infer) { try { const page = await getPage(); - await page.click(selector, { button, clickCount }); + // Puppeteer calls this `count`; passing `clickCount` was silently ignored, so + // multi-click requests always performed a single click. + await page.click(selector, { button, count: clickCount }); return `Successfully clicked on ${selector}`; } catch (error) { return `Error clicking on ${selector}: ${error}`; diff --git a/tools/files/applyPatchTool.ts b/tools/files/applyPatchTool.ts index c6e3400..f78c199 100644 --- a/tools/files/applyPatchTool.ts +++ b/tools/files/applyPatchTool.ts @@ -31,7 +31,10 @@ async function getSkillsRead(): Promise { } } -function pickExistingSkill(candidates: string[]): string | null { +/** The skill names `getHarperSkillTool` actually accepts. */ +type HarperSkillName = (typeof harperSkills)[number]; + +function pickExistingSkill(candidates: readonly HarperSkillName[]): HarperSkillName | null { for (const c of candidates) { if (harperSkills.includes(c)) { return c; } } return null; } @@ -39,7 +42,7 @@ function pickExistingSkill(candidates: string[]): string | null { async function requiredSkillForOperation( path: string, type: 'create_file' | 'update_file' | 'delete_file' | 'overwrite_file', -): Promise { +): Promise { if (type === 'delete_file') { return null; } const p = normalizedPath(path); const read = await getSkillsRead();