Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f13938a
perf: reuse stale extension command builds
JustYannicc Jul 4, 2026
72933e0
perf: target extension command refresh
JustYannicc Jul 4, 2026
f981072
fix: clean up extension spawn lifecycle
JustYannicc Jul 4, 2026
f2d1fb7
Fix extension fetch proxy cancellation
JustYannicc Jul 4, 2026
3f5e7c9
fix: clean up main abort listeners
JustYannicc Jul 4, 2026
ed455da
perf: stream cloud media buffers
JustYannicc Jul 4, 2026
e92884d
fix(raycast): guard registry microtask teardown
JustYannicc Jul 4, 2026
2a9a88d
fix: align runtime consolidation integration
JustYannicc Jul 5, 2026
2a8f637
perf(ai): bound stream parser buffers
JustYannicc Jul 5, 2026
5f53800
perf(lifecycle): scope extension timer builtins
JustYannicc Jul 5, 2026
155da90
perf(extensions): reuse on-demand build stamps
JustYannicc Jul 5, 2026
ca32b4c
perf(background): prevent overlapping refresh ticks
JustYannicc Jul 5, 2026
919c88e
perf(runtime): cancel extension HTTP work on abort
JustYannicc Jul 5, 2026
ecc3334
perf(ipc): harden native helper readiness
JustYannicc Jul 5, 2026
1ee1979
perf(media): cancel binary fetch downloads
JustYannicc Jul 5, 2026
5887f69
fix(runtime): align native helper lifecycle guards
JustYannicc Jul 5, 2026
f85bee3
fix(runtime): address PR 622 review feedback
JustYannicc Jul 8, 2026
bbb83df
Merge main into runtime lifecycle consolidation
JustYannicc Jul 12, 2026
b91e560
ci: allow cross-platform lockfile install
JustYannicc Jul 12, 2026
22ce2fa
test: align builtin facade integration
JustYannicc Jul 12, 2026
ea2e6c8
ci: skip Claude review for fork PRs
JustYannicc Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ on:

jobs:
claude-review:
# Fork PRs cannot receive the OIDC token required by this action.
if: github.event.pull_request.head.repo.full_name == github.repository
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
Expand Down Expand Up @@ -41,4 +43,3 @@ jobs:
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
cache: npm

- name: Install dependencies
run: npm ci
run: npm ci --force

- name: Run node test suite
run: npm test
41 changes: 35 additions & 6 deletions scripts/lib/ts-import.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,44 @@
// esbuild on the fly. This lets the recovery tests run the *real* production
// source (renderer-recovery.ts, reload-budget.ts) instead of grepping it.
//
// Only works for modules with no relative imports of their own — the recovery
// helpers are deliberately dependency-free for exactly this reason.
// By default this stays intentionally lightweight for dependency-free helpers.
// Tests that need production modules with a few heavy dependencies can pass
// stubs keyed by import specifier.

import { transform } from 'esbuild';
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';

