tasmota-esp-web-tools 12.1.2 → 12.2.1

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/README.md CHANGED
@@ -108,6 +108,68 @@ The `chipVariant` field is optional. If omitted, the build will match any varian
108
108
 
109
109
  See [manifest-example-p4-variants.json](manifest-example-p4-variants.json) for a complete example.
110
110
 
111
+ ## Flash Size Support
112
+
113
+ For chips with different flash sizes, you can specify `flashSizeMB` to target specific hardware configurations. This is useful for ESP32-S3 modules that come with various flash sizes.
114
+
115
+ ```json
116
+ {
117
+ "name": "My Firmware",
118
+ "builds": [
119
+ {
120
+ "chipFamily": "ESP32-S3",
121
+ "flashSizeMB": 16,
122
+ "parts": [{ "path": "s3-16mb.bin", "offset": 0 }]
123
+ },
124
+ {
125
+ "chipFamily": "ESP32-S3",
126
+ "flashSizeMB": 4,
127
+ "parts": [{ "path": "s3-4mb.bin", "offset": 0 }]
128
+ },
129
+ {
130
+ "chipFamily": "ESP32-S3",
131
+ "parts": [{ "path": "s3-generic.bin", "offset": 0 }]
132
+ }
133
+ ]
134
+ }
135
+ ```
136
+
137
+ A device with ESP32-S3 and 16MB flash gets the first build, 4MB gets the second, and any other ESP32-S3 falls back to the third.
138
+
139
+ The `flashSizeMB` field is **optional**. If omitted, the build will match any flash size. Builds with matching `flashSizeMB` are preferred over builds without it (most-specific-matching algorithm).
140
+
141
+ ## USB Interface Support (UART vs CDC)
142
+
143
+ For chips that can be connected either through a native USB interface (USB-JTAG/USB-OTG, e.g. ESP32-S2/S3/C3/C6/...) or through an external USB-to-Serial bridge (CP210x, FTDI, CH340, ...), you can ship dedicated firmware variants by specifying `usbInterface`:
144
+
145
+ - `"CDC"` – firmware built for native USB (CDC) console / programming
146
+ - `"UART"` – firmware built for the regular UART console via an external USB-to-Serial chip
147
+
148
+ ```json
149
+ {
150
+ "name": "My Firmware",
151
+ "builds": [
152
+ {
153
+ "chipFamily": "ESP32-S3",
154
+ "usbInterface": "CDC",
155
+ "parts": [{ "path": "s3-cdc.bin", "offset": 0 }]
156
+ },
157
+ {
158
+ "chipFamily": "ESP32-S3",
159
+ "usbInterface": "UART",
160
+ "parts": [{ "path": "s3-uart.bin", "offset": 0 }]
161
+ }
162
+ ]
163
+ }
164
+ ```
165
+
166
+ ESP Web Tools automatically detects how the device is connected:
167
+
168
+ - If the device is reached via native USB (USB-JTAG/USB-OTG) the build with `usbInterface: "CDC"` is selected.
169
+ - Otherwise (external USB-to-Serial bridge) the build with `usbInterface: "UART"` is selected.
170
+
171
+ The `usbInterface` field is **optional**. If omitted, the build will match any USB interface and is used as a fallback. It can also be combined freely with `chipVariant` and `flashSizeMB`.
172
+
111
173
  ## Performance
112
174
 
113
175
  ESP Web Tools supports configurable baud rates for flashing. By default, it uses 115200 baud for maximum compatibility. You can increase the baud rate for significantly faster flashing speeds.
