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();