@mindstudio-ai/remy 0.1.244 → 0.1.246

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.
@@ -19,7 +19,10 @@
19
19
  * page size (default 500, hard cap 2000). Response: {event:"history",
20
20
  * messages, startIndex, endIndex, totalMessageCount, ...}. Walk backward by
21
21
  * passing the previous response's `startIndex` as the next `before`. When
22
- * `startIndex === 0`, no older messages remain.
22
+ * `startIndex === 0`, no older messages remain. Indices are GLOBAL — they span
23
+ * the sealed session archives followed by the live tail (see getHistoryPage in
24
+ * session.ts), so scrollback continues past a rotation to the conversation
25
+ * start, not just to the start of the live (post-rotation) array.
23
26
  */
24
27
  interface HeadlessOptions {
25
28
  apiKey?: string;
package/dist/headless.js CHANGED
@@ -1881,11 +1881,11 @@ var setProjectMetadataTool = {
1881
1881
  },
1882
1882
  iconUrl: {
1883
1883
  type: "string",
1884
- description: "URL for the app icon (square."
1884
+ description: "URL for the app icon (square)."
1885
1885
  },
1886
1886
  openGraphShareImageUrl: {
1887
1887
  type: "string",
1888
- description: "URL for the Open Graph share image (1200x630)."
1888
+ description: "URL for the Open Graph share image (1200\xD7630 PNG)."
1889
1889
  }
1890
1890
  }
1891
1891
  }
