@gakim-digital/dexter-bridge 0.13.9 → 0.13.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.13.9",
3
+ "version": "0.13.11",
4
4
  "description": "Local bridge for InstaWebAI and Dexter — runs Codex, Claude Code, or OpenCode on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -98,6 +98,7 @@ export function harnessMcpProcessEnvironment(
98
98
  const companionModelSessions = new Map();
99
99
  const sharedProviderAdapters = new Map();
100
100
  const MAX_COMPANION_MODEL_SESSIONS = 64;
101
+ const FRAMER_CONTINUATION_NOTE = 'The user sent a new message in this conversation. Your instructions and the Framer guidance from earlier in this session still apply, and so does what you learned: what you changed, and what Framer rejected and why. Do not repeat an approach Framer already rejected; use what its error told you. The project inventory and assignment below are current. The user may have changed the project since your last reads, so read layers again before you edit them.';
101
102
 
102
103
  function enabledFlag(value, fallback = false) {
103
104
  const raw = String(value ?? '')
@@ -2432,10 +2433,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2432
2433
  const webflowOutcome = run?.outcome?.kind === 'webflow-site';
2433
2434
  if (webflowOutcome) assertWebflowHarnessAgent(definition.id);
2434
2435
  const designOutcome = framerAgentOutcome || webflowOutcome;
2435
- // Every Dexter message starts a fresh provider session. The assignment
2436
- // already carries the recent conversation, and resuming would make every
2437
- // turn of this run re-read the whole previous run as well.
2438
- const sessionKey = designOutcome
2436
+ // A Framer conversation keeps one provider session: Dexter resumes it on
2437
+ // each message, so the model keeps what it tried, what Framer rejected, and
2438
+ // why. Webflow messages still start a fresh session.
2439
+ const sessionKey = webflowOutcome
2439
2440
  ? run?.runId || companionSessionKey(run)
2440
2441
  : companionSessionKey(run);
2441
2442
  const infrastructureAbortController = new AbortController();
@@ -2657,7 +2658,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2657
2658
  }
2658
2659
  };
2659
2660
  const controlMonitor = monitorControl();
2660
- const rememberedSession = designOutcome
2661
+ const rememberedSession = webflowOutcome
2661
2662
  ? null
2662
2663
  : rememberedCompanionSession(sessionKey) ||