export async function importTs(absPath) {
function toDataUrl(code) {
return 'data:text/javascript;base64,' + Buffer.from(code).toString('base64');
}

async function transformTs(source) {
const { code } = await transform(source, { loader: 'ts', format: 'esm' });
return code;
}

function rewriteImportSpecifiers(code, absPath, options) {
const stubs = options.stubs || {};
const stubUrls = new Map(Object.keys(stubs).map((specifier) => [specifier, toDataUrl(stubs[specifier])]));

const rewriteSpecifier = (specifier) => {
if (stubUrls.has(specifier)) return stubUrls.get(specifier);
if ((specifier.startsWith('./') || specifier.startsWith('../')) && options.root) {
const resolved = path.resolve(path.dirname(absPath), specifier);
return pathToFileURL(resolved).href;
}
return specifier;
};

return code
.replace(/(\bfrom\s*["'])([^"']+)(["'])/g, (_match, prefix, specifier, suffix) => `${prefix}${rewriteSpecifier(specifier)}${suffix}`)
.replace(/(\bimport\s*["'])([^"']+)(["'])/g, (_match, prefix, specifier, suffix) => `${prefix}${rewriteSpecifier(specifier)}${suffix}`);
}

export async function importTs(absPath, options = {}) {
const src = fs.readFileSync(absPath, 'utf8');
const { code } = await transform(src, { loader: 'ts', format: 'esm' });
const dataUrl = 'data:text/javascript;base64,' + Buffer.from(code).toString('base64');
return import(dataUrl);
const code = rewriteImportSpecifiers(await transformTs(src), absPath, options);
return import(toDataUrl(code));
}
1 change: 1 addition & 0 deletions scripts/measure-extension-bundle-cache.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export async function bundleExtensionRunner(userDataDir) {
entryPoints: [path.join(root, 'src/main/extension-runner.ts')],
outfile: outFile,
bundle: true,
external: ['esbuild'],
platform: 'node',
format: 'cjs',
target: 'node20',
Expand Down
213 changes: 213 additions & 0 deletions scripts/test-ai-provider-stream-parser-bounds.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
#!/usr/bin/env node

import test from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import path from 'node:path';
import { PassThrough } from 'node:stream';
import { setImmediate as waitImmediate } from 'node:timers/promises';
import { importTs } from './lib/ts-import.mjs';

const REQUEST_STUB = `
import { EventEmitter } from 'node:events';
export function request(options, callback) {
const harness = globalThis.__aiProviderStreamParserHarness;
if (!harness) throw new Error('AI provider stream parser harness is not installed');
return harness.request(options, callback);
}
export default { request };
`;

const {
AI_STREAM_PARSER_MAX_BUFFER_CHARS,
streamAI,
} = await importTs(path.resolve('src/main/ai-provider.ts'), {
stubs: {
http: REQUEST_STUB,
https: REQUEST_STUB,
},
});

const OPENAI_CONFIG = {
enabled: true,
provider: 'openai',
openaiApiKey: 'test-openai-key',
};

const OLLAMA_CONFIG = {
enabled: true,
provider: 'ollama',
ollamaBaseUrl: 'http://127.0.0.1:11434',
};

function createRequestHarness() {
const requests = [];

return {
requests,
request(options, callback) {
const record = {
options,
body: '',
destroyed: false,
response: null,
};

const req = new EventEmitter();
req.write = (chunk) => {
record.body += chunk.toString();
return true;
};
req.end = () => {
const response = new PassThrough();
response.statusCode = 200;
record.response = response;
callback(response);
};
req.destroy = () => {
record.destroyed = true;
record.response?.destroy();
};

requests.push(record);
return req;
},
};
}

function installHarness() {
const harness = createRequestHarness();
globalThis.__aiProviderStreamParserHarness = harness;
return harness;
}

async function waitForRequestResponse(harness) {
for (let attempt = 0; attempt < 50; attempt += 1) {
const request = harness.requests[0];
if (request?.response) return request;
await waitImmediate();
}
assert.fail('timed out waiting for AI provider request');
}

function openAISseFrame(text) {
return `data: ${JSON.stringify({ choices: [{ delta: { content: text } }] })}\n\n`;
}

function ollamaNdjsonLine(text) {
return `${JSON.stringify({ response: text })}\n`;
}

function splitInsideUtf8Sequence(text, sequence) {
const buffer = Buffer.from(text);
const needle = Buffer.from(sequence);
const index = buffer.indexOf(needle);
assert.notEqual(index, -1, `expected payload to contain ${sequence}`);
return [
buffer.subarray(0, index + 1),
buffer.subarray(index + 1),
];
}

async function startStream(config, options = {}) {
const harness = installHarness();
const chunks = [];
const collecting = (async () => {
for await (const chunk of streamAI(config, {
prompt: 'stream parser harness prompt',
creativity: 0.2,
...options,
})) {
chunks.push(chunk);
}
return chunks;
})();

const request = await waitForRequestResponse(harness);
return { collecting, chunks, request };
}

test('SSE parser preserves UTF-8 characters split across provider chunks', async (t) => {
const expected = 'Hello 🙂 world';
const frame = openAISseFrame(expected);
const parts = splitInsideUtf8Sequence(frame, '🙂');
const { collecting, request } = await startStream(OPENAI_CONFIG);

request.response.write(parts[0]);
request.response.write(parts[1]);
request.response.end('data: [DONE]\n\n');

assert.deepEqual(await collecting, [expected]);

const legacyDecoded = parts.map((part) => part.toString()).join('');
assert.notEqual(legacyDecoded, frame);
t.diagnostic(`legacy split-SSE decode matched source: ${legacyDecoded === frame}`);
t.diagnostic('TextDecoder streaming split-SSE decode matched source: true');
});

test('NDJSON parser preserves UTF-8 characters split across provider chunks', async (t) => {
const expected = 'Ollama says 🙂';
const line = ollamaNdjsonLine(expected);
const parts = splitInsideUtf8Sequence(line, '🙂');
const { collecting, request } = await startStream(OLLAMA_CONFIG);

request.response.write(parts[0]);
request.response.write(parts[1]);
request.response.end();

assert.deepEqual(await collecting, [expected]);

const legacyDecoded = parts.map((part) => part.toString()).join('');
assert.notEqual(legacyDecoded, line);
t.diagnostic(`legacy split-NDJSON decode matched source: ${legacyDecoded === line}`);
t.diagnostic('TextDecoder streaming split-NDJSON decode matched source: true');
});

test('stream parsers keep valid long provider payloads below the buffer limit working', async () => {
const longText = 'provider-payload-'.repeat(16 * 1024);
const sse = await startStream(OPENAI_CONFIG);
sse.request.response.end(openAISseFrame(longText) + 'data: [DONE]\n\n');
assert.deepEqual(await sse.collecting, [longText]);

const ndjson = await startStream(OLLAMA_CONFIG);
ndjson.request.response.end(ollamaNdjsonLine(longText));
assert.deepEqual(await ndjson.collecting, [longText]);
});

test('SSE parser allows an incomplete line at the configured buffer limit', async (t) => {
const { collecting, request } = await startStream(OPENAI_CONFIG);

request.response.write('x'.repeat(AI_STREAM_PARSER_MAX_BUFFER_CHARS));
request.response.end('\n');

assert.deepEqual(await collecting, []);
t.diagnostic(`bounded SSE retained-buffer limit accepted: ${AI_STREAM_PARSER_MAX_BUFFER_CHARS} characters`);
});

test('SSE parser rejects an unterminated line above the configured buffer limit', async (t) => {
const { collecting, request } = await startStream(OPENAI_CONFIG);

request.response.write('x'.repeat(AI_STREAM_PARSER_MAX_BUFFER_CHARS + 1));

await assert.rejects(
collecting,
/AI stream parser exceeded \d+ buffered characters without a line break/,
);
request.response.destroy();
t.diagnostic(`legacy retained incomplete SSE characters: ${AI_STREAM_PARSER_MAX_BUFFER_CHARS + 1}`);
t.diagnostic(`bounded retained incomplete SSE characters: ${AI_STREAM_PARSER_MAX_BUFFER_CHARS}`);
});

test('NDJSON parser rejects an unterminated line above the configured buffer limit', async (t) => {
const { collecting, request } = await startStream(OLLAMA_CONFIG);

request.response.write('x'.repeat(AI_STREAM_PARSER_MAX_BUFFER_CHARS + 1));

await assert.rejects(
collecting,
/AI stream parser exceeded \d+ buffered characters without a line break/,
);
request.response.destroy();
t.diagnostic(`legacy retained incomplete NDJSON characters: ${AI_STREAM_PARSER_MAX_BUFFER_CHARS + 1}`);
t.diagnostic(`bounded retained incomplete NDJSON characters: ${AI_STREAM_PARSER_MAX_BUFFER_CHARS}`);
});
Loading
Loading