@midscene/shared 1.10.5-beta-20260716114658.0 → 1.10.5

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.
@@ -5,7 +5,7 @@ import { assert } from "../utils.mjs";
5
5
  import { maskConfig, parseJson } from "./helper.mjs";
6
6
  import { initDebugConfig } from "./init-debug.mjs";
7
7
  const MODEL_CONFIG_DOC_URL = 'https://midscenejs.com/model-common-config.html';
8
- const getCurrentVersion = ()=>"1.10.5-beta-20260716114658.0";
8
+ const getCurrentVersion = ()=>"1.10.5";
9
9
  const getInvalidModelFamilyMessage = (modelFamily)=>`Invalid MIDSCENE_MODEL_FAMILY value: ${modelFamily}. Current version v${getCurrentVersion()} accepts the following model families: ${MODEL_FAMILY_VALUES.join(', ')}. You can also visit ${MODEL_CONFIG_DOC_URL} for the latest configuration information.`;
10
10
  const KEYS_MAP = {
11
11
  insight: INSIGHT_MODEL_CONFIG_KEYS,
@@ -1,4 +1,4 @@
1
1
  import { imageInfoOfBase64, isValidImageBuffer, isValidJPEGImageBuffer, isValidPNGImageBuffer, validateScreenshotBuffer } from "./info.mjs";
2
- import { createImgBase64ByFormat, cropByRect, httpImg2Base64, inferBase64ImageFormat, localImg2Base64, normalizeBase64Image, normalizeScreenshotBase64, paddingToMatchBlockByBase64, parseBase64, preProcessImageUrl, resizeAndConvertImgBuffer, resizeImgBase64, saveBase64Image, scaleImage, zoomForGPT4o } from "./transform.mjs";
2
+ import { convertImgBufferToJpeg, createImgBase64ByFormat, cropByRect, httpImg2Base64, inferBase64ImageFormat, localImg2Base64, normalizeBase64Image, normalizeScreenshotBase64, paddingToMatchBlockByBase64, parseBase64, preProcessImageUrl, resizeAndConvertImgBuffer, resizeImgBase64, saveBase64Image, scaleImage, zoomForGPT4o } from "./transform.mjs";
3
3
  import { annotateRects, compositeElementInfoImg, compositePointMarkerImg, processImageElementInfo } from "./box-select.mjs";
4
- export { annotateRects, compositeElementInfoImg, compositePointMarkerImg, createImgBase64ByFormat, cropByRect, httpImg2Base64, imageInfoOfBase64, inferBase64ImageFormat, isValidImageBuffer, isValidJPEGImageBuffer, isValidPNGImageBuffer, localImg2Base64, normalizeBase64Image, normalizeScreenshotBase64, paddingToMatchBlockByBase64, parseBase64, preProcessImageUrl, processImageElementInfo, resizeAndConvertImgBuffer, resizeImgBase64, saveBase64Image, scaleImage, validateScreenshotBuffer, zoomForGPT4o };
4
+ export { annotateRects, compositeElementInfoImg, compositePointMarkerImg, convertImgBufferToJpeg, createImgBase64ByFormat, cropByRect, httpImg2Base64, imageInfoOfBase64, inferBase64ImageFormat, isValidImageBuffer, isValidJPEGImageBuffer, isValidPNGImageBuffer, localImg2Base64, normalizeBase64Image, normalizeScreenshotBase64, paddingToMatchBlockByBase64, parseBase64, preProcessImageUrl, processImageElementInfo, resizeAndConvertImgBuffer, resizeImgBase64, saveBase64Image, scaleImage, validateScreenshotBuffer, zoomForGPT4o };
@@ -70,6 +70,23 @@ async function resizeAndConvertImgBuffer(inputFormat, inputData, newSize) {
70
70
  };
71
71
  }
72
72
  const normalizeBase64Body = (body)=>body.replace(/\s/g, '');