@@ -2819,9 +2819,7 @@ async function analyzeImage(params) {
2819
2819
 
2820
2820
  // src/tools/_helpers/screenshot.ts
2821
2821
  var SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).`;
2822
- var TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.
2823
-
2824
- Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
2822
+ var ANALYSIS_RESPONSE_FORMAT = `Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
2825
2823
  function buildScreenshotAnalysisPrompt(opts) {
2826
2824
  let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
2827
2825
  if (opts?.styleMap) {
@@ -2835,7 +2833,7 @@ ${opts.styleMap}
2835
2833
  }
2836
2834
  p += `
2837
2835
 
2838
- ${TEXT_WRAP_DISCLAIMER}`;
2836
+ ${ANALYSIS_RESPONSE_FORMAT}`;
2839
2837
  return p;
2840
2838
  }
2841
2839
  async function streamScreenshotAnalysis(opts) {
@@ -2861,6 +2859,9 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2861
2859
  let model;
2862
2860
  let path13;
2863
2861
  let fullPage = true;
2862
+ let width;
2863
+ let height;
2864
+ let format;
2864
2865
  if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2865
2866
  prompt = promptOrOptions.prompt;
2866
2867
  existingUrl = promptOrOptions.imageUrl;
@@ -2868,11 +2869,17 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2868
2869
  if (promptOrOptions.fullPage !== void 0) {
2869
2870
  fullPage = promptOrOptions.fullPage;
2870
2871
  }
2872
+ width = promptOrOptions.width;
2873
+ height = promptOrOptions.height;
2874
+ format = promptOrOptions.format;
2871
2875
  onLog = promptOrOptions.onLog;
2872
2876
  model = promptOrOptions.model;
2873
2877
  } else {
2874
2878
  prompt = promptOrOptions;
2875
2879
  }
2880
+ if (width != null && height != null) {
2881
+ fullPage = false;
2882
+ }
2876
2883
  let url;
2877
2884
  let styleMap;
2878
2885
  if (existingUrl) {
@@ -2880,7 +2887,12 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2880
2887
  } else {
2881
2888
  const ssResult = await sidecarRequest(
2882
2889
  fullPage ? "/screenshot-full-page" : "/screenshot-viewport",
2883
- path13 ? { path: path13 } : void 0,
2890
+ {
2891
+ ...path13 ? { path: path13 } : {},
2892
+ ...width != null ? { width } : {},
2893
+ ...height != null ? { height } : {},
2894
+ ...format ? { format } : {}
2895
+ },
2884
2896
  { timeout: fullPage ? 12e4 : 3e4 }
2885
2897
  );
2886
2898
  url = ssResult?.url || ssResult?.screenshotUrl;
@@ -4009,7 +4021,7 @@ var screenshotTool = {
4009
4021
  clearable: true,
4010
4022
  definition: {
4011
4023
  name: "screenshot",
4012
- description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as imageUrl to skip recapture. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps.",
4024
+ description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as imageUrl to skip recapture. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To render a fixed-size image such as an Open Graph share card, set `width` and `height` (e.g. 1200 \xD7 630) and `format: 'png'`: the tool navigates to `path`, clips to exactly those pixel dimensions, and returns the image URL.",
4013
4025
  inputSchema: {
4014
4026
  type: "object",
4015
4027
  properties: {
@@ -4029,6 +4041,19 @@ var screenshotTool = {
4029
4041
  type: "string",
4030
4042
  description: 'Navigate to this path before capturing (e.g. "/settings", "/dashboard"). If omitted, screenshots the current page.'
4031
4043
  },
4044
+ width: {
4045
+ type: "number",
4046
+ description: "Exact capture width in pixels. Set together with `height` to render a fixed-size image; clips to exactly this viewport instead of the default preview size."
4047
+ },
4048
+ height: {
4049
+ type: "number",
4050
+ description: "Exact capture height in pixels. Set together with `width`."
4051
+ },
4052
+ format: {
4053
+ type: "string",
4054
+ enum: ["png", "jpeg"],
4055
+ description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics like share cards, where JPEG artifacts show on sharp type and edges."
4056
+ },
4032
4057
  instructions: {
4033
4058
  type: "string",
4034
4059
  description: "If the screenshot you need requires interaction first (dismissing a modal, clicking a tab, filling out a form, navigating a flow, scrolling to a section, getting through a login/auth checkpoint), describe the steps to get there. A browser automation agent will follow these instructions, then capture per your `fullPage` choice \u2014 so with `fullPage: false` you can scroll to a section and capture just that viewport. It can bypass auth and get right to where it needs to be if you tell it to authenticate as a test user and give it the path/screen to start its test at. Never describe what names or values to use when applying the instructions - the browser automation agent must use its own values for it to work properly. If a specific auth role is required to access the content, be sure to note that - it can automatically assume it for the purpose of testing. Use only when interaction is required to *reach* the state you want to capture \u2014 log in, dismiss a modal, switch a tab, follow a route, scroll to a section. If your steps are exercising the app's functionality across multiple states (running flows, asserting behavior under interaction, multi-step QA), use `runAutomatedBrowserTest` instead."
@@ -4071,6 +4096,9 @@ var screenshotTool = {
4071
4096
  prompt: input.prompt,
4072
4097
  path: input.path,
4073
4098
  fullPage,
4099
+ width: input.width,
4100
+ height: input.height,
4101
+ format: input.format,
4074
4102
  onLog: context?.onLog,
4075
4103
  model: resolveModel("imageAnalysis", context?.models, context?.model)
4076
4104
  });
@@ -6281,6 +6309,14 @@ var ARCHIVE_DIR = ".logs/sessions";
6281
6309
  var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
6282
6310
  var RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
6283
6311
  var ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
6312
+ var ARCHIVE_NAME_RE = /^(cleared|rotated)-.*\.json$/;
6313
+ var archiveSortKey = (name) => name.replace(/^(cleared|rotated)-/, "");
6314
+ var ARCHIVE_COUNT_RE = /\.c(\d+)\.json$/;
6315
+ var HISTORY_DEFAULT_LIMIT = 500;
6316
+ var HISTORY_MAX_LIMIT = 2e3;
6317
+ var archiveCountCache = /* @__PURE__ */ new Map();
6318
+ var archiveMsgCache = /* @__PURE__ */ new Map();
6319
+ var ARCHIVE_MSG_CACHE_MAX = 3;
6284
6320
  function loadSession(state) {
6285
6321
  pruneArchives();
6286
6322
  try {
@@ -6359,31 +6395,34 @@ function buildPayload(state) {
6359
6395
  function archiveMessages(messages, label, models) {
6360
6396
  fs21.mkdirSync(ARCHIVE_DIR, { recursive: true });
6361
6397
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6362
- let dest = path11.join(ARCHIVE_DIR, `${label}-${ts}.json`);
6398
+ const count = messages.length;
6399
+ let dest = path11.join(ARCHIVE_DIR, `${label}-${ts}.c${count}.json`);
6363
6400
  let n = 1;
6364
6401
  while (fs21.existsSync(dest)) {
6365
- dest = path11.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.json`);
6402
+ dest = path11.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.c${count}.json`);
6366
6403
  }
6367
6404
  const payload = { messages };
6368
6405
  if (models && Object.keys(models).length > 0) {
6369
6406
  payload.models = models;
6370
6407
  }
6371
6408
  fs21.writeFileSync(dest, JSON.stringify(payload), "utf-8");
6372
- log9.info("Session archived", { label, dest, messageCount: messages.length });
6409
+ archiveCountCache.set(path11.basename(dest), count);
6410
+ log9.info("Session archived", { label, dest, messageCount: count });
6373
6411
  pruneArchives();
6374
6412
  return dest;
6375
6413
  }
6376
6414
  function pruneArchives() {
6377
6415
  try {
6378
- const entries = fs21.readdirSync(ARCHIVE_DIR).filter((name) => /^(cleared|rotated)-.*\.json$/.test(name));
6416
+ const entries = fs21.readdirSync(ARCHIVE_DIR).filter((name) => ARCHIVE_NAME_RE.test(name));
6379
6417
  if (entries.length <= 1) {
6380
6418
  return;
6381
6419
  }
6382
- const sortKey = (name) => name.replace(/^(cleared|rotated)-/, "");
6383
6420
  const archives = entries.map((name) => ({
6384
6421
  name,
6385
6422
  size: fs21.statSync(path11.join(ARCHIVE_DIR, name)).size
6386
- })).sort((a, b) => sortKey(b.name).localeCompare(sortKey(a.name)));
6423
+ })).sort(
6424
+ (a, b) => archiveSortKey(b.name).localeCompare(archiveSortKey(a.name))
6425
+ );
6387
6426
  let kept = 0;
6388
6427
  let cut = archives.length;
6389
6428
  for (let i = 0; i < archives.length; i++) {
@@ -6414,6 +6453,129 @@ function pruneArchives() {
6414
6453
  } catch {
6415
6454
  }
6416
6455
  }
6456
+ function parseArchive(name) {
6457
+ const cached3 = archiveMsgCache.get(name);
6458
+ if (cached3) {
6459
+ archiveMsgCache.delete(name);
6460
+ archiveMsgCache.set(name, cached3);
6461
+ return cached3;
6462
+ }
6463
+ try {
6464
+ const raw = fs21.readFileSync(path11.join(ARCHIVE_DIR, name), "utf-8");
6465
+ const data = JSON.parse(raw);
6466
+ const messages = Array.isArray(data?.messages) ? data.messages : [];
6467
+ archiveCountCache.set(name, messages.length);
6468
+ archiveMsgCache.set(name, messages);
6469
+ while (archiveMsgCache.size > ARCHIVE_MSG_CACHE_MAX) {
6470
+ const oldest = archiveMsgCache.keys().next().value;
6471
+ if (oldest === void 0) {
6472
+ break;
6473
+ }
6474
+ archiveMsgCache.delete(oldest);
6475
+ }
6476
+ return messages;
6477
+ } catch (err) {
6478
+ log9.warn("Session archive unreadable", { name, error: err?.message });
6479
+ return null;
6480
+ }
6481
+ }
6482
+ function archiveCount(name) {
6483
+ const cached3 = archiveCountCache.get(name);
6484
+ if (cached3 !== void 0) {
6485
+ return cached3;
6486
+ }
6487
+ const m = ARCHIVE_COUNT_RE.exec(name);
6488
+ if (m) {
6489
+ const n = Number(m[1]);
6490
+ archiveCountCache.set(name, n);
6491
+ return n;
6492
+ }
6493
+ const msgs = parseArchive(name);
6494
+ return msgs ? msgs.length : null;
6495
+ }
6496
+ function readArchiveMessages(name) {
6497
+ return parseArchive(name) ?? [];
6498
+ }
6499
+ function listConversationArchives() {
6500
+ let names;
6501
+ try {
6502
+ names = fs21.readdirSync(ARCHIVE_DIR).filter((n) => ARCHIVE_NAME_RE.test(n));
6503
+ } catch {
6504
+ return { slots: [], archivedCount: 0 };
6505
+ }
6506
+ let maxClearedKey = null;
6507
+ for (const name of names) {
6508
+ if (name.startsWith("cleared-")) {
6509
+ const key = archiveSortKey(name);
6510
+ if (maxClearedKey === null || key > maxClearedKey) {
6511
+ maxClearedKey = key;
6512
+ }
6513
+ }
6514
+ }
6515
+ const rotated = names.filter((n) => n.startsWith("rotated-")).filter((n) => maxClearedKey === null || archiveSortKey(n) > maxClearedKey).sort((a, b) => archiveSortKey(a).localeCompare(archiveSortKey(b)));
6516
+ const slots = [];
6517
+ let offset = 0;
6518
+ for (const name of rotated) {
6519
+ const count = archiveCount(name);
6520
+ if (count === null || count <= 0) {
6521
+ continue;
6522
+ }
6523
+ slots.push({ name, count, offset });
6524
+ offset += count;
6525
+ }
6526
+ return { slots, archivedCount: offset };
6527
+ }
6528
+ function getHistoryPage(state, opts) {
6529
+ const { slots, archivedCount } = listConversationArchives();
6530
+ const liveLen = state.messages.length;
6531
+ const total = archivedCount + liveLen;
6532
+ const rawLimit = opts?.limit;
6533
+ const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.min(Math.max(1, rawLimit | 0), HISTORY_MAX_LIMIT) : HISTORY_DEFAULT_LIMIT;
6534
+ const rawBefore = opts?.before;
6535
+ const before = typeof rawBefore === "number" && Number.isFinite(rawBefore) ? Math.max(0, Math.min(rawBefore | 0, total)) : total;
6536
+ const peekGlobal = (i) => {
6537
+ if (i >= archivedCount) {
6538
+ return state.messages[i - archivedCount];
6539
+ }
6540
+ for (const slot of slots) {
6541
+ if (i < slot.offset + slot.count) {
6542
+ return readArchiveMessages(slot.name)[i - slot.offset];
6543
+ }
6544
+ }
6545
+ return void 0;
6546
+ };
6547
+ let startIndex = Math.max(0, before - limit);
6548
+ while (startIndex > 0) {
6549
+ const msg = peekGlobal(startIndex);
6550
+ if (msg && msg.role === "user" && msg.toolCallId) {
6551
+ startIndex--;
6552
+ } else {
6553
+ break;
6554
+ }
6555
+ }
6556
+ const endIndex = before;
6557
+ const messages = [];
6558
+ for (const slot of slots) {
6559
+ const slotEnd = slot.offset + slot.count;
6560
+ if (slotEnd <= startIndex || slot.offset >= endIndex) {
6561
+ continue;
6562
+ }
6563
+ const from = Math.max(startIndex, slot.offset) - slot.offset;
6564
+ const to = Math.min(endIndex, slotEnd) - slot.offset;
6565
+ const msgs = readArchiveMessages(slot.name);
6566
+ for (let i = from; i < to; i++) {
6567
+ messages.push(msgs[i]);
6568
+ }
6569
+ }
6570
+ if (endIndex > archivedCount) {
6571
+ const from = Math.max(startIndex, archivedCount) - archivedCount;
6572
+ const to = endIndex - archivedCount;
6573
+ for (let i = from; i < to; i++) {
6574
+ messages.push(state.messages[i]);
6575
+ }
6576
+ }
6577
+ return { messages, startIndex, endIndex, totalMessageCount: total };
6578
+ }
6417
6579
  function rotate(state) {
6418
6580
  const messages = state.messages;
6419
6581
  if (messages.length === 0) {
@@ -7951,8 +8113,6 @@ var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
7951
8113
  "presentPublishPlan"
7952
8114
  ]);
7953
8115
  var FORCED_COMPACTION_THRESHOLD_TOKENS = 85e4;
7954
- var HISTORY_DEFAULT_LIMIT = 500;
7955
- var HISTORY_MAX_LIMIT = 2e3;
7956
8116
  var HeadlessSession = class {
7957
8117
  // Configuration
7958
8118
  opts;
@@ -8698,30 +8858,24 @@ var HeadlessSession = class {
8698
8858
  }
8699
8859
  if (action === "get_history") {
8700
8860
  this.applyPendingBlockUpdates();
8701
- const total = this.state.messages.length;
8702
- const rawLimit = parsed.limit;
8703
- const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.min(Math.max(1, rawLimit | 0), HISTORY_MAX_LIMIT) : HISTORY_DEFAULT_LIMIT;
8704
- const rawBefore = parsed.before;
8705
- const before = typeof rawBefore === "number" && Number.isFinite(rawBefore) ? Math.max(0, Math.min(rawBefore | 0, total)) : total;
8706
- let startIndex = Math.max(0, before - limit);
8707
- while (startIndex > 0 && this.state.messages[startIndex].role === "user" && this.state.messages[startIndex].toolCallId) {
8708
- startIndex--;
8709
- }
8710
- const endIndex = before;
8861
+ const page = getHistoryPage(this.state, {
8862
+ ...typeof parsed.before === "number" ? { before: parsed.before } : {},
8863
+ ...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
8864
+ });
8711
8865
  log15.info("History response", {
8712
8866
  requestId,
8713
- startIndex,
8714
- endIndex,
8715
- count: endIndex - startIndex,
8716
- totalMessageCount: total,
8717
- beforeParam: rawBefore,
8718
- limitParam: rawLimit
8867
+ startIndex: page.startIndex,
8868
+ endIndex: page.endIndex,
8869
+ count: page.endIndex - page.startIndex,
8870
+ totalMessageCount: page.totalMessageCount,
8871
+ beforeParam: parsed.before,
8872
+ limitParam: parsed.limit
8719
8873
  });
8720
8874
  this.dispatchSimple(requestId, "history", () => ({
8721
- messages: this.state.messages.slice(startIndex, endIndex),
8722
- startIndex,
8723
- endIndex,
8724
- totalMessageCount: total,
8875
+ messages: page.messages,
8876
+ startIndex: page.startIndex,
8877
+ endIndex: page.endIndex,
8878
+ totalMessageCount: page.totalMessageCount,
8725
8879
  running: this.running,
8726
8880
  ...this.running && this.currentRequestId ? { currentRequestId: this.currentRequestId } : {},
8727
8881
  ...this.state.models && { models: this.state.models },
package/dist/index.js CHANGED
@@ -1462,11 +1462,11 @@ var init_setProjectMetadata = __esm({
1462
1462
  },
1463
1463
  iconUrl: {
1464
1464
  type: "string",
1465
- description: "URL for the app icon (square."
1465
+ description: "URL for the app icon (square)."
1466
1466
  },
1467
1467
  openGraphShareImageUrl: {
1468
1468
  type: "string",
1469
- description: "URL for the Open Graph share image (1200x630)."
1469
+ description: "URL for the Open Graph share image (1200\xD7630 PNG)."
1470
1470
  }
1471
1471
  }
1472
1472
  }
@@ -2314,31 +2314,34 @@ function buildPayload(state) {
2314
2314
  function archiveMessages(messages, label, models) {
2315
2315
  fs10.mkdirSync(ARCHIVE_DIR, { recursive: true });
2316
2316
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2317
- let dest = path4.join(ARCHIVE_DIR, `${label}-${ts}.json`);
2317
+ const count = messages.length;
2318
+ let dest = path4.join(ARCHIVE_DIR, `${label}-${ts}.c${count}.json`);
2318
2319
  let n = 1;
2319
2320
  while (fs10.existsSync(dest)) {
2320
- dest = path4.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.json`);
2321
+ dest = path4.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.c${count}.json`);
2321
2322
  }
2322
2323
  const payload = { messages };
2323
2324
  if (models && Object.keys(models).length > 0) {
2324
2325
  payload.models = models;
2325
2326
  }
2326
2327
  fs10.writeFileSync(dest, JSON.stringify(payload), "utf-8");
2327
- log3.info("Session archived", { label, dest, messageCount: messages.length });
2328
+ archiveCountCache.set(path4.basename(dest), count);
2329
+ log3.info("Session archived", { label, dest, messageCount: count });
2328
2330
  pruneArchives();
2329
2331
  return dest;
2330
2332
  }
2331
2333
  function pruneArchives() {
2332
2334
  try {
2333
- const entries = fs10.readdirSync(ARCHIVE_DIR).filter((name) => /^(cleared|rotated)-.*\.json$/.test(name));
2335
+ const entries = fs10.readdirSync(ARCHIVE_DIR).filter((name) => ARCHIVE_NAME_RE.test(name));
2334
2336
  if (entries.length <= 1) {
2335
2337
  return;
2336
2338
  }
2337
- const sortKey = (name) => name.replace(/^(cleared|rotated)-/, "");
2338
2339
  const archives = entries.map((name) => ({
2339
2340
  name,
2340
2341
  size: fs10.statSync(path4.join(ARCHIVE_DIR, name)).size
2341
- })).sort((a, b) => sortKey(b.name).localeCompare(sortKey(a.name)));
2342
+ })).sort(
2343
+ (a, b) => archiveSortKey(b.name).localeCompare(archiveSortKey(a.name))
2344
+ );
2342
2345
  let kept = 0;
2343
2346
  let cut = archives.length;
2344
2347
  for (let i = 0; i < archives.length; i++) {
@@ -2369,6 +2372,129 @@ function pruneArchives() {
2369
2372
  } catch {
2370
2373
  }
2371
2374
  }
2375
+ function parseArchive(name) {
2376
+ const cached3 = archiveMsgCache.get(name);
2377
+ if (cached3) {
2378
+ archiveMsgCache.delete(name);
2379
+ archiveMsgCache.set(name, cached3);
2380
+ return cached3;
2381
+ }
2382
+ try {
2383
+ const raw = fs10.readFileSync(path4.join(ARCHIVE_DIR, name), "utf-8");
2384
+ const data = JSON.parse(raw);
2385
+ const messages = Array.isArray(data?.messages) ? data.messages : [];
2386
+ archiveCountCache.set(name, messages.length);
2387
+ archiveMsgCache.set(name, messages);
2388
+ while (archiveMsgCache.size > ARCHIVE_MSG_CACHE_MAX) {
2389
+ const oldest = archiveMsgCache.keys().next().value;
2390
+ if (oldest === void 0) {
2391
+ break;
2392
+ }
2393
+ archiveMsgCache.delete(oldest);
2394
+ }
2395
+ return messages;
2396
+ } catch (err) {
2397
+ log3.warn("Session archive unreadable", { name, error: err?.message });
2398
+ return null;
2399
+ }
2400
+ }
2401
+ function archiveCount(name) {
2402
+ const cached3 = archiveCountCache.get(name);
2403
+ if (cached3 !== void 0) {
2404
+ return cached3;
2405
+ }
2406
+ const m = ARCHIVE_COUNT_RE.exec(name);
2407
+ if (m) {
2408
+ const n = Number(m[1]);
2409
+ archiveCountCache.set(name, n);
2410
+ return n;
2411
+ }
2412
+ const msgs = parseArchive(name);
2413
+ return msgs ? msgs.length : null;
2414
+ }
2415
+ function readArchiveMessages(name) {
2416
+ return parseArchive(name) ?? [];
2417
+ }
2418
+ function listConversationArchives() {
2419
+ let names;
2420
+ try {
2421
+ names = fs10.readdirSync(ARCHIVE_DIR).filter((n) => ARCHIVE_NAME_RE.test(n));
2422
+ } catch {
2423
+ return { slots: [], archivedCount: 0 };
2424
+ }
2425
+ let maxClearedKey = null;
2426
+ for (const name of names) {
2427
+ if (name.startsWith("cleared-")) {
2428
+ const key = archiveSortKey(name);
2429
+ if (maxClearedKey === null || key > maxClearedKey) {
2430
+ maxClearedKey = key;
2431
+ }
2432
+ }
2433
+ }
2434
+ const rotated = names.filter((n) => n.startsWith("rotated-")).filter((n) => maxClearedKey === null || archiveSortKey(n) > maxClearedKey).sort((a, b) => archiveSortKey(a).localeCompare(archiveSortKey(b)));
2435
+ const slots = [];
2436
+ let offset = 0;
2437
+ for (const name of rotated) {
2438
+ const count = archiveCount(name);
2439
+ if (count === null || count <= 0) {
2440
+ continue;
2441
+ }
2442
+ slots.push({ name, count, offset });
2443
+ offset += count;
2444
+ }
2445
+ return { slots, archivedCount: offset };
2446
+ }
2447
+ function getHistoryPage(state, opts) {
2448
+ const { slots, archivedCount } = listConversationArchives();
2449
+ const liveLen = state.messages.length;
2450
+ const total = archivedCount + liveLen;
2451
+ const rawLimit = opts?.limit;
2452
+ const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.min(Math.max(1, rawLimit | 0), HISTORY_MAX_LIMIT) : HISTORY_DEFAULT_LIMIT;
2453
+ const rawBefore = opts?.before;
2454
+ const before = typeof rawBefore === "number" && Number.isFinite(rawBefore) ? Math.max(0, Math.min(rawBefore | 0, total)) : total;
2455
+ const peekGlobal = (i) => {
2456
+ if (i >= archivedCount) {
2457
+ return state.messages[i - archivedCount];
2458
+ }
2459
+ for (const slot of slots) {
2460
+ if (i < slot.offset + slot.count) {
2461
+ return readArchiveMessages(slot.name)[i - slot.offset];
2462
+ }
2463
+ }
2464
+ return void 0;
2465
+ };
2466
+ let startIndex = Math.max(0, before - limit);
2467
+ while (startIndex > 0) {
2468
+ const msg = peekGlobal(startIndex);
2469
+ if (msg && msg.role === "user" && msg.toolCallId) {
2470
+ startIndex--;
2471
+ } else {
2472
+ break;
2473
+ }
2474
+ }
2475
+ const endIndex = before;
2476
+ const messages = [];
2477
+ for (const slot of slots) {
2478
+ const slotEnd = slot.offset + slot.count;
2479
+ if (slotEnd <= startIndex || slot.offset >= endIndex) {
2480
+ continue;
2481
+ }
2482
+ const from = Math.max(startIndex, slot.offset) - slot.offset;
2483
+ const to = Math.min(endIndex, slotEnd) - slot.offset;
2484
+ const msgs = readArchiveMessages(slot.name);
2485
+ for (let i = from; i < to; i++) {
2486
+ messages.push(msgs[i]);
2487
+ }
2488
+ }
2489
+ if (endIndex > archivedCount) {
2490
+ const from = Math.max(startIndex, archivedCount) - archivedCount;
2491
+ const to = endIndex - archivedCount;
2492
+ for (let i = from; i < to; i++) {
2493
+ messages.push(state.messages[i]);
2494
+ }
2495
+ }
2496
+ return { messages, startIndex, endIndex, totalMessageCount: total };
2497
+ }
2372
2498
  function rotate(state) {
2373
2499
  const messages = state.messages;
2374
2500
  if (messages.length === 0) {
@@ -2428,7 +2554,7 @@ function clearSession(state) {
2428
2554
  });
2429
2555
  }
2430
2556
  }
2431
- var log3, SESSION_FILE, ARCHIVE_DIR, ROTATE_THRESHOLD_BYTES, RETAIN_TAIL_BYTES, ARCHIVE_RETENTION_BYTES;
2557
+ var log3, SESSION_FILE, ARCHIVE_DIR, ROTATE_THRESHOLD_BYTES, RETAIN_TAIL_BYTES, ARCHIVE_RETENTION_BYTES, ARCHIVE_NAME_RE, archiveSortKey, ARCHIVE_COUNT_RE, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, archiveCountCache, archiveMsgCache, ARCHIVE_MSG_CACHE_MAX;
2432
2558
  var init_session = __esm({
2433
2559
  "src/session.ts"() {
2434
2560
  "use strict";
@@ -2442,6 +2568,14 @@ var init_session = __esm({
2442
2568
  ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
2443
2569
  RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
2444
2570
  ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
2571
+ ARCHIVE_NAME_RE = /^(cleared|rotated)-.*\.json$/;
2572
+ archiveSortKey = (name) => name.replace(/^(cleared|rotated)-/, "");
2573
+ ARCHIVE_COUNT_RE = /\.c(\d+)\.json$/;
2574
+ HISTORY_DEFAULT_LIMIT = 500;
2575
+ HISTORY_MAX_LIMIT = 2e3;
2576
+ archiveCountCache = /* @__PURE__ */ new Map();
2577
+ archiveMsgCache = /* @__PURE__ */ new Map();
2578
+ ARCHIVE_MSG_CACHE_MAX = 3;
2445
2579
  }
2446
2580
  });
2447
2581
 
@@ -3555,7 +3689,7 @@ ${opts.styleMap}
3555
3689
  }
3556
3690
  p += `
