Compare commits

...
Author SHA1 Message Date
Paul Bakaus e8017cbef8 Release: include Mac Intel package integrity in lockfile
AI assistance: prepared and validated with Codex under Paul Bakaus direction.
2026-09-05 10:42:42 -07:00
Paul Bakaus dff411d8e8 Release: lock published engine 0.1.1 packages
AI assistance: prepared and validated with Codex under Paul Bakaus direction.
2026-09-05 10:42:03 -07:00
Paul Bakaus 2564531fd0 Fix: checkpoint annotated sessions after creation
Defer generate_started until the server acknowledges the generate event, including screenshot capture/upload. Guard failed and superseded creation. Cover delayed creation in source-function tests and a slow-upload Svelte browser regression without reload recovery.

Prepared with AI assistance under maintainer direction.
2026-09-05 10:21:47 -07:00
Paul Bakaus 8c880d75f9 Fix: retain bounded DeepSeek planning in release tests
Use explicit low-effort thinking: non-thinking mode was unreliable for the full live specification, while the provider high-effort default exhausted bounded responses. Keep real provider edits and existing assertions.

Prepared with AI assistance under maintainer direction.
2026-09-04 19:33:40 -07:00
Paul Bakaus cc988144a0 Fix: wait for settled Svelte test variants
Reuse the shared mount-aware cycling helper before subsequent clicks and reload. Keep the DeepSeek request limits scoped to that provider so other provider defaults stay unchanged.

Prepared with AI assistance under maintainer direction.
2026-09-04 19:29:46 -07:00
Paul Bakaus dc5c4b2184 Fix: bound DeepSeek release-test requests
Explicitly disable thinking for the bounded JSON edit harness and give steer the same timeout/retry limits as manual edits. Keep real provider calls and all browser assertions intact.

Prepared with AI assistance under maintainer direction.
2026-09-04 19:24:42 -07:00
Paul Bakaus 9a7fae4361 Release: prepare engine 0.1.1 and CLI 4.0.2
Activate the signed-bundle verifier from #734. Publish the engine binaries and platform packages before merging these pins and publishing the CLI. The signed skill 4.2.0 ZIP remains unchanged.