73
+ async function convertImgBufferToJpeg(inputData, quality = 90) {
74
+ if (ifInNode) try {
75
+ const Sharp = await get_sharp();
76
+ return await Sharp(inputData).jpeg({
77
+ quality
78
+ }).toBuffer();
79
+ } catch (error) {
80
+ imgDebug('Sharp failed, falling back to Photon:', error);
81
+ }
82
+ const mimeType = detectImageMimeTypeFromBuffer(inputData) ?? 'image/png';
83
+ const photonImage = await photonFromBase64(`data:${mimeType};base64,${inputData.toString('base64')}`);
84
+ try {
85
+ return Buffer.from(photonImage.get_bytes_jpeg(quality));
86
+ } finally{
87
+ photonImage.free();
88
+ }
89
+ }
73
90
  const base64ImageDataUrlPattern = /^data:image\/[a-zA-Z0-9.+-]+;base64,/i;
74
91
  const supportedScreenshotDataUriPattern = /^data:image\/(png|jpe?g);base64,([\s\S]*)$/i;
75
92
  const rawBase64BodyPattern = /^[A-Za-z0-9+/=\s]+$/;
@@ -310,4 +327,4 @@ async function scaleImage(imageBase64, scale) {
310
327
  imageBase64: base64
311
328
  };
312
329
  }
313
- export { createImgBase64ByFormat, cropByRect, httpImg2Base64, inferBase64ImageFormat, localImg2Base64, normalizeBase64Body, normalizeBase64Image, normalizeScreenshotBase64, paddingToMatchBlock, paddingToMatchBlockByBase64, parseBase64, photonFromBase64, photonToBase64, preProcessImageUrl, resizeAndConvertImgBuffer, resizeImgBase64, saveBase64Image, scaleImage, zoomForGPT4o };
330
+ export { convertImgBufferToJpeg, createImgBase64ByFormat, cropByRect, httpImg2Base64, inferBase64ImageFormat, localImg2Base64, normalizeBase64Body, normalizeBase64Image, normalizeScreenshotBase64, paddingToMatchBlock, paddingToMatchBlockByBase64, parseBase64, photonFromBase64, photonToBase64, preProcessImageUrl, resizeAndConvertImgBuffer, resizeImgBase64, saveBase64Image, scaleImage, zoomForGPT4o };
@@ -6,32 +6,21 @@ import { getMidsceneRunSubDir } from "./common.mjs";
6
6
  import { ifInNode } from "./utils.mjs";
7
7
  const topicPrefix = 'midscene';
8
8
  const logStreams = new Map();
9
- const backpressuredLogStreams = new Set();
10
- const unavailableLogStreams = new Set();
11
9
  const debugInstances = new Map();
12
10
  function getLogStream(topic) {
13
11
  const topicFileName = topic.replace(/:/g, '-');
14
- if (unavailableLogStreams.has(topicFileName)) return null;
15
12
  if (!logStreams.has(topicFileName)) {
16
13
  const logFile = node_path.join(getMidsceneRunSubDir('log'), `${topicFileName}.log`);
17
14
  const stream = node_fs.createWriteStream(logFile, {
18
15
  flags: 'a'
19
16
  });
20
- stream.on('error', ()=>{
21
- unavailableLogStreams.add(topicFileName);
22
- backpressuredLogStreams.delete(topicFileName);
23
- if (logStreams.get(topicFileName) === stream) logStreams.delete(topicFileName);
24
- });
25
17
  logStreams.set(topicFileName, stream);
26
18
  }
27
- return logStreams.get(topicFileName) ?? null;
19
+ return logStreams.get(topicFileName);
28
20
  }