3557
3691
 
3558
- ${TEXT_WRAP_DISCLAIMER}`;
3692
+ ${ANALYSIS_RESPONSE_FORMAT}`;
3559
3693
  return p;
3560
3694
  }
3561
3695
  async function streamScreenshotAnalysis(opts) {
@@ -3581,6 +3715,9 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3581
3715
  let model;
3582
3716
  let path14;
3583
3717
  let fullPage = true;
3718
+ let width;
3719
+ let height;
3720
+ let format;
3584
3721
  if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
3585
3722
  prompt = promptOrOptions.prompt;
3586
3723
  existingUrl = promptOrOptions.imageUrl;
@@ -3588,11 +3725,17 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3588
3725
  if (promptOrOptions.fullPage !== void 0) {
3589
3726
  fullPage = promptOrOptions.fullPage;
3590
3727
  }
3728
+ width = promptOrOptions.width;
3729
+ height = promptOrOptions.height;
3730
+ format = promptOrOptions.format;
3591
3731
  onLog = promptOrOptions.onLog;
3592
3732
  model = promptOrOptions.model;
3593
3733
  } else {
3594
3734
  prompt = promptOrOptions;
3595
3735
  }
3736
+ if (width != null && height != null) {
3737
+ fullPage = false;
3738
+ }
3596
3739
  let url;