2663
2664
  (run?.outcome?.session?.providerSessionId
@@ -2859,6 +2860,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2859
2860
  const callStartedAt = Date.now();
2860
2861
  const usageAccumulator = createCompanionUsageAccumulator(definition.id);
2861
2862
  let providerAttempts = 0;
2863
+ let sessionResumed = false;
2862
2864
  let result;
2863
2865
  try {
2864
2866
  const preflight = designOutcome
@@ -2882,32 +2884,42 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2882
2884
  // them from the cache instead of writing them again.
2883
2885
  let systemPromptFile = null;
2884
2886
  let systemPromptBytes = 0;
2885
- if (
2886
- designOutcome
2887
- && definition.id === 'claude-code'
2888
- && !options.providerAdapter?.runOutcome
2889
- ) {
2887
+ // A resumed Framer session already holds the instructions and guidance,
2888
+ // so it gets only this message's task.
2889
+ let continuationPrompt = null;
2890
+ if (designOutcome) {
2890
2891
  const { instructions, task } = webflowOutcome
2891
2892
  ? buildWebflowOutcomePrompt(executionOutcome, product, promptContext)
2892
2893
  : buildFramerOutcomePrompt(executionOutcome, product, promptContext);
2893
- const systemPrompt = `${(webflowOutcome ? claudeWebflowOutcomeSystemPrompt : claudeOutcomeSystemPrompt)(product)}\n\n${instructions}`;
2894
- systemPromptFile = writeClaudeSystemPromptFile(systemPrompt);
2895
- systemPromptBytes = Buffer.byteLength(systemPrompt, 'utf8');
2896
- prompt = task;
2894
+ if (definition.id === 'claude-code' && !options.providerAdapter?.runOutcome) {
2895
+ const systemPrompt = `${(webflowOutcome ? claudeWebflowOutcomeSystemPrompt : claudeOutcomeSystemPrompt)(product)}\n\n${instructions}`;
2896
+ systemPromptFile = writeClaudeSystemPromptFile(systemPrompt);
2897
+ systemPromptBytes = Buffer.byteLength(systemPrompt, 'utf8');
2898
+ prompt = task;
2899
+ } else {
2900
+ prompt = buildOutcomePrompt(executionOutcome, product, promptContext);
2901
+ }
2902
+ if (framerAgentOutcome) continuationPrompt = `${FRAMER_CONTINUATION_NOTE}\n\n${task}`;
2897
2903
  } else {
2898
2904
  prompt = buildOutcomePrompt(executionOutcome, product, promptContext);
2899
2905
  }
2900
2906
  const unresolvedTargets = preflight?.unresolvedTargets || [];
2901
- if (unresolvedTargets.length > 0) {
2902
- prompt = [
2903
- prompt,
2904
- '',
2905
- `These selected layer IDs do not resolve to native nodes on the active page: ${unresolvedTargets.map((target) => target.id).join(', ')}. The user still means those layers. Locate them by name and position in the provided context and the live tree, treat the matching native nodes as the selection, and ask only if no layer clearly matches.`,
2906
- ].join('\n');
2907
- }
2908
- if (materializedNativeSkills) {
2909
- prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
2910
- }
2907
+ const withRunNotes = (text) => {
2908
+ let result = text;
2909
+ if (unresolvedTargets.length > 0) {
2910
+ result = [
2911
+ result,
2912
+ '',
2913
+ `These selected layer IDs do not resolve to native nodes on the active page: ${unresolvedTargets.map((target) => target.id).join(', ')}. The user still means those layers. Locate them by name and position in the provided context and the live tree, treat the matching native nodes as the selection, and ask only if no layer clearly matches.`,
2914
+ ].join('\n');
2915
+ }
2916
+ if (materializedNativeSkills) {
2917
+ result = claudeNativeSkillPrompt(result, materializedNativeSkills);
2918
+ }
2919
+ return result;
2920
+ };
2921
+ prompt = withRunNotes(prompt);
2922
+ if (continuationPrompt) continuationPrompt = withRunNotes(continuationPrompt);
2911
2923
  harnessTools.configurePromptContext?.({
2912
2924
  doctrineLoaded: Boolean(run?.doctrine),
2913
2925
  doctrineVersion: run?.doctrineVersion,
@@ -2954,6 +2966,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2954
2966
  turnPrompt,
2955
2967
  resumeSessionIdOverride,
2956
2968
  step = 0,
2969
+ continuation = null,
2957
2970
  ) => {
2958
2971
  // User attachments ride on the first turn as native image input, exactly
2959
2972
  // as if pasted into the agent; follow-up turns already have them in context.
@@ -2964,6 +2977,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2964
2977
  sessionId: sessionKey,
2965
2978
  resumeSessionId: resumeSessionIdOverride,
2966
2979
  prompt: turnPrompt,
2980
+ ...(continuation ? { continuationPrompt: continuation } : {}),
2967
2981
  model: adapterInvocationModel(run, selectedModel, definition.id),
2968
2982
  outputSchema: schema,
2969
2983
  responseContract: STRUCTURED_RESPONSE_CONTRACTS.OUTCOME,
@@ -2984,6 +2998,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2984
2998
  onProgress: progress,
2985
2999
  });
2986
3000
  providerAttempts += providerAttemptCount(providerResult);
3001
+ if (step === 0) sessionResumed = providerResult?.sessionContinued === true;
2987
3002
  return providerResult;
2988
3003
  }
2989
3004
  const runtime =
@@ -3001,7 +3016,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
3001
3016
  let processResult;
3002
3017
  try {
3003
3018
  providerAttempts += 1;
3004
- processResult = await callLocalJsonAgent(definition.id, turnPrompt, {
3019
+ processResult = await callLocalJsonAgent(definition.id, resumeSessionId && continuation ? continuation : turnPrompt, {
3005
3020
  ...options,
3006
3021
  model: selectedModelId,
3007
3022
  timeoutMs: outcomeInactivityTimeoutMs,
@@ -3024,8 +3039,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
3024
3039
  ...(mcpConfig ? { mcpConfig } : {}),
3025
3040
  onProgress: progress,
3026
3041
  });
3042
+ if (step === 0) sessionResumed = Boolean(resumeSessionId);
3027
3043
  } catch (error) {
3028
3044
  if (!resumeSessionId || !resumeFailure(error)) throw error;
3045
+ if (step === 0) sessionResumed = false;
3029
3046
  providerAttempts += 1;
3030
3047
  processResult = await callLocalJsonAgent(definition.id, turnPrompt, {
3031
3048
  ...options,
@@ -3060,6 +3077,8 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
3060
3077
  const providerExecution = executeProviderOutcome(
3061
3078
  prompt,
3062
3079
  rememberedSession?.providerSessionId,
3080
+ 0,
3081
+ continuationPrompt,
3063
3082
  );
3064
3083
  const infrastructureFailure = harnessTools
3065
3084
  .waitForFatalInfrastructure()
@@ -3274,6 +3293,8 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
3274
3293
  : normalized.status,
3275
3294
  durationMs: Date.now() - callStartedAt,
3276
3295
  providerAttemptCount: Math.max(1, providerAttempts),
3296
+ resumed: sessionResumed,
3297
+ resumeSessionAvailable: Boolean(rememberedSession),
3277
3298
  ...usageSnapshot.tokenUsage,
3278
3299
  estimatedCostUSD: usageSnapshot.estimatedCostUSD,
3279
3300
  },
@@ -0,0 +1,81 @@
1
+ // How to work around properties Framer rejects. Framer's message says what is
2
+ // wrong; each fix says what to change instead, so the model does not retry the
3
+ // same write or give up. Add an entry when production runs show a rejection
4
+ // that keeps coming back, and test it in framerErrorFixes.test.js.
5
+ //
6
+ // The first matching fix wins for an error.
7
+ export const FRAMER_ERROR_FIXES = [
8
+ {
9
+ id: 'custom-cursor-needs-ancestor',
10
+ matches: (error) => /customCursor/i.test(error.property) && /ancestor to provide a custom cursor/i.test(error.message),
11
+ fix: 'Framer applies a custom cursor variant only to a layer whose ancestor provides the custom cursor component. Set the cursor component on an ancestor first, such as the breakpoint or the root of the shared component the layer sits in, then set the variant on the layer; or set it on a layer that already has a cursor ancestor. If the project has no cursor component, say so instead of retrying. Read guides/Cursors/part-1.md and its later parts with framer_instructions if you are unsure.',
12
+ },
13
+ {
14
+ id: 'cursor-on-text-node',
15
+ matches: (error) => /^(customCursor|cursor)/i.test(error.property) && /virtual (rich-text|textBlock) node/i.test(error.message),
16
+ fix: 'A cursor cannot be set on a text node. Set it on the frame or link that contains the text.',
17
+ },
18
+ {
19
+ id: 'aspect-ratio-needs-sizing',
20
+ matches: (error) => error.property === 'aspectRatio' && /auto or fit-image sizing/i.test(error.message),
21
+ fix: 'aspectRatio applies only when the layer\'s width and height are not auto or fit-image. In the same patch, give the layer a fixed or fill width and height as the node-properties guidance describes, then set aspectRatio. For images in a CMS collection list, change the image layer inside the repeated item, not each copy. Take a screenshot afterwards to check the crop.',
22
+ },
23
+ {
24
+ id: 'text-style-preset-controls',
25
+ matches: (error) => /preset-controlled text properties|text style preset is set/i.test(error.message),
26
+ fix: 'This text uses a text style preset, which controls its font, size, weight, and spacing. Either change the preset itself, which changes every text that uses it, apply a different preset, or remove the preset from this layer before setting those properties. If changing a shared preset would affect text the user did not mention, say so.',
27
+ },
28
+ {
29
+ id: 'text-style-on-runs',
30
+ matches: (error) => /Expected text in the same patch or styling on inserted TextRun children/i.test(error.message)
31
+ || /to virtual (rich-text|textBlock) node/i.test(error.message),
32
+ fix: 'Text styling belongs on the text runs, not on the text layer\'s frame. Set it in the same patch that sets the text, or style the TextRun children as the tree API guidance describes.',
33
+ },
34
+ {
35
+ id: 'rich-text-field-blocks',
36
+ matches: (error) => /^\$control__/.test(error.property) && /TextBlock/.test(error.message),
37
+ fix: 'A rich text field or control takes plain text, not a list of blocks. Set it to the first paragraph\'s plain text, then insert each further heading, paragraph, and list with tree.insert, using parent "<layer or item id>/<field id>" and a tag on every TextBlock, as the preloaded rich text guidance shows. Insert long text a few sections per framer_edit. Do not put the whole text in one plain-text value: it becomes a single paragraph and loses every heading.',
38
+ },
39
+ {
40
+ id: 'layout-template-controls',
41
+ matches: (error) => /layout-template-controlled properties/i.test(error.message),
42
+ fix: 'This page uses a layout template, which controls the breakpoint\'s layout and stack. Change the layout template, which affects every page that uses it, or change the content inside the page instead.',
43
+ },
44
+ {
45
+ id: 'unsupported-property',
46
+ matches: (error) => /is not a supported property/i.test(error.message),
47
+ fix: 'Framer has no property by that name. Find the right one in the preloaded node-properties guidance instead of guessing: for example, bold is a font weight on the text runs, and hover styles are variants or interactions.',
48
+ },
49
+ {
50
+ id: 'font-unavailable',
51
+ matches: (error) => /font/i.test(`${error.property} ${error.message}`)
52
+ && /not (loaded|available|found|installed)|unknown font|unavailable/i.test(error.message),
53
+ fix: 'A font Framer rejected is not available to this project. Follow the preloaded project APIs guidance: resolve the family through api.fonts before setting it, or use a family the project already uses.',
54
+ },
55
+ ];
56
+
57
+ function normalized(error) {
58
+ return {
59
+ property: String(error?.property || ''),
60
+ message: String(error?.message || ''),
61
+ };
62
+ }
63
+
64
+ export function framerErrorFix(error) {
65
+ const value = normalized(error);
66
+ return FRAMER_ERROR_FIXES.find((entry) => entry.matches(value)) || null;
67
+ }
68
+
69
+ // One line per distinct fix, so a batch of the same rejection reads once.
70
+ export function framerErrorFixHint(errors = []) {
71
+ const fixes = [];
72
+ for (const error of errors) {
73
+ const entry = framerErrorFix(error);
74
+ if (entry && !fixes.includes(entry)) fixes.push(entry);
75
+ }
76
+ if (!fixes.length) return null;
77
+ return [
78
+ ...fixes.map((entry) => entry.fix),
79
+ 'Change what Framer\'s message asks for in your next framer_edit; do not send the same property again unchanged.',
80
+ ].join(' ');
81
+ }
@@ -1,5 +1,6 @@
1
1
  import crypto from 'node:crypto';
2
2
  import jsTokens from 'js-tokens';
3
+ import { framerErrorFixHint } from './framerErrorFixes.js';
3
4
  import { createFramerMcpClient } from './framerMcpClient.js';
4
5
  import { withUserMessages } from './userMessages.js';
5
6
  import {
@@ -38,6 +39,7 @@ const GUIDANCE_PATHS = [
38
39
  'references/script/node-properties/part-1.md',
39
40
  'references/script/node-properties/part-2.md',
40
41
  'references/script/project-apis/part-1.md',
42
+ 'references/project/rich-text/part-1.md',
41
43
  ];
42
44
  const CONTEXT_PATH = 'context.md';
43
45
  const PRELOADED_PATHS = new Set([CONTEXT_PATH, ...GUIDANCE_PATHS]);
@@ -50,7 +52,6 @@ const MAX_RESULT_CHARS = 6_000;
50
52
  // after this long is told to start building.
51
53
  const FIRST_EDIT_NUDGE_MS = 60_000;
52
54
  const FIRST_EDIT_NUDGE = 'If this request changes the canvas, nothing is on it yet and you know enough to start: make your first framer_edit now with a small first section, such as the navigation and the hero headline, then build the rest two sections per edit. Read more only when a specific detail blocks the next edit. If the user asked only for an answer or an analysis, keep to that.';
53
- const FONT_ERROR_HINT = 'A font Framer rejected is not available to this project. Follow the preloaded project APIs guidance: resolve the family through api.fonts before setting it, or use a family the project already uses. Fix it in your next framer_edit, not at the end.';
54
55
  const ACTIVE_PAGE_OUTLINE_DEPTH = 2;
55
56
  // Framer returns the previous layout for about 50ms after an edit.
56
57
  const LAYOUT_SETTLE_MS = 150;
@@ -254,6 +255,21 @@ function codeOutsideLiterals(code) {
254
255
  return visible;
255
256
  }
256
257
 
258
+ // An edit program as it can be kept in run logs: code, layer ids, and short
259
+ // values stay; longer strings, which are usually the site's copy, do not.
260
+ // Kept as lines so no single log string is long enough to be dropped.
261
+ const LOGGED_LITERAL_CHARS = 40;
262
+ export function loggedProgram(code) {
263
+ let text = '';
264
+ for (const token of jsTokens(String(code || ''))) {
265
+ const literal = LITERAL_TOKEN_TYPES.has(token.type);
266
+ text += literal && token.value.length > LOGGED_LITERAL_CHARS + 2
267
+ ? `"[omitted ${token.value.length - 2} chars]"`
268
+ : token.value;
269
+ }
270
+ return text.split('\n').slice(0, 400).map((line) => line.slice(0, 1_000));
271
+ }
272
+
257
273
  // Text blocks inside a layer have virtual ids such as "v:dD8FHq_BL:0"; they
258
274
  // belong to the layer named in the middle.
259
275
  function layerIdOf(id) {
@@ -385,10 +401,6 @@ export function fontFamiliesIn(value, found = new Set()) {
385
401
  return [...found].slice(0, 20);
386
402
  }
387
403
 
388
- function isFontError(error) {
389
- return /font/i.test(`${error?.property || ''} ${error?.message || ''}`);
390
- }
391
-
392
404
  function boundedResult(value) {
393
405
  if (value === undefined) return undefined;
394
406
  let text;
@@ -1224,6 +1236,8 @@ export function createFramerAgentToolRuntime({
1224
1236
  let lastMutationAt = 0;
1225
1237
  let lastReadAfterMutationAt = 0;
1226
1238
  let lastVisualAt = 0;
1239
+ // Tool calls whose result handed the model at least one image.
1240
+ let screenshotCalls = 0;
1227
1241
  let firstToolAt = null;
1228
1242
  let guidance = { content: '', inventory: '', loaded: false, files: [] };
1229
1243
  const mutationReceipts = [];
@@ -1528,6 +1542,10 @@ export function createFramerAgentToolRuntime({
1528
1542
  return {
1529
1543
  visualRequired: required.has('visual'),
1530
1544
  visualCompleted: required.has('visual') && mutationCalls > 0 && lastVisualAt >= lastMutationAt,
1545
+ // The model saw the result after its last change, whether or not its
1546
+ // plan declared visual verification.
1547
+ visualAfterLastChange: mutationCalls > 0 && lastVisualAt >= lastMutationAt,
1548
+ screenshotCalls,
1531
1549
  completed: missingChecks.length === 0,
1532
1550
  missingChecks,
1533
1551
  };
@@ -1628,6 +1646,13 @@ export function createFramerAgentToolRuntime({
1628
1646
  await snapshotUnchanged();
1629
1647
  const result = await client.exec(wrapProgram(program, { allowContentDeletion }));
1630
1648
  const applied = applyReceipt(result, screenshotNodeIds);
1649
+ // An edit Framer rejected, and the edit after it, keep their program
1650
+ // in the run log, so a later fix shows what worked instead.
1651
+ const logged = mutationReceipts.at(-1) === applied.receipt;
1652
+ const previousReceipt = mutationReceipts.at(logged ? -2 : -1);
1653
+ if (logged && (applied.receipt.propertyErrors.length || previousReceipt?.propertyErrors?.length)) {
1654
+ applied.receipt.program = loggedProgram(program);
1655
+ }
1631
1656
  requirements = renameRequirementNodes(requirements, applied.receipt.renamedIds);
1632
1657
  const refused = Array.isArray(applied.output?.result?.dexterRefused) ? applied.output.result.dexterRefused : [];
1633
1658
  if (refused.length) {
@@ -1661,6 +1686,7 @@ export function createFramerAgentToolRuntime({
1661
1686
  }
1662
1687
  if (applied.images.length) lastVisualAt = Date.now();
1663
1688
  const due = evidenceStillDue();
1689
+ const propertyErrorHint = framerErrorFixHint(applied.receipt.propertyErrors);
1664
1690
  const inner = applied.output.result && typeof applied.output.result === 'object'
1665
1691
  ? applied.output.result
1666
1692
  : {};
@@ -1694,7 +1720,7 @@ export function createFramerAgentToolRuntime({
1694
1720
  }
1695
1721
  : {}),
1696
1722
  propertyErrors: applied.receipt.propertyErrors,
1697
- ...(applied.receipt.propertyErrors.some(isFontError) ? { propertyErrorHint: FONT_ERROR_HINT } : {}),
1723
+ ...(propertyErrorHint ? { propertyErrorHint } : {}),
1698
1724
  ...(applied.receipt.linterWarnings.length ? { linterWarnings: applied.receipt.linterWarnings } : {}),
1699
1725
  codeFiles: applied.receipt.codeFiles,
1700
1726
  ...(applied.receipt.branchChange ? { branchChange: applied.receipt.branchChange } : {}),
@@ -1811,6 +1837,7 @@ export function createFramerAgentToolRuntime({
1811
1837
  const { modelImages, ...rest } = payload;
1812
1838
  const ready = await modelReadyImages(modelImages);
1813
1839
  payload = ready.length ? { ...rest, screenshotsAttached: ready.length, modelImages: ready } : rest;
1840
+ if (ready.length) screenshotCalls += 1;
1814
1841
  }
1815
1842
  reportActivity({
1816
1843
  kind: 'tool_active',
@@ -1971,6 +1998,8 @@ export function createFramerAgentToolRuntime({
1971
1998
  for (const error of propertyErrors) weighedPropertyErrors.add(propertyErrorKey(error));
1972
1999
  if (propertyErrors.length) {
1973
2000
  facts.push(`Framer rejected these properties, so they are not set: ${propertyErrors.map((error) => `${error.nodeId}.${error.property}: ${error.message}`).join('; ')}.`);
2001
+ const hint = framerErrorFixHint(propertyErrors);
2002
+ if (hint) facts.push(hint);
1974
2003
  }
1975
2004
  if (pendingRepair?.claimed?.length) {
1976
2005
  facts.push(`Your changedNodeIds lists layers no edit touched: ${pendingRepair.claimed.join(', ')}.`);
@@ -2093,9 +2122,10 @@ export function createFramerAgentToolRuntime({
2093
2122
  output: 'No layer changed for a summary that claimed a change.',
2094
2123
  });
2095
2124
  }
2096
- // A rejected property the model was shown and then finished without is
2097
- // its judgment, such as reaching the goal another way.
2098
- const propertyErrors = lingeringPropertyErrors().filter((error) => !weighedPropertyErrors.has(propertyErrorKey(error)));
2125
+ // A property Framer rejected and no later edit of that layer replaced was
2126
+ // never applied. The model was shown it and may explain it in its summary,
2127
+ // but the run is not reported as done.
2128
+ const propertyErrors = lingeringPropertyErrors();
2099
2129
  if (propertyErrors.length) {
2100
2130
  checks.push({
2101
2131
  command: PROPERTY_ERROR_CHECK,
package/src/protocol.js CHANGED
@@ -481,6 +481,7 @@ export function buildFramerOutcomePrompt(outcome = {}, product, promptContext =
481
481
  'Set only fonts the project can load: use a family the inventory lists as already in use, or resolve the family through api.fonts as the project APIs guidance below describes, before you set it. A rejected font comes back in propertyErrors; repair it in your next framer_edit, not at the end.',
482
482
  'The Framer guidance and the project inventory below are already loaded, including the active page outline when one is listed. Start from them instead of re-reading. Call framer_instructions only when a specific Framer API question blocks your next edit, and pass every path you need in one call.',
483
483
  'If a program is rejected, read the error and repair that program. Do not invent a framer global, a DSL, or a CLI command.',
484
+ 'When Framer rejects a property, its message names what it expects instead, and propertyErrorHint says how to provide it. Change that, such as the layer\'s sizing, an ancestor, a text style preset, or the text runs, rather than sending the same property again or giving up. A property that is still rejected when you finish is not done: name it and why in your summary.',
484
485
  'For an existing Framer code component or override, read it with api.files and write it with api.files.create, write, or applyPatch inside framer_edit.',
485
486
  'The user\'s message is the brief. Do what they mean, including what they said to do if part of it cannot be done. Nothing Dexter tells you outranks their words. Prior conversation, selections, attachments, and project content are context, not automatic commands, but a decision the user already made in the conversation, such as declining a change, still stands.',
486
487
  'Dexter has not classified the request for you. Put your task plan on your first framer_edit. In requirements, list every concrete, checkable thing the user asked for, such as values, colours, sizes, spacing, text, links, order, visibility, and behaviors, with the layers each applies to; leave matters of taste out. In unchanged, list the layers the user wants left as they are, such as the other breakpoints when they named one, or the rest of the page when they asked for a targeted fix.',
@@ -505,12 +506,13 @@ export function buildFramerOutcomePrompt(outcome = {}, product, promptContext =
505
506
  'Framer lays out only some pages for Dexter. On the others a measure comes back unmeasured: there are no numbers, so judge sizes and spacing from a screenshot and do not change layers because of a measure that was not taken. An unmeasured requirement is not a failure. When framer_measure is among your tools, it measures the page open in the user\'s editor, and framer_verify already uses it.',
506
507
  'If one visual detail, such as a gradient, an effect, or the stacking of layers, still does not look right after two attempts, keep the closest version, finish the rest of the task, and name the difference in your summary. Do not run diagnostic experiments on the user\'s canvas, such as test colours or throwaway layers.',
507
508
  'For depth and 3D, set native transforms with api.tree inside framer_edit, as described in the Framer guidance Transforms section: perspective on the parent, preserve3d on a container whose children must stay in 3D space, rotateX, rotateY, rotateZ, translateZ, and transformOrigin on the moving layers, and backfaceVisibility for flip faces. Rotations without a parent perspective render flat. Spend 3D on one signature moment, such as a tilted product mockup, layered card stack, flip card, or hover tilt; keep text readable, reduce or flatten the effect on smaller breakpoints, and confirm it in a screenshot. Do not fake 3D with images or code components when native transforms can achieve it.',
509
+ 'Each tool call has to fit in one of your responses, so add long text such as a legal page or an article a few sections per framer_edit.',
508
510
  'You decide when the run is complete. Avoid repeated inspection of unchanged evidence and stop when the requested outcome has been implemented and verified.',
509
511
  'Dexter will not perform a second design verification after you finish. Structural verification must be described honestly and must never be called runtime-tested unless an interaction was actually exercised.',
510
512
  'Return blocked only when Framer authorization is missing or you need a decision the user has not made yet. Otherwise return completed once you have done what can be done. When part of the request cannot be done, or the user said to leave something as it is, explaining plainly what you did, what is left, and why is a complete answer.',
511
513
  'A greeting, thanks, or question that needs no project change is not blocked: answer it in summary without reading or editing the project and return completed.',
512
514
  'Set projectChanged to true only when this run applied a change to the Framer project, and list the ids of the layers you changed in changedNodeIds. The summary must describe only changes that were applied. If a requirement is still not met, or you could not find or change something, name it plainly in the summary; never describe it as done.',
513
- 'Some requests need something beyond the layers you may change, such as photos on other pages, a component shared across the site, or site-wide styles. If the user has not decided about it yet, ask one plain question that names it and what changing it affects, and return blocked; when they agree, Dexter gives you that reach in the next run. If they already said no, or told you what to do in that case, do not ask again: do what you can within what you may change, and explain plainly what is in the way, such as which shared setting causes it. Never mention access, scope, permissions, grants, or authorization.',
515
+ 'The open page and the selected layers show where the user is working. Start there, but they do not limit what you may change: when the user\'s words are about another page, a component shared across the site, the site-wide header, navigation, or footer, or site-wide styles, change those too, and say in your summary what else the change affects. Ask first, with one plain question that names it and what changing it affects, and return blocked, only when a shared change would also alter places the user did not mention and they have not decided about it. If they already said no, or told you what to do in that case, do not ask again: do what you can, and explain plainly what is in the way, such as which shared setting causes it. Never mention access, scope, permissions, grants, or authorization.',
514
516
  'Deleting CMS collections, items, or fields, or pages, needs the user\'s clear permission in this conversation; context.permissions.deleteContent is true when they gave it. A request to change, fix, or replace content is not permission to delete it. Without it, ask one plain question naming exactly what you would delete and return blocked, before building any replacement for it.',
515
517
  'If you changed layers before asking a question, say in your summary what you changed.',
516
518
  'When you return blocked, write the question itself in summary, since summary is what the user reads; blockedReason is a short note for Dexter.',
@@ -529,7 +531,7 @@ export function buildFramerOutcomePrompt(outcome = {}, product, promptContext =
529
531
  const task = [
530
532
  ...(selectionFirst
531
533
  ? [
532
- 'This is a selection-first run. Start from the selected nodes: they show what the user is pointing at. When the user\'s words name other layers on this page, or the setting they asked about sits on a parent of the selection, change that too, within the reach in the assignment.',
534
+ 'This is a selection-first run. Start from the selected nodes: they show what the user is pointing at. When the user\'s words name other layers or pages, or the setting they asked about sits on a parent of the selection, change that too.',
533
535
  'Use a focused live Framer read of the selected nodes and their descendants before editing when you need current details.',
534
536
  'Prefer focused inspection. Expand beyond the selected subtree only when a concrete dependency requires it.',
535
537
  'Batch related canvas edits when practical, but make as many repair batches as the verified result genuinely requires.',
@@ -132,7 +132,7 @@ function codexOutcomeInstructions(product, { platform = null } = {}) {
132
132
  return [
133
133
  `You are an autonomous Framer harness embedded inside ${name}.`,
134
134
  'Work directly in the connected Framer project through Framer MCP. Programs use api.tree. There is no framer global.',
135
- 'When selected nodes are supplied, start with them and reuse their supplied snapshot. They show what the user is pointing at; the user\'s words decide what to change, within the reach in the assignment.',
135
+ 'When selected nodes are supplied, start with them and reuse their supplied snapshot. They show what the user is pointing at; the user\'s words decide what to change.',
136
136
  'Prefer focused inspection and batched edits, but use as many reads, repairs, and verification passes as the work genuinely requires.',
137
137
  'You own the full Framer loop: inspect, edit, read diagnostics, verify the actual result, repair concrete failures, and decide when the request is complete.',
138
138
  'Avoid repeated inspection of unchanged evidence and avoid broad project inspection unless a concrete dependency requires it.',
@@ -1177,7 +1177,7 @@ export function createCodexAppServerAdapter({
1177
1177
 
1178
1178
  async function ensureThread(
1179
1179
  runId,
1180
- { model, cwd: requestedCwd, harnessMode = false, dynamicTools = [], dynamicToolHandler, resumeThreadId, webHosts = [] } = {},
1180
+ { model, cwd: requestedCwd, harnessMode = false, dynamicTools = [], dynamicToolHandler, resumeThreadId, webHosts = [], threadState = {} } = {},
1181
1181
  ) {
1182
1182
  const resolution = await resolveProvider();
1183
1183
  const existing = threadsByRun.get(runId);
@@ -1198,6 +1198,7 @@ export function createCodexAppServerAdapter({
1198
1198
  if (dynamicToolHandler) {
1199
1199
  dynamicToolHandlersByThread.set(existing, dynamicToolHandler);
1200
1200
  }
1201
+ threadState.continued = true;
1201
1202
  return existing;
1202
1203
  }
1203
1204
  if (existing) {
@@ -1267,6 +1268,7 @@ export function createCodexAppServerAdapter({
1267
1268
  } catch {
1268
1269
  response = null;
1269
1270
  }
1271
+ if (response) threadState.continued = true;
1270
1272
  }
1271
1273
  if (!response) {
1272
1274
  response = await active.request(CODEX_APP_SERVER_METHODS.threadStart, params, {
@@ -1326,11 +1328,11 @@ export function createCodexAppServerAdapter({
1326
1328
  cwd: requestedCwd,
1327
1329
  harnessTools,
1328
1330
  resumeSessionId,
1331
+ continuationPrompt,
1329
1332
  imageFiles = [],
1330
1333
  webHosts = [],
1331
1334
  } = {}) {
1332
1335
  const transportSchema = outputSchema ? codexStructuredOutputSchema(outputSchema, { responseContract }) : null;
1333
- const transportPrompt = transportSchema ? codexStructuredOutputPrompt(prompt, responseContract) : prompt;
1334
1336
  await resolveProvider();
1335
1337
  await initialize();
1336
1338
  const active = ensureClient();
@@ -1339,6 +1341,7 @@ export function createCodexAppServerAdapter({
1339
1341
  const materializedNativeSkills = await prepareNativeSkills(threadKey, nativeSkills);
1340
1342
  const designHarness =
1341
1343
  harnessMode && Boolean(designHarnessPlatform(harnessTools?.definitions || []));
1344
+ const threadState = {};
1342
1345
  const threadId = await ensureThread(threadKey, {
1343
1346
  model,
1344
1347
  cwd: requestedCwd,
@@ -1347,7 +1350,12 @@ export function createCodexAppServerAdapter({
1347
1350
  dynamicToolHandler: harnessMode ? harnessTools?.invoke : undefined,
1348
1351
  resumeThreadId: harnessMode ? resumeSessionId : undefined,
1349
1352
  webHosts: harnessMode ? webHosts : [],
1353
+ threadState,
1350
1354
  });
1355
+ // A thread that already holds the conversation gets only what is new;
1356
+ // a fresh thread needs the whole prompt.
1357
+ const turnPrompt = threadState.continued && continuationPrompt ? continuationPrompt : prompt;
1358
+ const transportPrompt = transportSchema ? codexStructuredOutputPrompt(turnPrompt, responseContract) : turnPrompt;
1351
1359
  const priorSkillDigest = invokedSkillDigestsByThread.get(threadId);
1352
1360
  const nativeSkillsReused =
1353
1361
  Boolean(materializedNativeSkills) && priorSkillDigest === materializedNativeSkills.digest;
@@ -1575,6 +1583,7 @@ export function createCodexAppServerAdapter({
1575
1583
  return {
1576
1584
  ...result,
1577
1585
  providerAttemptCount,
1586
+ sessionContinued: Boolean(threadState.continued),
1578
1587
  ...(materializedNativeSkills
1579
1588
  ? {
1580
1589
  nativeSkillLifecycle: nativeSkillLifecycle(materializedNativeSkills, 'skill-input', nativeSkillsReused),
@@ -1613,6 +1622,7 @@ export function createCodexAppServerAdapter({
1613
1622
  return {
1614
1623
  ...result,
1615
1624
  providerAttemptCount,
1625
+ sessionContinued: Boolean(threadState.continued),
1616
1626
  retryReason: 'transport_correction',
1617
1627
  ...(materializedNativeSkills
1618
1628
  ? {