AI-assisted release preparation by Codex at Paul Bakaus’s request.
2026-09-04 18:25:37 -07:00
13 changed files with 143 additions and 53 deletions
Generated
+16 -16
View File
@@ -509,7 +509,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable" name = "impeccable"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"base64", "base64",
"impeccable-browser", "impeccable-browser",
@@ -528,7 +528,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-browser" name = "impeccable-browser"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"base64", "base64",
"impeccable-core", "impeccable-core",
@@ -542,7 +542,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-bundle" name = "impeccable-bundle"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"base64", "base64",
"impeccable-core", "impeccable-core",
@@ -551,14 +551,14 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-common" name = "impeccable-common"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"libc", "libc",
] ]
[[package]] [[package]]
name = "impeccable-comp" name = "impeccable-comp"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"image", "image",
"once_cell", "once_cell",
@@ -570,7 +570,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-comp-verbs" name = "impeccable-comp-verbs"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"impeccable-common", "impeccable-common",
"impeccable-comp", "impeccable-comp",
@@ -583,7 +583,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-context" name = "impeccable-context"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"flate2", "flate2",
"impeccable-common", "impeccable-common",
@@ -600,7 +600,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-core" name = "impeccable-core"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"impeccable-core", "impeccable-core",
"impeccable-foundation", "impeccable-foundation",
@@ -612,7 +612,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-detect" name = "impeccable-detect"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"impeccable-common", "impeccable-common",
"impeccable-core", "impeccable-core",
@@ -624,7 +624,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-foundation" name = "impeccable-foundation"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"cssparser", "cssparser",
"once_cell", "once_cell",
@@ -637,7 +637,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-hook" name = "impeccable-hook"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"impeccable-common", "impeccable-common",
"impeccable-context", "impeccable-context",
@@ -651,7 +651,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-html" name = "impeccable-html"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"cssparser", "cssparser",
"ego-tree", "ego-tree",
@@ -672,7 +672,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-live" name = "impeccable-live"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"getrandom 0.2.17", "getrandom 0.2.17",
"impeccable-common", "impeccable-common",
@@ -690,7 +690,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-skills" name = "impeccable-skills"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"impeccable-common", "impeccable-common",
"impeccable-context", "impeccable-context",
@@ -709,7 +709,7 @@ dependencies = [
[[package]] [[package]]
name = "impeccable-wasm" name = "impeccable-wasm"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"impeccable-core", "impeccable-core",
"impeccable-detect", "impeccable-detect",
@@ -1681,7 +1681,7 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]] [[package]]
name = "xtask" name = "xtask"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"impeccable-bundle", "impeccable-bundle",
] ]
+1 -1
View File
@@ -6,7 +6,7 @@ resolver = "2"
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
license = "Apache-2.0" license = "Apache-2.0"
publish = false publish = false
+1 -1
View File
@@ -1 +1 @@
0.1.0 0.1.1
+15 -5
View File
@@ -19,11 +19,11 @@
"zod": "^4.3.6", "zod": "^4.3.6",
}, },
"optionalDependencies": { "optionalDependencies": {
"@impeccable/cli-darwin-arm64": "0.1.0", "@impeccable/cli-darwin-arm64": "0.1.1",
"@impeccable/cli-darwin-x64": "0.1.0", "@impeccable/cli-darwin-x64": "0.1.1",
"@impeccable/cli-linux-arm64": "0.1.0", "@impeccable/cli-linux-arm64": "0.1.1",
"@impeccable/cli-linux-x64": "0.1.0", "@impeccable/cli-linux-x64": "0.1.1",
"@impeccable/cli-windows-x64": "0.1.0", "@impeccable/cli-windows-x64": "0.1.1",
}, },
}, },
}, },
@@ -72,6 +72,16 @@
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@impeccable/cli-darwin-arm64": ["@impeccable/cli-darwin-arm64@0.1.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "impeccable-darwin-arm64": "bin/impeccable" } }, "sha512-1/DZYaiZqDoNwpXpyoG4gRQpgLZ4YDCMADGzA5wYmNiG279b3KPt2RLYUww6NF2hxoMxdVQUAGzdc5062KZKHg=="],
"@impeccable/cli-darwin-x64": ["@impeccable/cli-darwin-x64@0.1.1", "", { "os": "darwin", "cpu": "x64", "bin": { "impeccable-darwin-x64": "bin/impeccable" } }, "sha512-/itjFZEHPcz1RQDBx3+2aeTebQ4pCD30VKAJ3Zst5KxHteJQOW3hoiTFE7jAOXg3vG2D/qAaEh/7Eo+xzEawew=="],
"@impeccable/cli-linux-arm64": ["@impeccable/cli-linux-arm64@0.1.1", "", { "os": "linux", "cpu": "arm64", "bin": { "impeccable-linux-arm64": "bin/impeccable" } }, "sha512-uGJ2DNVq3NzH8+RlTlyn1XWpAsNS1bv6kix7PsAnzCa0aBiOHaYSUkPXbqDVUTDy9X4oEOsZYuzbgoY+otkoMQ=="],
"@impeccable/cli-linux-x64": ["@impeccable/cli-linux-x64@0.1.1", "", { "os": "linux", "cpu": "x64", "bin": { "impeccable-linux-x64": "bin/impeccable" } }, "sha512-wPul+V7w9g0MZAgFmJJvEOXpy8AYt4htmLQJIx7XvakSp8CS+PYBjEFqmWSW41NlrNAaifzDKdUe6rzeRli97A=="],
"@impeccable/cli-windows-x64": ["@impeccable/cli-windows-x64@0.1.1", "", { "os": "win32", "cpu": "x64", "bin": { "impeccable-windows-x64": "bin/impeccable.exe" } }, "sha512-dqcQ8VQFschjA1iFzhKvO44UEzsbQQGHAZW3MmsxJbqo/uSDENIL8WA5l4K8Q9wqjf4Kbc/H1hqo6MjOuQw85A=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+6 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "impeccable", "name": "impeccable",
"version": "4.0.1", "version": "4.0.2",
"author": "Paul Bakaus", "author": "Paul Bakaus",
"description": "Design skills, commands, and anti-pattern detection for AI coding agents", "description": "Design skills, commands, and anti-pattern detection for AI coding agents",
"keywords": [ "keywords": [
@@ -71,11 +71,11 @@
"check:engine-release": "node scripts/check-engine-release.mjs" "check:engine-release": "node scripts/check-engine-release.mjs"
}, },
"optionalDependencies": { "optionalDependencies": {
"@impeccable/cli-darwin-arm64": "0.1.0", "@impeccable/cli-darwin-arm64": "0.1.1",
"@impeccable/cli-darwin-x64": "0.1.0", "@impeccable/cli-darwin-x64": "0.1.1",
"@impeccable/cli-linux-x64": "0.1.0", "@impeccable/cli-linux-x64": "0.1.1",
"@impeccable/cli-linux-arm64": "0.1.0", "@impeccable/cli-linux-arm64": "0.1.1",
"@impeccable/cli-windows-x64": "0.1.0" "@impeccable/cli-windows-x64": "0.1.1"
}, },
"devDependencies": { "devDependencies": {
"@ai-sdk/anthropic": "^4.0.7", "@ai-sdk/anthropic": "^4.0.7",
+1 -1
View File
@@ -1 +1 @@
0.1.0 0.1.1
+6 -4
View File
@@ -7834,7 +7834,6 @@
if (editBadgeEl && editBadgeEl.style.display !== 'none') renderEditBadge('idle-disabled'); if (editBadgeEl && editBadgeEl.style.display !== 'none') renderEditBadge('idle-disabled');
showBar('generating'); showBar('generating');
saveSession(); saveSession();
sendCheckpoint('generate_started');
writeScrollY(window.scrollY); writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect(); if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId); variantObserver = startVariantObserver(currentSessionId);
@@ -7916,7 +7915,6 @@
showBar('generating'); showBar('generating');
startScrollTracking(); startScrollTracking();
saveSession(); saveSession();
sendCheckpoint('generate_started');
writeScrollY(window.scrollY); writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect(); if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId); variantObserver = startVariantObserver(currentSessionId);
@@ -8238,7 +8236,8 @@
// rasterization from delaying the fetch itself. // rasterization from delaying the fetch itself.
if (!hasAnnotations) { if (!hasAnnotations) {
basePayload.clientSentAt = Date.now(); basePayload.clientSentAt = Date.now();
await sendEvent(basePayload); const created = await sendEvent(basePayload);
if (created?.ok && currentSessionId === basePayload.id) sendCheckpoint('generate_started');
} }
let screenshotPath; let screenshotPath;
@@ -8279,7 +8278,10 @@
// is semantic input. Plain requests were already dispatched above. // is semantic input. Plain requests were already dispatched above.
if (hasAnnotations) { if (hasAnnotations) {
basePayload.clientSentAt = Date.now(); basePayload.clientSentAt = Date.now();
sendEvent(screenshotPath ? { ...basePayload, screenshotPath } : basePayload); const created = await sendEvent(screenshotPath ? { ...basePayload, screenshotPath } : basePayload);
// Capture/upload can take seconds. Progress before this acknowledgment
// refers to an unknown session and would clear our own active work.
if (created?.ok && currentSessionId === basePayload.id) sendCheckpoint('generate_started');
} }
} }
@@ -57,6 +57,12 @@
"readyPattern": "Local:\\s+https?://[^:]+:(\\d+)", "readyPattern": "Local:\\s+https?://[^:]+:(\\d+)",
"readyTimeoutMs": 120000, "readyTimeoutMs": 120000,
"steer": false, "steer": false,
"liveChrome": {
"annotations": {
"selector": "h1.hero-title",
"uploadDelayMs": 300
}
},
"pickSelector": "ul.expense-list", "pickSelector": "ul.expense-list",
"pickPosition": { "pickPosition": {
"x": 10, "x": 10,
+42 -1
View File
@@ -2,12 +2,53 @@ import { describe, it } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { runInNewContext } from 'node:vm';
const SOURCE = readFileSync(join(process.cwd(), 'skill/scripts/live-browser.js'), 'utf-8'); const SOURCE = readFileSync(join(process.cwd(), 'skill/scripts/live-browser.js'), 'utf-8');
const PENDING_DOCK_POSITION_SOURCE = SOURCE.match(/function positionPendingDock\(\) \{[\s\S]*?\n \}/)?.[0] || ''; const PENDING_DOCK_POSITION_SOURCE = SOURCE.match(/function positionPendingDock\(\) \{[\s\S]*?\n \}/)?.[0] || '';
const CAPTURE_AND_EMIT_SOURCE = SOURCE.match(/async function captureAndEmit\([\s\S]*?\n \}/)?.[0] || ''; const CAPTURE_AND_EMIT_SOURCE = SOURCE.match(/async function captureAndEmit\([\s\S]*?\n \}/)?.[0] || '';
describe('live-browser source contracts', () => { describe('live-browser source contracts', () => {
it('does not checkpoint a generation before captureAndEmit creates its session', () => {
for (const name of ['handleGo', 'handleInsertCreate']) {
const body = SOURCE.match(new RegExp(`function ${name}\\(\\) \\{[\\s\\S]*?\\n \\}`))?.[0];
assert.ok(body);
assert.doesNotMatch(body, /sendCheckpoint\('generate_started'\)/);
}
});
for (const annotated of [false, true]) {
for (const outcome of ['created', 'failed', 'superseded']) {
it(`${annotated ? 'annotated' : 'plain'} generation checkpoints only its acknowledged current session (${outcome})`, async () => {
const capture = Promise.withResolvers();
const creation = Promise.withResolvers();
const events = [];
const context = {
currentSessionId: 'session-a', state: 'GENERATING', PORT: 1234, TOKEN: 'test',
console, Date,
captureElementToBlob: () => capture.promise,
showShaderOverlay() {},
fetch: async () => ({ ok: true, json: async () => ({ path: '/annotation.png' }) }),
sendEvent: async (payload) => { events.push(payload.type); return creation.promise; },
sendCheckpoint: (reason) => events.push(reason),
};
const emit = runInNewContext(`(${CAPTURE_AND_EMIT_SOURCE})`, context);
const pending = emit({}, { type: 'generate', id: 'session-a' }, {
comments: annotated ? [{ text: 'change title' }] : [], strokes: [],
}, {});
await new Promise(resolve => setImmediate(resolve));
assert.deepEqual(events, annotated ? [] : ['generate']);
capture.resolve({ blob: {}, paper: 'white' });
await new Promise(resolve => setImmediate(resolve));
assert.deepEqual(events, ['generate'], 'capture/upload must not checkpoint before creation is acknowledged');
if (outcome === 'superseded') context.currentSessionId = 'session-b';
creation.resolve(outcome === 'failed' ? null : { ok: true });
await pending;
assert.deepEqual(events, outcome === 'created' ? ['generate', 'generate_started'] : ['generate']);
});
}
}
it('reports foreground poll connectivity without a background worker dependency', () => { it('reports foreground poll connectivity without a background worker dependency', () => {
assert.match( assert.match(
SOURCE, SOURCE,
@@ -29,7 +70,7 @@ describe('live-browser source contracts', () => {
); );
assert.match( assert.match(
CAPTURE_AND_EMIT_SOURCE, CAPTURE_AND_EMIT_SOURCE,
/if \(hasAnnotations\) \{[\s\S]*?basePayload\.clientSentAt = Date\.now\(\);\s*sendEvent\(screenshotPath \? \{ \.\.\.basePayload, screenshotPath \} : basePayload\);\s*\}/, /if \(hasAnnotations\) \{[\s\S]*?basePayload\.clientSentAt = Date\.now\(\);\s*const created = await sendEvent\(screenshotPath \? \{ \.\.\.basePayload, screenshotPath \} : basePayload\);/,
'annotated generation should dispatch exactly after capture and upload resolve', 'annotated generation should dispatch exactly after capture and upload resolve',
); );
}); });
+14
View File
@@ -7,6 +7,7 @@ import {
MANUAL_EDIT_SYSTEM_INSTRUCTIONS, MANUAL_EDIT_SYSTEM_INSTRUCTIONS,
VARIANT_SYSTEM_INSTRUCTIONS, VARIANT_SYSTEM_INSTRUCTIONS,
createLlmAgent, createLlmAgent,
llmRequestSettings,
parseManualEditResponse, parseManualEditResponse,
parseVariantResponse, parseVariantResponse,
progressiveVariantGuidance, progressiveVariantGuidance,
@@ -19,6 +20,19 @@ import {
validateVariantVisibleCopy, validateVariantVisibleCopy,
} from './live-e2e/agents/llm-agent.mjs'; } from './live-e2e/agents/llm-agent.mjs';
describe('live-e2e LLM request settings', () => {
it('explicitly selects low-effort DeepSeek thinking for bounded JSON edit requests', () => {
assert.deepEqual(llmRequestSettings('deepseek'), {
thinking: { type: 'enabled' }, output_config: { effort: 'low' },
});
});
it('leaves other providers unchanged', () => {
assert.deepEqual(llmRequestSettings('anthropic'), {});
assert.deepEqual(llmRequestSettings('openai'), {});
});
});
describe('live-e2e LLM agent provider config', () => { describe('live-e2e LLM agent provider config', () => {
it('defaults to OpenAI gpt-5.6-terra at medium reasoning effort', () => { it('defaults to OpenAI gpt-5.6-terra at medium reasoning effort', () => {
const config = resolveLlmAgentConfig({}, {}); const config = resolveLlmAgentConfig({}, {});
+10 -6
View File
@@ -1283,17 +1283,21 @@ for (const { name, fixture } of fixtures) {
const pickSelector = annotation.selector || fixture.runtime.pickSelector || 'h1.hero-title'; const pickSelector = annotation.selector || fixture.runtime.pickSelector || 'h1.hero-title';
try { try {
await waitForHandshake(page); await waitForHandshake(page);
if (annotation.uploadDelayMs) {
await page.route('**/annotation?*', async (route) => {
await new Promise(resolve => setTimeout(resolve, annotation.uploadDelayMs));
await route.continue();
});
}
if (fixture.runtime.preActions) await runPreActions(page, fixture.runtime.preActions); if (fixture.runtime.preActions) await runPreActions(page, fixture.runtime.preActions);
await pickElement(page, pickSelector, { resetPickMode: true }); await pickElement(page, pickSelector, { resetPickMode: true });
await drawAnnotationPinAndStroke(page, { await drawAnnotationPinAndStroke(page, {
comment: annotation.comment || 'Make this selected element easier to scan', comment: annotation.comment || 'Make this selected element easier to scan',
}); });
await clickGo(page); await clickGo(page);
await waitForCyclingRobust(page, 3, { // A reload would mask a checkpoint-before-creation race by adopting
agentMode, // the session again. Annotated generation must complete in this tab.
preActions: fixture.runtime.preActions, await waitForCycling(page, 3, { timeout: agentMode === 'llm' ? 180_000 : 30_000 });
log: (m) => t.diagnostic(m),
});
const generateEvent = recordedGenerateEvents.at(-1); const generateEvent = recordedGenerateEvents.at(-1);
await assertAnnotationUploadEvent(generateEvent); await assertAnnotationUploadEvent(generateEvent);
@@ -1302,7 +1306,7 @@ for (const { name, fixture } of fixtures) {
const sourceFile = await locateSessionFile(session.appRoot); const sourceFile = await locateSessionFile(session.appRoot);
const svelteComponentTarget = svelteComponentTargetFor(sourceFile); const svelteComponentTarget = svelteComponentTargetFor(sourceFile);
await clickNext(page); await cycleToVariant(page, 2, 3);
assert.equal(await getVisibleVariant(page), 2, 'variant 2 visible after annotated generate'); assert.equal(await getVisibleVariant(page), 2, 'variant 2 visible after annotated generate');
await clickAccept(page, { expectedVariant: 2 }); await clickAccept(page, { expectedVariant: 2 });
await waitForBarHidden(page); await waitForBarHidden(page);
+18 -1
View File
@@ -248,6 +248,17 @@ function resolveProvider(opts, env) {
return 'openai'; return 'openai';
} }
export function llmRequestSettings(provider) {
// DeepSeek defaults to high-effort thinking, which can consume the entire
// bounded response before emitting the JSON these edit tests exercise.
// Low effort retains planning for the full live spec without inheriting
// the provider's high-effort default.
// https://api-docs.deepseek.com/guides/thinking_mode/
return provider === 'deepseek'
? { thinking: { type: 'enabled' }, output_config: { effort: 'low' } }
: {};
}
/** /**
* Anthropic-SDK-shaped shim over the `ai` SDK for OpenAI models, so the * Anthropic-SDK-shaped shim over the `ai` SDK for OpenAI models, so the
* three text-only call sites in this file stay provider-agnostic. system * three text-only call sites in this file stay provider-agnostic. system
@@ -331,6 +342,7 @@ export async function createLlmAgent(opts = {}) {
try { try {
response = await client.messages.create( response = await client.messages.create(
{ {
...llmRequestSettings(provider),
model, model,
temperature: 0, temperature: 0,
max_tokens: 16000, max_tokens: 16000,
@@ -476,6 +488,7 @@ export async function createLlmAgent(opts = {}) {
try { try {
response = await client.messages.create( response = await client.messages.create(
{ {
...llmRequestSettings(provider),
model, model,
temperature: 0, temperature: 0,
max_tokens: 16000, max_tokens: 16000,
@@ -605,11 +618,15 @@ export async function createLlmAgent(opts = {}) {
].join('\n'); ].join('\n');
const response = await client.messages.create({ const response = await client.messages.create({
...llmRequestSettings(provider),
model, model,
max_tokens: 4096, max_tokens: 4096,
system: systemBlocks(STEER_SYSTEM_INSTRUCTIONS), system: systemBlocks(STEER_SYSTEM_INSTRUCTIONS),
messages: [{ role: 'user', content: userMessage }], messages: [{ role: 'user', content: userMessage }],
}); }, provider === 'deepseek' ? {
maxRetries: LLM_REQUEST_MAX_RETRIES,
timeout: MANUAL_EDIT_REQUEST_TIMEOUT_MS,
} : {});
const cacheRead = response.usage?.cache_read_input_tokens ?? 0; const cacheRead = response.usage?.cache_read_input_tokens ?? 0;
const inputTokens = response.usage?.input_tokens ?? 0; const inputTokens = response.usage?.input_tokens ?? 0;
+7 -11
View File
@@ -26,8 +26,7 @@ import {
clickEditCopy, clickEditCopy,
clickExitLiveMode, clickExitLiveMode,
clickGo, clickGo,
clickNext, cycleToVariant,
clickPrev,
clickSaveEdit, clickSaveEdit,
drawAnnotationPinAndStroke, drawAnnotationPinAndStroke,
editTextLeaf, editTextLeaf,
@@ -40,6 +39,7 @@ import {
waitForBarHidden, waitForBarHidden,
waitForCycling, waitForCycling,
waitForHandshake, waitForHandshake,
waitForVariantSettled,
} from './live-e2e/ui.mjs'; } from './live-e2e/ui.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -281,7 +281,7 @@ async function runAnnotationGenerateFlow({ page, tmp, evidence }) {
const generateEvent = latestJournalEvent(tmp, (event) => event.type === 'generate' && event.screenshotPath); const generateEvent = latestJournalEvent(tmp, (event) => event.type === 'generate' && event.screenshotPath);
await assertAnnotationUploadEvent(generateEvent); await assertAnnotationUploadEvent(generateEvent);
assert.ok(existsSync(generateEvent.screenshotPath), 'annotation screenshot file exists'); assert.ok(existsSync(generateEvent.screenshotPath), 'annotation screenshot file exists');
await clickNext(page); await cycleTo(page, 2);
await assertVariantCounter(page, 2, 3); await assertVariantCounter(page, 2, 3);
await evidence.capture('annotation-cycle'); await evidence.capture('annotation-cycle');
await clickDiscard(page); await clickDiscard(page);
@@ -514,14 +514,10 @@ async function clickPendingTrash(page) {
} }
async function cycleTo(page, target) { async function cycleTo(page, target) {
for (let i = 0; i < 6; i++) { // Component imports finish after the counter changes. Do not send the next
const visible = await getVisibleVariant(page); // click (or reload) while the previous variant is still mounting.
if (visible === target) return; await cycleToVariant(page, target, 3);
if (visible == null) await page.waitForTimeout(250); await waitForVariantSettled(page, target, 3);
else if (visible < target) await clickNext(page);
else await clickPrev(page);
}
assert.equal(await getVisibleVariant(page), target, `variant ${target} visible`);
} }
async function waitForVisibleCycling(page, count, { timeout }) { async function waitForVisibleCycling(page, count, { timeout }) {