3597
3740
  let styleMap;
3598
3741
  if (existingUrl) {
@@ -3600,7 +3743,12 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3600
3743
  } else {
3601
3744
  const ssResult = await sidecarRequest(
3602
3745
  fullPage ? "/screenshot-full-page" : "/screenshot-viewport",
3603
- path14 ? { path: path14 } : void 0,
3746
+ {
3747
+ ...path14 ? { path: path14 } : {},
3748
+ ...width != null ? { width } : {},
3749
+ ...height != null ? { height } : {},
3750
+ ...format ? { format } : {}
3751
+ },
3604
3752
  { timeout: fullPage ? 12e4 : 3e4 }
3605
3753
  );
3606
3754
  url = ssResult?.url || ssResult?.screenshotUrl;
@@ -3627,16 +3775,14 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
3627
3775
  model
3628
3776
  });
3629
3777
  }
3630
- var SCREENSHOT_ANALYSIS_PROMPT, TEXT_WRAP_DISCLAIMER;
3778
+ var SCREENSHOT_ANALYSIS_PROMPT, ANALYSIS_RESPONSE_FORMAT;
3631
3779
  var init_screenshot = __esm({
3632
3780
  "src/tools/_helpers/screenshot.ts"() {
3633
3781
  "use strict";
3634
3782
  init_sidecar();
3635
3783
  init_analyzeImage();
3636
3784
  SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).`;
3637
- TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.
3638
-
3639
- Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
3785
+ ANALYSIS_RESPONSE_FORMAT = `Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
3640
3786
  }
3641
3787
  });