29
21
  function writeLogToFile(topic, message) {
30
22
  if (!ifInNode) return;
31
- const topicFileName = topic.replace(/:/g, '-');
32
- if (backpressuredLogStreams.has(topicFileName)) return;
33
23
  const stream = getLogStream(topic);
34
- if (!stream) return;
35
24
  const now = new Date();
36
25
  const isoDate = now.toLocaleDateString('sv-SE');
37
26
  const isoTime = now.toLocaleTimeString('sv-SE');
@@ -42,17 +31,7 @@ function writeLogToFile(topic, message) {
42
31
  const minutes = (Math.abs(timezoneOffsetMinutes) % 60).toString().padStart(2, '0');
43
32
  const timezoneString = `${sign}${hours}:${minutes}`;
44
33
  const localISOTime = `${isoDate}T${isoTime}.${milliseconds}${timezoneString}`;
45
- try {
46
- if (!stream.write(`[${localISOTime}] ${message}\n`)) {
47
- backpressuredLogStreams.add(topicFileName);
48
- stream.once('drain', ()=>{
49
- backpressuredLogStreams.delete(topicFileName);
50
- });
51
- }
52
- } catch {
53
- unavailableLogStreams.add(topicFileName);
54
- backpressuredLogStreams.delete(topicFileName);
55
- }
34
+ stream.write(`[${localISOTime}] ${message}\n`);
56
35
  }
57
36
  function getDebug(topic, options) {
58
37
  const fullTopic = `${topicPrefix}:${topic}`;
@@ -37,7 +37,7 @@ const external_utils_js_namespaceObject = require("../utils.js");
37
37
  const external_helper_js_namespaceObject = require("./helper.js");
38
38
  const external_init_debug_js_namespaceObject = require("./init-debug.js");
39
39
  const MODEL_CONFIG_DOC_URL = 'https://midscenejs.com/model-common-config.html';
40
- const getCurrentVersion = ()=>"1.10.5-beta-20260716114658.0";
40
+ const getCurrentVersion = ()=>"1.10.5";
41
41
  const getInvalidModelFamilyMessage = (modelFamily)=>`Invalid MIDSCENE_MODEL_FAMILY value: ${modelFamily}. Current version v${getCurrentVersion()} accepts the following model families: ${external_types_js_namespaceObject.MODEL_FAMILY_VALUES.join(', ')}. You can also visit ${MODEL_CONFIG_DOC_URL} for the latest configuration information.`;
42
42
  const KEYS_MAP = {
43
43
  insight: external_constants_js_namespaceObject.INSIGHT_MODEL_CONFIG_KEYS,
@@ -24,9 +24,10 @@ var __webpack_require__ = {};
24
24
  var __webpack_exports__ = {};
25
25
  __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
- paddingToMatchBlockByBase64: ()=>external_transform_js_namespaceObject.paddingToMatchBlockByBase64,
27
+ convertImgBufferToJpeg: ()=>external_transform_js_namespaceObject.convertImgBufferToJpeg,
28
28
  inferBase64ImageFormat: ()=>external_transform_js_namespaceObject.inferBase64ImageFormat,
29
29
  localImg2Base64: ()=>external_transform_js_namespaceObject.localImg2Base64,
30
+ paddingToMatchBlockByBase64: ()=>external_transform_js_namespaceObject.paddingToMatchBlockByBase64,
30
31
  parseBase64: ()=>external_transform_js_namespaceObject.parseBase64,
31
32
  resizeAndConvertImgBuffer: ()=>external_transform_js_namespaceObject.resizeAndConvertImgBuffer,
32
33
  resizeImgBase64: ()=>external_transform_js_namespaceObject.resizeImgBase64,
@@ -55,6 +56,7 @@ const external_box_select_js_namespaceObject = require("./box-select.js");
55
56
  exports.annotateRects = __webpack_exports__.annotateRects;
56
57
  exports.compositeElementInfoImg = __webpack_exports__.compositeElementInfoImg;
57
58
  exports.compositePointMarkerImg = __webpack_exports__.compositePointMarkerImg;
59
+ exports.convertImgBufferToJpeg = __webpack_exports__.convertImgBufferToJpeg;
58
60
  exports.createImgBase64ByFormat = __webpack_exports__.createImgBase64ByFormat;
59
61
  exports.cropByRect = __webpack_exports__.cropByRect;
60
62
  exports.httpImg2Base64 = __webpack_exports__.httpImg2Base64;
@@ -80,6 +82,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
80
82
  "annotateRects",
81
83
  "compositeElementInfoImg",
82
84
  "compositePointMarkerImg",
85
+ "convertImgBufferToJpeg",
83
86
  "createImgBase64ByFormat",
84
87
  "cropByRect",
85
88
  "httpImg2Base64",
@@ -33,12 +33,13 @@ var __webpack_require__ = {};
33
33
  var __webpack_exports__ = {};
34
34
  __webpack_require__.r(__webpack_exports__);
35
35
  __webpack_require__.d(__webpack_exports__, {
36
- paddingToMatchBlockByBase64: ()=>paddingToMatchBlockByBase64,
36
+ convertImgBufferToJpeg: ()=>convertImgBufferToJpeg,
37
37
  inferBase64ImageFormat: ()=>inferBase64ImageFormat,
38
38
  localImg2Base64: ()=>localImg2Base64,
39
+ paddingToMatchBlockByBase64: ()=>paddingToMatchBlockByBase64,
39
40
  photonFromBase64: ()=>photonFromBase64,
40
- photonToBase64: ()=>photonToBase64,
41
41
  parseBase64: ()=>parseBase64,
42
+ httpImg2Base64: ()=>httpImg2Base64,
42
43
  resizeAndConvertImgBuffer: ()=>resizeAndConvertImgBuffer,
43
44
  resizeImgBase64: ()=>resizeImgBase64,
44
45
  saveBase64Image: ()=>saveBase64Image,
@@ -51,7 +52,7 @@ __webpack_require__.d(__webpack_exports__, {
51
52
  zoomForGPT4o: ()=>zoomForGPT4o,
52
53
  createImgBase64ByFormat: ()=>createImgBase64ByFormat,
53
54
  paddingToMatchBlock: ()=>paddingToMatchBlock,
54
- httpImg2Base64: ()=>httpImg2Base64
55
+ photonToBase64: ()=>photonToBase64
55
56
  });
56
57
  const external_node_assert_namespaceObject = require("node:assert");
57
58
  var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
@@ -129,6 +130,23 @@ async function resizeAndConvertImgBuffer(inputFormat, inputData, newSize) {
129
130
  };
130
131
  }
131
132
  const normalizeBase64Body = (body)=>body.replace(/\s/g, '');
133
+ async function convertImgBufferToJpeg(inputData, quality = 90) {
134
+ if (external_utils_js_namespaceObject.ifInNode) try {
135
+ const Sharp = await external_get_sharp_js_default()();
136
+ return await Sharp(inputData).jpeg({
137
+ quality
138
+ }).toBuffer();
139
+ } catch (error) {
140
+ imgDebug('Sharp failed, falling back to Photon:', error);
141
+ }
142
+ const mimeType = detectImageMimeTypeFromBuffer(inputData) ?? 'image/png';
143
+ const photonImage = await photonFromBase64(`data:${mimeType};base64,${inputData.toString('base64')}`);
144
+ try {
145
+ return external_node_buffer_namespaceObject.Buffer.from(photonImage.get_bytes_jpeg(quality));
146
+ } finally{
147
+ photonImage.free();
148
+ }
149
+ }
132
150
  const base64ImageDataUrlPattern = /^data:image\/[a-zA-Z0-9.+-]+;base64,/i;
133
151
  const supportedScreenshotDataUriPattern = /^data:image\/(png|jpe?g);base64,([\s\S]*)$/i;
134
152
  const rawBase64BodyPattern = /^[A-Za-z0-9+/=\s]+$/;
@@ -369,6 +387,7 @@ async function scaleImage(imageBase64, scale) {
369
387
  imageBase64: base64
370
388
  };
371
389
  }
390
+ exports.convertImgBufferToJpeg = __webpack_exports__.convertImgBufferToJpeg;
372
391
  exports.createImgBase64ByFormat = __webpack_exports__.createImgBase64ByFormat;
373
392
  exports.cropByRect = __webpack_exports__.cropByRect;
374
393
  exports.httpImg2Base64 = __webpack_exports__.httpImg2Base64;
@@ -389,6 +408,7 @@ exports.saveBase64Image = __webpack_exports__.saveBase64Image;
389
408
  exports.scaleImage = __webpack_exports__.scaleImage;
390
409
  exports.zoomForGPT4o = __webpack_exports__.zoomForGPT4o;
391
410
  for(var __rspack_i in __webpack_exports__)if (-1 === [
411
+ "convertImgBufferToJpeg",
392
412
  "createImgBase64ByFormat",
393
413
  "cropByRect",
394
414
  "httpImg2Base64",
@@ -48,32 +48,21 @@ const external_common_js_namespaceObject = require("./common.js");
48
48
  const external_utils_js_namespaceObject = require("./utils.js");
49
49
  const topicPrefix = 'midscene';
50
50
  const logStreams = new Map();
51
- const backpressuredLogStreams = new Set();
52
- const unavailableLogStreams = new Set();
53
51
  const debugInstances = new Map();
54
52
  function getLogStream(topic) {
55
53
  const topicFileName = topic.replace(/:/g, '-');
56
- if (unavailableLogStreams.has(topicFileName)) return null;
57
54
  if (!logStreams.has(topicFileName)) {
58
55
  const logFile = external_node_path_default().join((0, external_common_js_namespaceObject.getMidsceneRunSubDir)('log'), `${topicFileName}.log`);
59
56
  const stream = external_node_fs_default().createWriteStream(logFile, {
60
57
  flags: 'a'
61
58
  });
62
- stream.on('error', ()=>{
63
- unavailableLogStreams.add(topicFileName);
64
- backpressuredLogStreams.delete(topicFileName);
65
- if (logStreams.get(topicFileName) === stream) logStreams.delete(topicFileName);
66
- });
67
59
  logStreams.set(topicFileName, stream);
68
60
  }
69
- return logStreams.get(topicFileName) ?? null;
61
+ return logStreams.get(topicFileName);
70
62
  }
71
63
  function writeLogToFile(topic, message) {
72
64
  if (!external_utils_js_namespaceObject.ifInNode) return;
73
- const topicFileName = topic.replace(/:/g, '-');
74
- if (backpressuredLogStreams.has(topicFileName)) return;
75
65
  const stream = getLogStream(topic);
76
- if (!stream) return;
77
66
  const now = new Date();
78
67
  const isoDate = now.toLocaleDateString('sv-SE');
79
68
  const isoTime = now.toLocaleTimeString('sv-SE');
@@ -84,17 +73,7 @@ function writeLogToFile(topic, message) {
84
73
  const minutes = (Math.abs(timezoneOffsetMinutes) % 60).toString().padStart(2, '0');
85
74
  const timezoneString = `${sign}${hours}:${minutes}`;
86
75
  const localISOTime = `${isoDate}T${isoTime}.${milliseconds}${timezoneString}`;
87
- try {
88
- if (!stream.write(`[${localISOTime}] ${message}\n`)) {
89
- backpressuredLogStreams.add(topicFileName);
90
- stream.once('drain', ()=>{
91
- backpressuredLogStreams.delete(topicFileName);
92
- });
93
- }
94
- } catch {
95
- unavailableLogStreams.add(topicFileName);
96
- backpressuredLogStreams.delete(topicFileName);
97
- }
76
+ stream.write(`[${localISOTime}] ${message}\n`);
98
77
  }
99
78
  function getDebug(topic, options) {
100
79
  const fullTopic = `${topicPrefix}:${topic}`;
@@ -1,3 +1,3 @@
1
1
  export { imageInfoOfBase64, isValidPNGImageBuffer, isValidJPEGImageBuffer, isValidImageBuffer, validateScreenshotBuffer, type ValidateScreenshotBufferOptions, } from './info';
2
- export { resizeAndConvertImgBuffer, resizeImgBase64, zoomForGPT4o, saveBase64Image, paddingToMatchBlockByBase64, cropByRect, scaleImage, localImg2Base64, httpImg2Base64, preProcessImageUrl, parseBase64, createImgBase64ByFormat, inferBase64ImageFormat, normalizeBase64Image, normalizeScreenshotBase64, type NormalizeScreenshotBase64Options, } from './transform';
2
+ export { resizeAndConvertImgBuffer, convertImgBufferToJpeg, resizeImgBase64, zoomForGPT4o, saveBase64Image, paddingToMatchBlockByBase64, cropByRect, scaleImage, localImg2Base64, httpImg2Base64, preProcessImageUrl, parseBase64, createImgBase64ByFormat, inferBase64ImageFormat, normalizeBase64Image, normalizeScreenshotBase64, type NormalizeScreenshotBase64Options, } from './transform';
3
3
  export { processImageElementInfo, compositeElementInfoImg, compositePointMarkerImg, annotateRects, } from './box-select';
@@ -27,6 +27,8 @@ export declare function resizeAndConvertImgBuffer(inputFormat: string, inputData
27
27
  format: string;
28
28
  }>;
29
29
  export declare const normalizeBase64Body: (body: string) => string;
30
+ /** Convert an image buffer to JPEG without changing its dimensions. */
31
+ export declare function convertImgBufferToJpeg(inputData: Buffer, quality?: number): Promise<Buffer>;
30
32
  export declare const inferBase64ImageFormat: (base64Body: string) => "jpeg" | "png";
31
33
  export declare const createImgBase64ByFormat: (format: string, body: string) => string;
32
34
  export interface NormalizeScreenshotBase64Options {
@@ -13,18 +13,6 @@ export interface MidsceneRecorderPageInfo {
13
13
  width: number;
14
14
  height: number;
15
15
  }
16
- /**
17
- * A screenshot stored outside the recording event payload.
18
- *
19
- * Recorder events are persisted in the Studio renderer. Keeping full data
20
- * URLs there makes long recordings retain every screenshot in the renderer
21
- * heap, so screenshot bytes live in the Playground run directory instead.
22
- */
23
- export interface MidsceneRecorderScreenshotAssetRef {
24
- id: string;
25
- mimeType: string;
26
- bytes: number;
27
- }
28
16
  export interface MidsceneRecorderEvent {
29
17
  type: MidsceneRecorderEventType;
30
18
  source?: MidsceneRecorderSourceKind;
@@ -37,8 +25,6 @@ export interface MidsceneRecorderEvent {
37
25
  pageInfo: MidsceneRecorderPageInfo;
38
26
  screenshotBefore?: string;
39
27
  screenshotAfter?: string;
40
- /** The single screenshot retained for AI description and Markdown export. */
41
- screenshotAsset?: MidsceneRecorderScreenshotAssetRef;
42
28
  semantic?: MidsceneRecorderSemantic;
43
29
  elementDescription?: string;
44
30
  descriptionLoading?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/shared",
3
- "version": "1.10.5-beta-20260716114658.0",
3
+ "version": "1.10.5",
4
4
  "repository": "https://github.com/web-infra-dev/midscene",
5
5
  "homepage": "https://midscenejs.com/",
6
6
  "types": "./dist/types/index.d.ts",
package/src/img/index.ts CHANGED
@@ -8,6 +8,7 @@ export {
8
8
  } from './info';
9
9
  export {
10
10
  resizeAndConvertImgBuffer,
11
+ convertImgBufferToJpeg,
11
12
  resizeImgBase64,
12
13
  zoomForGPT4o,
13
14
  saveBase64Image,
@@ -156,6 +156,31 @@ export async function resizeAndConvertImgBuffer(
156
156
 
157
157
  export const normalizeBase64Body = (body: string) => body.replace(/\s/g, '');
158
158
 
159
+ /** Convert an image buffer to JPEG without changing its dimensions. */
160
+ export async function convertImgBufferToJpeg(
161
+ inputData: Buffer,
162
+ quality = 90,
163
+ ): Promise<Buffer> {
164
+ if (ifInNode) {
165
+ try {
166
+ const Sharp = await getSharp();
167
+ return await Sharp(inputData).jpeg({ quality }).toBuffer();
168
+ } catch (error) {
169
+ imgDebug('Sharp failed, falling back to Photon:', error);
170
+ }
171
+ }
172
+
173
+ const mimeType = detectImageMimeTypeFromBuffer(inputData) ?? 'image/png';
174
+ const photonImage = await photonFromBase64(
175
+ `data:${mimeType};base64,${inputData.toString('base64')}`,
176
+ );
177
+ try {
178
+ return Buffer.from(photonImage.get_bytes_jpeg(quality));
179
+ } finally {
180
+ photonImage.free();
181
+ }
182
+ }
183
+
159
184
  const base64ImageDataUrlPattern = /^data:image\/[a-zA-Z0-9.+-]+;base64,/i;
160
185
  const supportedScreenshotDataUriPattern =
161
186
  /^data:image\/(png|jpe?g);base64,([\s\S]*)$/i;
package/src/logger.ts CHANGED
@@ -8,53 +8,28 @@ import { ifInNode } from './utils';
8
8
  const topicPrefix = 'midscene';
9
9
  // Map to store file streams
10
10
  const logStreams = new Map<string, fs.WriteStream>();
11
- // A WriteStream queues every write made after it reports backpressure. The
12
- // main process can keep running while macOS has deprioritized a backgrounded
13
- // app, so retaining diagnostic logs here can otherwise grow without bound.
14
- // Drop best-effort logs until the stream drains instead.
15
- const backpressuredLogStreams = new Set<string>();
16
- const unavailableLogStreams = new Set<string>();
17
11
  // Map to store debug instances
18
12
  const debugInstances = new Map<string, DebugFunction>();
19
13
 
20
14
  // Function to get or create a log stream
21
- function getLogStream(topic: string): fs.WriteStream | null {
15
+ function getLogStream(topic: string): fs.WriteStream {
22
16
  const topicFileName = topic.replace(/:/g, '-');
23
- if (unavailableLogStreams.has(topicFileName)) {
24
- return null;
25
- }
26
17
  if (!logStreams.has(topicFileName)) {
27
18
  const logFile = path.join(
28
19
  getMidsceneRunSubDir('log'),
29
20
  `${topicFileName}.log`,
30
21
  );
31
22
  const stream = fs.createWriteStream(logFile, { flags: 'a' });
32
- // A stream error without a listener terminates the Electron main process.
33
- // Logging must remain best-effort, so disable this topic after a file error
34
- // rather than repeatedly queuing writes to a broken stream.
35
- stream.on('error', () => {
36
- unavailableLogStreams.add(topicFileName);
37
- backpressuredLogStreams.delete(topicFileName);
38
- if (logStreams.get(topicFileName) === stream) {
39
- logStreams.delete(topicFileName);
40
- }
41
- });
42
23
  logStreams.set(topicFileName, stream);
43
24
  }
44
- return logStreams.get(topicFileName) ?? null;
25
+ return logStreams.get(topicFileName)!;
45
26
  }
46
27
 
47
28
  // Function to write log to file
48
29
  function writeLogToFile(topic: string, message: string): void {
49
30
  if (!ifInNode) return;
50
31
 
51
- const topicFileName = topic.replace(/:/g, '-');
52
- if (backpressuredLogStreams.has(topicFileName)) {
53
- return;
54
- }
55
-
56
32
  const stream = getLogStream(topic);
57
- if (!stream) return;
58
33
  // Generate ISO format timestamp with local timezone
59
34
  const now = new Date();
60
35
  // Use sv-SE locale to get ISO-like format (YYYY-MM-DD HH:mm:ss)
@@ -72,17 +47,7 @@ function writeLogToFile(topic: string, message: string): void {
72
47
  .padStart(2, '0');
73
48
  const timezoneString = `${sign}${hours}:${minutes}`;
74
49
  const localISOTime = `${isoDate}T${isoTime}.${milliseconds}${timezoneString}`;
75
- try {
76
- if (!stream.write(`[${localISOTime}] ${message}\n`)) {
77
- backpressuredLogStreams.add(topicFileName);
78
- stream.once('drain', () => {
79
- backpressuredLogStreams.delete(topicFileName);
80
- });
81
- }
82
- } catch {
83
- unavailableLogStreams.add(topicFileName);
84
- backpressuredLogStreams.delete(topicFileName);
85
- }
50
+ stream.write(`[${localISOTime}] ${message}\n`);
86
51
  }
87
52
 
88
53
  export type DebugFunction = (...args: unknown[]) => void;
package/src/recorder.ts CHANGED
@@ -34,19 +34,6 @@ export interface MidsceneRecorderPageInfo {
34
34
  height: number;
35
35
  }
36
36
 
37
- /**
38
- * A screenshot stored outside the recording event payload.
39
- *
40
- * Recorder events are persisted in the Studio renderer. Keeping full data
41
- * URLs there makes long recordings retain every screenshot in the renderer
42
- * heap, so screenshot bytes live in the Playground run directory instead.
43
- */
44
- export interface MidsceneRecorderScreenshotAssetRef {
45
- id: string;
46
- mimeType: string;
47
- bytes: number;
48
- }
49
-
50
37
  export interface MidsceneRecorderEvent {
51
38
  type: MidsceneRecorderEventType;
52
39
  source?: MidsceneRecorderSourceKind;
@@ -59,8 +46,6 @@ export interface MidsceneRecorderEvent {
59
46
  pageInfo: MidsceneRecorderPageInfo;
60
47
  screenshotBefore?: string;
61
48
  screenshotAfter?: string;
62
- /** The single screenshot retained for AI description and Markdown export. */
63
- screenshotAsset?: MidsceneRecorderScreenshotAssetRef;
64
49
  semantic?: MidsceneRecorderSemantic;
65
50
  elementDescription?: string;
66
51
  descriptionLoading?: boolean;