@@ -20,11 +20,13 @@ async function loadLittleFS() {
20
20
  try {
21
21
  // Try to import from the calculated path
22
22
  const indexUrl = _wasmBasePath + "index.js";
23
+ // eslint-disable-next-line no-console
23
24
  console.log("[LittleFS] Loading module from:", indexUrl);
24
25
  _littleFSModule = await import(/* @vite-ignore */ indexUrl);
25
26
  return _littleFSModule;
26
27
  }
27
28
  catch (err) {
29
+ // eslint-disable-next-line no-console
28
30
  console.error("[LittleFS] Failed to load from calculated path:", _wasmBasePath, err);
29
31
  // Fallback to relative import (for local development)
30
32
  try {
@@ -32,6 +34,7 @@ async function loadLittleFS() {
32
34
  return _littleFSModule;
33
35
  }
34
36
  catch (fallbackErr) {
37
+ // eslint-disable-next-line no-console
35
38
  console.error("[LittleFS] Fallback import also failed:", fallbackErr);
36
39
  throw new Error(`Failed to load LittleFS module: ${err}`);
37
40
  }
package/dist/connect.js CHANGED
@@ -50,6 +50,7 @@ export const connect = async (button) => {
50
50
  esploader = await esptoolConnect({
51
51
  log: () => { }, // Silent logger for connection
52
52
  debug: () => { },
53
+ // eslint-disable-next-line no-console
53
54
  error: (msg) => console.error(msg),
54
55
  });
55
56
  }
package/dist/const.d.ts CHANGED
@@ -6,6 +6,14 @@ export interface Logger {
6
6
  export interface Build {
7
7
  chipFamily: "ESP32" | "ESP8266" | "ESP32-S2" | "ESP32-S3" | "ESP32-C2" | "ESP32-C3" | "ESP32-C5" | "ESP32-C6" | "ESP32-C61" | "ESP32-H2" | "ESP32-P4";
8
8
  chipVariant?: string;
9
+ flashSizeMB?: number;
10
+ /**
11
+ * Optional USB interface qualifier.
12
+ * - "CDC": native USB CDC (USB-JTAG/USB-OTG, e.g. direct USB on S2/S3/C3/C6/...)
13
+ * - "UART": external USB-to-Serial bridge (CP210x, FTDI, CH340, ...)
14
+ * If omitted, the build will match any USB interface.
15
+ */
16
+ usbInterface?: "UART" | "CDC";
9
17
  parts: {
10
18
  path: string;
11
19
  offset: number;
@@ -29,6 +37,7 @@ export interface BaseFlashState {
29
37
  build?: Build;
30
38
  chipFamily?: Build["chipFamily"] | "Unknown Chip";
31
39
  chipVariant?: string | null;
40
+ flashSize?: string;
32
41
  }
33
42
  export interface InitializingState extends BaseFlashState {
34
43
  state: FlashStateType.INITIALIZING;
package/dist/flash.js CHANGED
@@ -1,18 +1,87 @@
1
1
  import { getChipFamilyName } from "./util/chip-family-name";
2
2
  import { sleep } from "./util/sleep";
3
3
  import { corsProxyFetch } from "./util/cors-proxy";
4
+ /**
5
+ * Parse flash size string (e.g., "4MB", "8MB", "16MB") to megabytes number
6
+ */
7
+ function parseFlashSizeToMB(flashSize) {
8
+ if (!flashSize)
9
+ return undefined;
10
+ const match = flashSize.match(/^(\d+)(MB|GB)$/);
11
+ if (!match)
12
+ return undefined;
13
+ const size = parseInt(match[1], 10);
14
+ const unit = match[2];
15
+ if (unit === "GB")
16
+ return size * 1024;
17
+ return size;
18
+ }
19
+ /**
20
+ * Select the best build using most-specific-matching algorithm
21
+ * - Builds with matching usbInterface are strongly preferred
22
+ * - Builds with matching flashSizeMB are preferred
23
+ * - Among builds with same specificity, first one wins
24
+ * - Builds without these qualifiers are fallback options
25
+ */
26
+ function selectBestBuild(builds, detectedFlashSizeMB, detectedUsbInterface) {
27
+ if (builds.length === 0)
28
+ return undefined;
29
+ // Score builds: higher score = more specific match
30
+ let bestBuild;
31
+ let bestScore = -Infinity;
32
+ for (const build of builds) {
33
+ let score = 0;
34
+ // USB interface match - if specified, must match
35
+ if (build.usbInterface !== undefined) {
36
+ if (detectedUsbInterface !== undefined &&
37
+ build.usbInterface === detectedUsbInterface) {
38
+ score += 1000; // Strong preference for explicit usbInterface match
39
+ }
40
+ else {
41
+ // Mismatched usbInterface - disqualify this build
42
+ continue;
43
+ }
44
+ }
45
+ // Builds without usbInterface stay neutral (compatible with any)
46
+ // Flash size match gives second priority
47
+ if (build.flashSizeMB !== undefined && detectedFlashSizeMB !== undefined) {
48
+ if (build.flashSizeMB === detectedFlashSizeMB) {
49
+ score += 100; // Exact flash size match
50
+ }
51
+ else {
52
+ score -= 1; // Penalize non-matching specific builds
53
+ }
54
+ }
55
+ else if (build.flashSizeMB !== undefined) {
56
+ // flashSizeMB is defined but detectedFlashSizeMB is undefined
57
+ score -= 1; // Penalize non-matching specific builds
58
+ }
59
+ // Generic builds (flashSizeMB undefined) stay at score 0
60
+ // Prefer this build if it has higher score
61
+ // If same score, keep the first one (stable selection)
62
+ if (score > bestScore) {
63
+ bestScore = score;
64
+ bestBuild = build;
65
+ }
66
+ }
67
+ return bestScore >= 0 ? bestBuild : undefined;
68
+ }
4
69
  export const flash = async (onEvent, esploader, // ESPLoader instance from tasmota-webserial-esptool
5
70
  logger, manifestPath, eraseFirst, firmwareBuffer, _baudRate) => {
6
71
  let manifest;
7
72
  let build;
73
+ // eslint-disable-next-line prefer-const
8
74
  let chipFamily;
9
75
  let chipVariant = null;
76
+ // eslint-disable-next-line prefer-const
77
+ let flashSize;
10
78
  const fireStateEvent = (stateUpdate) => onEvent({
11
79
  ...stateUpdate,
12
80
  manifest,
13
81
  build,
14
82
  chipFamily,
15
83
  chipVariant,
84
+ flashSize,
16
85
  });
17
86
  let manifestProm = null;
18
87
  let manifestURL = "";
@@ -51,9 +120,34 @@ logger, manifestPath, eraseFirst, firmwareBuffer, _baudRate) => {
51
120
  }
52
121
  chipFamily = getChipFamilyName(esploader);
53
122
  chipVariant = esploader.chipVariant;
123
+ // Detect flash size if not already detected
124
+ if (!esploader.flashSize && esploader.detectFlashSize) {
125
+ try {
126
+ await esploader.detectFlashSize();
127
+ }
128
+ catch (err) {
129
+ logger.debug("Failed to detect flash size:", err);
130
+ }
131
+ }
132
+ flashSize = esploader.flashSize; // e.g., "4MB", "8MB"
133
+ const flashSizeMB = flashSize ? parseFlashSizeToMB(flashSize) : undefined;
134
+ // Detect USB connection type to pick CDC vs UART firmware variants
135
+ // - true: native USB (USB-JTAG/USB-OTG) -> CDC
136
+ // - false: external USB-to-Serial bridge -> UART
137
+ let detectedUsbInterface;
138
+ if (typeof esploader.detectUsbConnectionType === "function") {
139
+ try {
140
+ const isUsbJtagOrOtg = await esploader.detectUsbConnectionType();
141
+ detectedUsbInterface = isUsbJtagOrOtg ? "CDC" : "UART";
142
+ logger.debug(`Detected USB interface: ${detectedUsbInterface}`);
143
+ }
144
+ catch (err) {
145
+ logger.debug("Failed to detect USB connection type:", err);
146
+ }
147
+ }
54
148
  fireStateEvent({
55
149
  state: "initializing" /* FlashStateType.INITIALIZING */,
56
- message: `Initialized. Found ${chipFamily}${chipVariant ? ` (${chipVariant})` : ""}`,
150
+ message: `Initialized. Found ${chipFamily}${chipVariant ? ` (${chipVariant})` : ""}${flashSize ? `, ${flashSize}` : ""}`,
57
151
  details: { done: true },
58
152
  });
59
153
  fireStateEvent({
@@ -73,17 +167,24 @@ logger, manifestPath, eraseFirst, firmwareBuffer, _baudRate) => {
73
167
  await esploader.disconnect();
74
168
  return;
75
169
  }
76
- build = manifest.builds.find((b) => {
77
- // Match chipFamily and optionally chipVariant
170
+ // Filter builds by chipFamily and chipVariant
171
+ const compatibleBuilds = manifest.builds.filter((b) => {
78
172
  if (b.chipFamily !== chipFamily) {
79
173
  return false;
80
174
  }
81
- // If build specifies chipVariant, it must match
82
175
  if (b.chipVariant && b.chipVariant !== chipVariant) {
83
176
  return false;
84
177
  }
85
178
  return true;
86
179
  });
180
+ // Select the best build using most-specific-matching algorithm
181
+ // Prefer builds with more matching qualifiers (flashSizeMB)
182
+ const exactVariantBuilds = compatibleBuilds.filter((b) => b.chipVariant !== undefined && b.chipVariant === chipVariant);
183
+ const variantAgnosticBuilds = compatibleBuilds.filter((b) => b.chipVariant === undefined);
184
+ build = selectBestBuild(exactVariantBuilds, flashSizeMB, detectedUsbInterface);
185
+ if (!build) {
186
+ build = selectBestBuild(variantAgnosticBuilds, flashSizeMB, detectedUsbInterface);
187
+ }
87
188
  if (!build) {
88
189
  fireStateEvent({
89
190
  state: "error" /* FlashStateType.ERROR */,
@@ -45,12 +45,14 @@ export declare class EwtInstallDialog extends LitElement {
45
45
  private _consoleInitialized;
46
46
  private _improvSupported;
47
47
  private _isUsbJtagOrOtgDevice;
48
+ private _flashSize?;
48
49
  private _openConsoleAfterReconnect;
49
50
  private _visitDeviceAfterReconnect;
50
51
  private _addToHAAfterReconnect;
51
52
  private _changeWiFiAfterReconnect;
52
53
  private _ensureStub;
53
54
  private get _port();
55
+ private _probeFlashSize;
54
56
  private _isUsbJtagOrOtg;
55
57
  private _isWebUsbWithExternalSerial;
56
58
  private _releaseReaderWriter;
@@ -57,6 +57,8 @@ export class EwtInstallDialog extends LitElement {
57
57
  this._handleDisconnect = () => {
58
58
  this._state = "ERROR";
59
59
  this._error = "Disconnected";
60
+ // Reset flash size when device is actually disconnected
61
+ this._flashSize = undefined;
60
62
  };
61
63
  }
62
64
  // Ensure stub is initialized (called before any operation that needs it)
@@ -116,6 +118,8 @@ export class EwtInstallDialog extends LitElement {
116
118
  const espStub = await this.esploader.runStub();
117
119
  this.logger.log(`Stub created: IS_STUB=${espStub.IS_STUB}, chipFamily=${getChipFamilyName(espStub)}`);
118
120
  this._espStub = espStub;
121
+ // Detect flash size AFTER stub creation (flash size is only available after stub)
122
+ await this._probeFlashSize(espStub);
119
123
  // Set baudrate BEFORE any operations (use user-selected baudrate if available)
120
124
  if (this.baudRate && this.baudRate > 115200) {
121
125
  this.logger.log(`Setting baudrate to ${this.baudRate}...`);
@@ -138,6 +142,19 @@ export class EwtInstallDialog extends LitElement {
138
142
  get _port() {
139
143
  return this.esploader.port;
140
144
  }
145
+ // Helper to probe flash size from a running stub (only available after stub)
146
+ async _probeFlashSize(espStub) {
147
+ if (espStub.detectFlashSize && !this._flashSize) {
148
+ try {
149
+ await espStub.detectFlashSize();
150
+ this._flashSize = espStub.flashSize;
151
+ this.logger.log(`Flash size detected: ${this._flashSize}`);
152
+ }
153
+ catch (err) {
154
+ this.logger.debug("Failed to detect flash size:", err);
155
+ }
156
+ }
157
+ }
141
158
  // Helper to check if device is using USB-JTAG or USB-OTG (not external serial chip)
142
159
  async _isUsbJtagOrOtg() {
143
160
  // Use detectUsbConnectionType from tasmota-webserial-esptool
@@ -504,16 +521,17 @@ export class EwtInstallDialog extends LitElement {
504
521
  }
505
522
  _renderDashboard() {
506
523
  const heading = this._info.name;
507
- let content;
508
524
  const hideActions = true;
509
525
  const allowClosing = true;
510
- content = html `
526
+ const content = html `
511
527
  <ew-list>
512
528
  <ew-list-item>
513
529
  <div slot="headline">Connected to ${this._info.name}</div>
514
530
  <div slot="supporting-text">
515
531
  ${this._info.firmware}&nbsp;${this._info.version}
516
- (${this._info.chipFamily})
532
+ (${this._info.chipFamily}${this._flashSize
533
+ ? `, ${this._flashSize}`
534
+ : ""})
517
535
  </div>
518
536
  </ew-list-item>
519
537
  ${!this._isSameVersion
@@ -827,11 +845,25 @@ export class EwtInstallDialog extends LitElement {
827
845
  }
828
846
  _renderDashboardNoImprov() {
829
847
  const heading = "Device Dashboard";
830
- let content;
831
848
  const hideActions = true;
832
849
  const allowClosing = true;
833
- content = html `
850
+ // Build device info string if available
851
+ const chipFamily = this.esploader.chipFamily
852
+ ? getChipFamilyName(this.esploader)
853
+ : null;
854
+ const deviceInfo = chipFamily
855
+ ? `(${chipFamily}${this._flashSize ? `, ${this._flashSize}` : ""})`
856
+ : null;
857
+ const content = html `
834
858
  <ew-list>
859
+ ${deviceInfo
860
+ ? html `
861
+ <ew-list-item>
862
+ <div slot="headline">${chipFamily}</div>
863
+ <div slot="supporting-text">${deviceInfo}</div>
864
+ </ew-list-item>
865
+ `
866
+ : ""}
835
867
  <ew-list-item
836
868
  type="button"
837
869
  ?disabled=${this._busy}
@@ -1219,6 +1251,7 @@ export class EwtInstallDialog extends LitElement {
1219
1251
  return [heading, content];
1220
1252
  }
1221
1253
  _renderInstall() {
1254
+ var _a, _b, _c;
1222
1255
  let heading;
1223
1256
  let content;
1224
1257
  let hideActions = false;
@@ -1247,11 +1280,20 @@ export class EwtInstallDialog extends LitElement {
1247
1280
  else if (!this._installConfirmed) {
1248
1281
  heading = "Confirm Installation";
1249
1282
  const action = isUpdate ? "update to" : "install";
1283
+ // Build device info with flash size if available
1284
+ const deviceInfo = this._flashSize
1285
+ ? html ` (${((_a = this._info) === null || _a === void 0 ? void 0 : _a.chipFamily) || ""}${((_b = this._info) === null || _b === void 0 ? void 0 : _b.chipFamily)
1286
+ ? `, ${this._flashSize}`
1287
+ : this._flashSize})`
1288
+ : "";
1250
1289
  content = html `
1251
1290
  ${isUpdate
1252
1291
  ? html `Your device is running
1253
- ${this._info.firmware}&nbsp;${this._info.version}.<br /><br />`
1254
- : ""}
1292
+ ${this._info.firmware}&nbsp;${this._info
1293
+ .version}${deviceInfo}.<br /><br />`
1294
+ : deviceInfo
1295
+ ? html `Device detected: ${deviceInfo}<br /><br />`
1296
+ : ""}
1255
1297
  Do you want to ${action}
1256
1298
  ${this._manifest.name}&nbsp;${this._manifest.version}?
1257
1299
  ${this._installErase
@@ -1274,7 +1316,11 @@ export class EwtInstallDialog extends LitElement {
1274
1316
  this._installState.state === "manifest" /* FlashStateType.MANIFEST */ ||
1275
1317
  this._installState.state === "preparing" /* FlashStateType.PREPARING */) {
1276
1318
  heading = "Installing";
1277
- content = this._renderProgress("Preparing installation");
1319
+ // Show flash size in preparing message if available
1320
+ const preparingMsg = ((_c = this._installState) === null || _c === void 0 ? void 0 : _c.flashSize)
1321
+ ? `Preparing installation (${this._installState.flashSize})`
1322
+ : "Preparing installation";
1323
+ content = this._renderProgress(preparingMsg);
1278
1324
  hideActions = true;
1279
1325
  }
1280
1326
  else if (this._installState.state === "erasing" /* FlashStateType.ERASING */) {
@@ -1355,9 +1401,8 @@ export class EwtInstallDialog extends LitElement {
1355
1401
  }
1356
1402
  _renderLogs() {
1357
1403
  const heading = `Logs`;
1358
- let content;
1359
1404
  const hideActions = false;
1360
- content = html `
1405
+ const content = html `
1361
1406
  <ew-console
1362
1407
  .port=${this._port}
1363
1408
  .logger=${this.logger}
@@ -1422,7 +1467,22 @@ export class EwtInstallDialog extends LitElement {
1422
1467
  `;
1423
1468
  }
1424
1469
  else {
1470
+ // Build device info with flash size for partition view
1471
+ const chipFamily = this.esploader.chipFamily
1472
+ ? getChipFamilyName(this.esploader)
1473
+ : null;
1474
+ const deviceInfo = chipFamily
1475
+ ? `${chipFamily}${this._flashSize ? `, ${this._flashSize}` : ""}`
1476
+ : null;
1425
1477
  content = html `
1478
+ ${deviceInfo
1479
+ ? html `<div
1480
+ class="device-info"
1481
+ style="margin-bottom: 16px; font-size: 14px; color: var(--md-sys-color-on-surface-variant, #666);"
1482
+ >
1483
+ Device: ${deviceInfo}
1484
+ </div>`
1485
+ : ""}
1426
1486
  <div class="partition-list">
1427
1487
  <table class="partition-table">
1428
1488
  <thead>
@@ -1786,6 +1846,8 @@ export class EwtInstallDialog extends LitElement {
1786
1846
  this._espStub = await this.esploader.runStub();
1787
1847
  this.logger.log(`Stub created: IS_STUB=${this._espStub.IS_STUB}`);
1788
1848
  }
1849
+ // Detect flash size after stub is running
1850
+ await this._probeFlashSize(this._espStub);
1789
1851
  // CRITICAL: Save parent loader
1790
1852
  const loaderToSave = this._espStub._parent || this._espStub;
1791
1853
  this._savedLoaderBeforeConsole = loaderToSave;
@@ -2500,6 +2562,8 @@ export class EwtInstallDialog extends LitElement {
2500
2562
  await this._closeClientWithoutEvents(this._client);
2501
2563
  }
2502
2564
  document.body.style.overflow = (_a = this._bodyOverflow) !== null && _a !== void 0 ? _a : "";
2565
+ // Reset flash size when dialog is closed
2566
+ this._flashSize = undefined;
2503
2567
  fireEvent(this, "closed");
2504
2568
  this.parentNode.removeChild(this);
2505
2569
  }
@@ -2689,4 +2753,7 @@ __decorate([
2689
2753
  __decorate([
2690
2754
  state()
2691
2755
  ], EwtInstallDialog.prototype, "_isUsbJtagOrOtgDevice", void 0);
2756
+ __decorate([
2757
+ state()
2758
+ ], EwtInstallDialog.prototype, "_flashSize", void 0);
2692
2759
  customElements.define("ewt-install-dialog", EwtInstallDialog);
@@ -11,7 +11,9 @@ export class LineBreakTransformer {
11
11
  let lastIndex = 0;
12
12
  let match;
13
13
  while ((match = re.exec(this.chunks)) !== null) {
14
- if (match[0] === "\r" && re.lastIndex === this.chunks.length) {
14
+ // If this is a lone \r at the very end of the buffer, leave it so it can
15
+ // be combined with a possible following \n in the next chunk.
16
+ if (match[0] === "\r" && match.index === this.chunks.length - 1) {
15
17
  break;
16
18
  }
17
19
  const line = this.chunks.substring(lastIndex, match.index);
@@ -7,6 +7,7 @@ export const downloadManifest = async (manifestPath) => {
7
7
  }
8
8
  const manifest = await resp.json();
9
9
  if ("new_install_skip_erase" in manifest) {
10
+ // eslint-disable-next-line no-console
10
11
  console.warn('Manifest option "new_install_skip_erase" is deprecated. Use "new_install_prompt_erase" instead.');
11
12
  if (manifest.new_install_skip_erase) {
12
13
  manifest.new_install_prompt_erase = true;
@@ -7,6 +7,9 @@
7
7
  // [HH:MM:SS.mmm] wall-clock bracket with millis
8
8
  // HH:MM:SS.mmm plain wall-clock
9
9
  const DEVICE_TIMESTAMP_RE = /^\s*(?:\[\d{2}:\d{2}:\d{2}(?:\.\d+)?\]|(?:\d{2}:){2}\d{2}\.\d)/;
10
+ // Matches leading ANSI SGR (color/style) codes at the start of a string
11
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences
12
+ const LEADING_ANSI_RE = /^(\x1b\[(?:\d+;)*\d*m)+/;
10
13
  export class TimestampTransformer {
11
14
  constructor() {
12
15
  this.deviceHasTimestamps = false;
@@ -25,11 +28,30 @@ export class TimestampTransformer {
25
28
  controller.enqueue(chunk);
26
29
  return;
27
30
  }
31
+ // Extract leading ANSI codes to preserve them across line splits
32
+ const ansiMatch = chunk.match(LEADING_ANSI_RE);
33
+ const leadingAnsi = ansiMatch ? ansiMatch[0] : "";
34
+ const contentWithoutAnsi = leadingAnsi
35
+ ? chunk.slice(leadingAnsi.length)
36
+ : chunk;
28
37
  const date = new Date();
29
38
  const h = date.getHours().toString().padStart(2, "0");
30
39
  const m = date.getMinutes().toString().padStart(2, "0");
31
40
  const s = date.getSeconds().toString().padStart(2, "0");
32
- controller.enqueue(`[${h}:${m}:${s}] ${chunk}`);
41
+ const timestamp = `[${h}:${m}:${s}]`;
42
+ // For multi-line chunks, we need to preserve ANSI codes on each line
43
+ // Split on newlines, but keep the newline characters
44
+ const lines = contentWithoutAnsi.split(/(\r?\n)/);
45
+ let result = "";
46
+ for (const part of lines) {
47
+ if (part === "\n" || part === "\r\n") {
48
+ result += part;
49
+ }
50
+ else if (part !== "") {
51
+ result += leadingAnsi + timestamp + " " + part;
52
+ }
53
+ }
54
+ controller.enqueue(result);
33
55
  }
34
56
  reset() {
35
57
  this.deviceHasTimestamps = false;
@@ -1,4 +1,4 @@
1
- import{y as e,z as t,k as o,_ as l,t as r,b as i,h as a}from"./styles-D69gtq6_.js";const n=e`
1
+ import{y as e,z as t,k as o,_ as l,t as r,b as i,h as a}from"./styles-D4zXLz1s.js";const n=e`
2
2
  <svg
3
3
  version="1.1"
4
4
  id="Capa_1"