3642
3788
 
@@ -4622,7 +4768,7 @@ var init_screenshot2 = __esm({
4622
4768
  clearable: true,
4623
4769
  definition: {
4624
4770
  name: "screenshot",
4625
- description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as imageUrl to skip recapture. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps.",
4771
+ description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as imageUrl to skip recapture. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To render a fixed-size image such as an Open Graph share card, set `width` and `height` (e.g. 1200 \xD7 630) and `format: 'png'`: the tool navigates to `path`, clips to exactly those pixel dimensions, and returns the image URL.",
4626
4772
  inputSchema: {
4627
4773
  type: "object",
4628
4774
  properties: {
@@ -4642,6 +4788,19 @@ var init_screenshot2 = __esm({
4642
4788
  type: "string",
4643
4789
  description: 'Navigate to this path before capturing (e.g. "/settings", "/dashboard"). If omitted, screenshots the current page.'
4644
4790
  },
4791
+ width: {
4792
+ type: "number",
4793
+ description: "Exact capture width in pixels. Set together with `height` to render a fixed-size image; clips to exactly this viewport instead of the default preview size."
4794
+ },
4795
+ height: {
4796
+ type: "number",
4797
+ description: "Exact capture height in pixels. Set together with `width`."
4798
+ },
4799
+ format: {
4800
+ type: "string",
4801
+ enum: ["png", "jpeg"],
4802
+ description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics like share cards, where JPEG artifacts show on sharp type and edges."
4803
+ },
4645
4804
  instructions: {
4646
4805
  type: "string",
4647
4806
  description: "If the screenshot you need requires interaction first (dismissing a modal, clicking a tab, filling out a form, navigating a flow, scrolling to a section, getting through a login/auth checkpoint), describe the steps to get there. A browser automation agent will follow these instructions, then capture per your `fullPage` choice \u2014 so with `fullPage: false` you can scroll to a section and capture just that viewport. It can bypass auth and get right to where it needs to be if you tell it to authenticate as a test user and give it the path/screen to start its test at. Never describe what names or values to use when applying the instructions - the browser automation agent must use its own values for it to work properly. If a specific auth role is required to access the content, be sure to note that - it can automatically assume it for the purpose of testing. Use only when interaction is required to *reach* the state you want to capture \u2014 log in, dismiss a modal, switch a tab, follow a route, scroll to a section. If your steps are exercising the app's functionality across multiple states (running flows, asserting behavior under interaction, multi-step QA), use `runAutomatedBrowserTest` instead."
@@ -4684,6 +4843,9 @@ var init_screenshot2 = __esm({
4684
4843
  prompt: input.prompt,
4685
4844
  path: input.path,
4686
4845
  fullPage,
4846
+ width: input.width,
4847
+ height: input.height,
4848
+ format: input.format,
4687
4849
  onLog: context?.onLog,
4688
4850
  model: resolveModel("imageAnalysis", context?.models, context?.model)
4689
4851
  });
@@ -8811,7 +8973,7 @@ var headless_exports = {};
8811
8973
  __export(headless_exports, {
8812
8974
  HeadlessSession: () => HeadlessSession
8813
8975
  });
8814
- var log15, EXTERNAL_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, FORCED_COMPACTION_THRESHOLD_TOKENS, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, HeadlessSession;
8976
+ var log15, EXTERNAL_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, FORCED_COMPACTION_THRESHOLD_TOKENS, HeadlessSession;
8815
8977
  var init_headless = __esm({
8816
8978
  "src/headless/index.ts"() {
8817
8979
  "use strict";
@@ -8840,8 +9002,6 @@ var init_headless = __esm({
8840
9002
  "presentPublishPlan"
8841
9003
  ]);
8842
9004
  FORCED_COMPACTION_THRESHOLD_TOKENS = 85e4;
8843
- HISTORY_DEFAULT_LIMIT = 500;
8844
- HISTORY_MAX_LIMIT = 2e3;
8845
9005
  HeadlessSession = class {
8846
9006
  // Configuration
8847
9007
  opts;
@@ -9587,30 +9747,24 @@ var init_headless = __esm({
9587
9747
  }
9588
9748
  if (action === "get_history") {
9589
9749
  this.applyPendingBlockUpdates();
9590
- const total = this.state.messages.length;
9591
- const rawLimit = parsed.limit;
9592
- const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.min(Math.max(1, rawLimit | 0), HISTORY_MAX_LIMIT) : HISTORY_DEFAULT_LIMIT;
9593
- const rawBefore = parsed.before;
9594
- const before = typeof rawBefore === "number" && Number.isFinite(rawBefore) ? Math.max(0, Math.min(rawBefore | 0, total)) : total;
9595
- let startIndex = Math.max(0, before - limit);
9596
- while (startIndex > 0 && this.state.messages[startIndex].role === "user" && this.state.messages[startIndex].toolCallId) {
9597
- startIndex--;
9598
- }
9599
- const endIndex = before;
9750
+ const page = getHistoryPage(this.state, {
9751
+ ...typeof parsed.before === "number" ? { before: parsed.before } : {},
9752
+ ...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
9753
+ });
9600
9754
  log15.info("History response", {
9601
9755
  requestId,
9602
- startIndex,
9603
- endIndex,
9604
- count: endIndex - startIndex,
9605
- totalMessageCount: total,
9606
- beforeParam: rawBefore,
9607
- limitParam: rawLimit
9756
+ startIndex: page.startIndex,
9757
+ endIndex: page.endIndex,
9758
+ count: page.endIndex - page.startIndex,
9759
+ totalMessageCount: page.totalMessageCount,
9760
+ beforeParam: parsed.before,
9761
+ limitParam: parsed.limit
9608
9762
  });
9609
9763
  this.dispatchSimple(requestId, "history", () => ({
9610
- messages: this.state.messages.slice(startIndex, endIndex),
9611
- startIndex,
9612
- endIndex,
9613
- totalMessageCount: total,
9764
+ messages: page.messages,
9765
+ startIndex: page.startIndex,
9766
+ endIndex: page.endIndex,
9767
+ totalMessageCount: page.totalMessageCount,
9614
9768
  running: this.running,
9615
9769
  ...this.running && this.currentRequestId ? { currentRequestId: this.currentRequestId } : {},
9616
9770
  ...this.state.models && { models: this.state.models },
@@ -76,6 +76,6 @@ When you receive background results:
76
76
 
77
77
  You can only background the following two tasks, unless the user specifically asks you to do work in the background:
78
78
  - `productVision` seeding the intiial roadmap after writing the spec for the first time or updating the roadmap after large work sessions. This task takes a while and we can allow the user to continue building while it happens in the background.
79
- - After writing the spec, once you have finalized the shape of the app, ask `visualDesignExpert` to create an icon and an open graph shring image for the app, then set them with `setProjectMetadata`, alongside the app's name and short description.
79
+ - After writing the spec, once you have finalized the shape of the app, ask `visualDesignExpert` to create an icon and to design an Open Graph share image, then set them with `setProjectMetadata` alongside the app's name and short description. The icon is a generated asset. The share image should be composed as a self-contained HTML card (real lockup, brand fonts, exact type) and captured — not generated: serve the card HTML from `dist/interfaces/web/public/`, screenshot it with the `screenshot` tool at `width: 1200, height: 630, format: 'png'`, and save the resulting PNG into the app's public assets so the deployed site hosts it.
80
80
 
81
81
  Do not background any other tasks. Be aware that sometimes tools like specSync will background on their own - this is not something that is within your control.
@@ -77,7 +77,9 @@ Keep logos and icons consistent - if you already have a logo, use `editImages` t
77
77
 
78
78
  #### Open Graph Sharing Images
79
79
 
80
- OG images show up in iMessage, Slack, Twitter, etc. at small sizes. They're a mood piece, not a messaging opportunity. Keep text minimal: the app name and at most a short tagline (three to five words). Think App Store feature card — one beautiful composition that makes someone want to tap. The text should feel integrated into the scene, not pasted on a background. Generate at 4096 × 2150 and return the CDN URL.
80
+ OG images show up in iMessage, Slack, Twitter, etc. at small sizes. They're a mood piece, not a messaging opportunity. Keep text minimal: the app name and at most a short tagline (three to five words). Think App Store feature card — one beautiful composition that makes someone want to tap. The text should feel integrated into the scene, not pasted on a background.
81
+
82
+ A share card is a wordmark, a short line, and a logo on a brand field — **compose it as HTML, don't generate it with the image model.** A generated image gives you odd letterforms and no brand fidelity; HTML gives you the real SVG lockup, the actual brand fonts, exact colors, and pixel-perfect spacing. Author a self-contained HTML document sized to 1200 × 630 — the lockup inline as SVG, the brand fonts inlined as base64 (so nothing loads late and captures as a fallback), the palette and type exact — then capture it with the `screenshot` tool at `width: 1200, height: 630, format: 'png'`, a faithful real-browser render. Don't route it through a document/HTML-to-image "openGraph" render mode; that pipeline strips CSS backgrounds. The same compose-and-capture approach beats generation for any precise brand graphic where letterforms and spacing carry the design.
81
83
 
82
84
  ### When to use images
83
85
 
@@ -69,12 +69,6 @@ For app icons and logos, the goal is something that reads clearly at phone home
69
69
  - You must specify that the image is full bleed - never say anything about rounded corners or there is a high likelihood that the image will come back as a rounded rectangle on a white background!
70
70
  - Apply the same material/lighting/color density as photography prompts, just to a single object. Describe the surface finish ("high-gloss lacquered finish with clean specular highlights," "soft matte ceramic with subtle surface texture"), the lighting behavior ("warm directional light from upper left producing a bright highlight streak across the curved surface and a soft shadow beneath"), and color as relationships ("deep coral body graduating to warm peach at the highlight edge, with a cream accent on the lens element"). Generic descriptors like "clean surfaces, soft lighting" produce generic icons.
71
71
 
72
- #### Open Graph Sharing Images
73
-
74
- OG images are often a user's first impression of the app — they show up in iMessage, Slack, Twitter, etc. at small sizes. Keep text minimal: the app name and at most a short tagline (three to five words). This is a mood piece, not a messaging opportunity. Think App Store feature card — one beautiful composition that makes someone want to tap.
75
-
76
- Apply the same material/lighting/color density as photography prompts. The text should feel integrated into the scene — typeset within the composition, not pasted on top. Describe the typography treatment (weight, size, color, position) as part of the overall image, and describe how the background interacts with the text (glow, depth, contrast). The whole image should read as one cohesive graphic, not layers.
77
-
78
72
  ## Output
79
73
 
80
74
  Respond with ONLY the enhanced prompt. Be detailed and specific — a good prompt is a dense paragraph of 80-150 words that paints a complete picture: style, subject, materials, lighting behavior, color relationships, atmosphere, and composition. Terse prompts produce generic images.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.244",
3
+ "version": "0.1.246",
4
4
  "description": "Remy coding agent",
5
5
  "repository": {
6
6
  "type": "git",