@mindexec/cli 0.2.40 → 0.2.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.40",
3
+ "version": "0.2.42",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/remote-hub.js CHANGED
@@ -9,6 +9,8 @@ const DEFAULT_AGENT_TASK_TIMEOUT_MS = 120000;
9
9
  const MAX_LINE_CHARS = 4 * 1024 * 1024;
10
10
  const MAX_THUMBNAIL_BASE64_CHARS = 3 * 1024 * 1024;
11
11
  const MAX_STREAM_BASE64_CHARS = 3 * 1024 * 1024;
12
+ const MAX_THUMBNAIL_BINARY_BYTES = Math.floor(MAX_THUMBNAIL_BASE64_CHARS * 3 / 4);
13
+ const MAX_STREAM_BINARY_BYTES = Math.floor(MAX_STREAM_BASE64_CHARS * 3 / 4);
12
14
  const MAX_AGENT_TASK_CHARS = 4000;
13
15
  const MAX_AGENT_TASK_RESULT_CHARS = 3000;
14
16
  const RECENT_TASK_LIMIT = 12;
@@ -1075,6 +1077,161 @@ export function createRemoteHub(options = {}) {
1075
1077
  return task;
1076
1078
  }
1077
1079
 
1080
+ function normalizeFrameByteLength(framePayload, frameData = '') {
1081
+ if (Buffer.isBuffer(framePayload)) {
1082
+ return framePayload.length;
1083
+ }
1084
+
1085
+ return Math.floor(String(frameData || '').length * 3 / 4);
1086
+ }
1087
+
1088
+ function buildFrameDataUrl(framePayload, frameData, mimeType) {
1089
+ if (Buffer.isBuffer(framePayload)) {
1090
+ return `data:${mimeType};base64,${framePayload.toString('base64')}`;
1091
+ }
1092
+
1093
+ return String(frameData || '').startsWith('data:')
1094
+ ? String(frameData || '')
1095
+ : `data:${mimeType};base64,${frameData}`;
1096
+ }
1097
+
1098
+ function applyThumbnailFrame(device, message, framePayload, transport = 'json-base64') {
1099
+ const frameData = Buffer.isBuffer(framePayload)
1100
+ ? ''
1101
+ : safeString(framePayload, MAX_THUMBNAIL_BASE64_CHARS + 1);
1102
+ const frameSeq = Number(message.frameSeq);
1103
+ const byteLength = normalizeFrameByteLength(framePayload, frameData);
1104
+ if ((!Buffer.isBuffer(framePayload) && !frameData)
1105
+ || byteLength > MAX_THUMBNAIL_BINARY_BYTES
1106
+ || !Number.isFinite(frameSeq)) {
1107
+ device.counters.thumbnailFramesDropped += 1;
1108
+ emitRemoteEvent('RemoteFrameDropped', device, {
1109
+ reason: 'invalid-thumbnail-frame',
1110
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
1111
+ transport
1112
+ });
1113
+ return false;
1114
+ }
1115
+
1116
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1117
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1118
+ device.latestThumbnail = {
1119
+ streamId: safeString(message.streamId, 128) || 'thumbnail',
1120
+ frameSeq,
1121
+ commandId: safeString(message.commandId, 128),
1122
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1123
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1124
+ mimeType,
1125
+ format: mimeType,
1126
+ capturedAt,
1127
+ receivedAt: device.lastSeenAt,
1128
+ byteLength,
1129
+ transport,
1130
+ dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType)
1131
+ };
1132
+ device.counters.thumbnailFramesReceived += 1;
1133
+ emitRemoteEvent('RemoteFrameReceived', device, {
1134
+ streamId: device.latestThumbnail.streamId,
1135
+ frameSeq,
1136
+ width: device.latestThumbnail.width,
1137
+ height: device.latestThumbnail.height,
1138
+ transport
1139
+ });
1140
+ return true;
1141
+ }
1142
+
1143
+ function applyLiveFrame(device, message, framePayload, transport = 'json-base64') {
1144
+ const frameData = Buffer.isBuffer(framePayload)
1145
+ ? ''
1146
+ : safeString(framePayload, MAX_STREAM_BASE64_CHARS + 1);
1147
+ const frameSeq = Number(message.frameSeq);
1148
+ const streamId = safeString(message.streamId, 128) || 'live';
1149
+ if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
1150
+ device.counters.liveFramesDropped += 1;
1151
+ emitRemoteEvent('RemoteFrameDropped', device, {
1152
+ reason: 'stale-live-stream-frame',
1153
+ streamId,
1154
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
1155
+ transport
1156
+ });
1157
+ return false;
1158
+ }
1159
+
1160
+ const byteLength = normalizeFrameByteLength(framePayload, frameData);
1161
+ if ((!Buffer.isBuffer(framePayload) && !frameData)
1162
+ || byteLength > MAX_STREAM_BINARY_BYTES
1163
+ || !Number.isFinite(frameSeq)) {
1164
+ device.counters.liveFramesDropped += 1;
1165
+ emitRemoteEvent('RemoteFrameDropped', device, {
1166
+ reason: 'invalid-live-frame',
1167
+ streamId,
1168
+ frameSeq: Number.isFinite(frameSeq) ? frameSeq : null,
1169
+ transport
1170
+ });
1171
+ return false;
1172
+ }
1173
+
1174
+ const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1175
+ const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1176
+ device.latestLiveFrame = {
1177
+ streamId,
1178
+ frameSeq,
1179
+ commandId: safeString(message.commandId, 128),
1180
+ width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1181
+ height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1182
+ mimeType,
1183
+ format: mimeType,
1184
+ mode: safeString(message.mode || device.activeLiveStream.mode || 'remote-fast', 80),
1185
+ fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
1186
+ capturedAt,
1187
+ receivedAt: device.lastSeenAt,
1188
+ byteLength,
1189
+ transport,
1190
+ dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType)
1191
+ };
1192
+ device.activeLiveStream.lastFrameAt = device.lastSeenAt;
1193
+ device.activeLiveStream.lastFrameSeq = frameSeq;
1194
+ device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
1195
+ device.counters.liveFramesReceived += 1;
1196
+ emitRemoteEvent('RemoteFrameReceived', device, {
1197
+ streamId,
1198
+ frameSeq,
1199
+ width: device.latestLiveFrame.width,
1200
+ height: device.latestLiveFrame.height,
1201
+ mode: device.latestLiveFrame.mode,
1202
+ transport
1203
+ });
1204
+ return true;
1205
+ }
1206
+
1207
+ function handleAgentBinaryFrame(socket, state, header, framePayload) {
1208
+ if (!state.authenticated || !state.device) {
1209
+ writeJsonLine(socket, { type: 'error', error: 'hello-required' });
1210
+ socket.destroy();
1211
+ return;
1212
+ }
1213
+
1214
+ const device = state.device;
1215
+ device.counters.messagesReceived += 1;
1216
+ device.lastSeenAt = new Date().toISOString();
1217
+
1218
+ const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
1219
+ if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
1220
+ applyThumbnailFrame(device, header, framePayload, 'binary');
1221
+ return;
1222
+ }
1223
+
1224
+ if (frameKind === 'stream' || frameKind === 'live' || header.type === 'stream.binary') {
1225
+ applyLiveFrame(device, header, framePayload, 'binary');
1226
+ return;
1227
+ }
1228
+
1229
+ emitRemoteEvent('RemoteAgentMessageIgnored', device, {
1230
+ messageType: safeString(header.type, 80),
1231
+ frameKind
1232
+ });
1233
+ }
1234
+
1078
1235
  function handleAgentMessage(socket, state, message) {
1079
1236
  if (!message || typeof message !== 'object') {
1080
1237
  return;
@@ -1139,97 +1296,11 @@ export function createRemoteHub(options = {}) {
1139
1296
  });
1140
1297
  break;
1141
1298
  case 'thumbnail.frame': {
1142
- const frameData = safeString(message.data, MAX_THUMBNAIL_BASE64_CHARS + 1);
1143
- const frameSeq = Number(message.frameSeq);
1144
- if (!frameData || frameData.length > MAX_THUMBNAIL_BASE64_CHARS || !Number.isFinite(frameSeq)) {
1145
- device.counters.thumbnailFramesDropped += 1;
1146
- emitRemoteEvent('RemoteFrameDropped', device, {
1147
- reason: 'invalid-thumbnail-frame',
1148
- frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
1149
- });
1150
- break;
1151
- }
1152
-
1153
- const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1154
- const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1155
- device.latestThumbnail = {
1156
- streamId: safeString(message.streamId, 128) || 'thumbnail',
1157
- frameSeq,
1158
- commandId: safeString(message.commandId, 128),
1159
- width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1160
- height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1161
- mimeType,
1162
- format: mimeType,
1163
- capturedAt,
1164
- receivedAt: device.lastSeenAt,
1165
- byteLength: Math.floor(frameData.length * 3 / 4),
1166
- dataUrl: frameData.startsWith('data:')
1167
- ? frameData
1168
- : `data:${mimeType};base64,${frameData}`
1169
- };
1170
- device.counters.thumbnailFramesReceived += 1;
1171
- emitRemoteEvent('RemoteFrameReceived', device, {
1172
- streamId: device.latestThumbnail.streamId,
1173
- frameSeq,
1174
- width: device.latestThumbnail.width,
1175
- height: device.latestThumbnail.height
1176
- });
1299
+ applyThumbnailFrame(device, message, message.data, 'json-base64');
1177
1300
  break;
1178
1301
  }
1179
1302
  case 'stream.frame': {
1180
- const frameData = safeString(message.data, MAX_STREAM_BASE64_CHARS + 1);
1181
- const frameSeq = Number(message.frameSeq);
1182
- const streamId = safeString(message.streamId, 128) || 'live';
1183
- if (!device.activeLiveStream?.active || device.activeLiveStream.streamId !== streamId) {
1184
- device.counters.liveFramesDropped += 1;
1185
- emitRemoteEvent('RemoteFrameDropped', device, {
1186
- reason: 'stale-live-stream-frame',
1187
- streamId,
1188
- frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
1189
- });
1190
- break;
1191
- }
1192
-
1193
- if (!frameData || frameData.length > MAX_STREAM_BASE64_CHARS || !Number.isFinite(frameSeq)) {
1194
- device.counters.liveFramesDropped += 1;
1195
- emitRemoteEvent('RemoteFrameDropped', device, {
1196
- reason: 'invalid-live-frame',
1197
- streamId,
1198
- frameSeq: Number.isFinite(frameSeq) ? frameSeq : null
1199
- });
1200
- break;
1201
- }
1202
-
1203
- const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
1204
- const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
1205
- device.latestLiveFrame = {
1206
- streamId,
1207
- frameSeq,
1208
- commandId: safeString(message.commandId, 128),
1209
- width: Number.isFinite(Number(message.width)) ? Number(message.width) : 0,
1210
- height: Number.isFinite(Number(message.height)) ? Number(message.height) : 0,
1211
- mimeType,
1212
- format: mimeType,
1213
- mode: safeString(message.mode || device.activeLiveStream.mode || 'remote-fast', 80),
1214
- fps: Number.isFinite(Number(message.fps)) ? Number(message.fps) : device.activeLiveStream.fps,
1215
- capturedAt,
1216
- receivedAt: device.lastSeenAt,
1217
- byteLength: Math.floor(frameData.length * 3 / 4),
1218
- dataUrl: frameData.startsWith('data:')
1219
- ? frameData
1220
- : `data:${mimeType};base64,${frameData}`
1221
- };
1222
- device.activeLiveStream.lastFrameAt = device.lastSeenAt;
1223
- device.activeLiveStream.lastFrameSeq = frameSeq;
1224
- device.activeLiveStream.framesReceived = (device.activeLiveStream.framesReceived || 0) + 1;
1225
- device.counters.liveFramesReceived += 1;
1226
- emitRemoteEvent('RemoteFrameReceived', device, {
1227
- streamId,
1228
- frameSeq,
1229
- width: device.latestLiveFrame.width,
1230
- height: device.latestLiveFrame.height,
1231
- mode: device.latestLiveFrame.mode
1232
- });
1303
+ applyLiveFrame(device, message, message.data, 'json-base64');
1233
1304
  break;
1234
1305
  }
1235
1306
  default:
@@ -1242,14 +1313,14 @@ export function createRemoteHub(options = {}) {
1242
1313
 
1243
1314
  function handleSocket(socket) {
1244
1315
  allSockets.add(socket);
1245
- socket.setEncoding('utf8');
1246
1316
  socket.setNoDelay(true);
1247
1317
  socket.setKeepAlive(true, heartbeatMs);
1248
1318
 
1249
1319
  const state = {
1250
1320
  authenticated: false,
1251
1321
  device: null,
1252
- buffer: ''
1322
+ buffer: Buffer.alloc(0),
1323
+ pendingBinaryFrame: null
1253
1324
  };
1254
1325
 
1255
1326
  const helloTimer = setTimeout(() => {
@@ -1260,27 +1331,63 @@ export function createRemoteHub(options = {}) {
1260
1331
  }, 10000);
1261
1332
 
1262
1333
  socket.on('data', chunk => {
1263
- state.buffer += chunk;
1264
- if (state.buffer.length > MAX_LINE_CHARS) {
1265
- writeJsonLine(socket, { type: 'error', error: 'message-too-large' });
1266
- socket.destroy();
1267
- return;
1268
- }
1334
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1335
+ state.buffer = state.buffer.length > 0
1336
+ ? Buffer.concat([state.buffer, incoming])
1337
+ : incoming;
1338
+
1339
+ while (!socket.destroyed) {
1340
+ if (state.pendingBinaryFrame) {
1341
+ const { header, byteLength } = state.pendingBinaryFrame;
1342
+ if (state.buffer.length < byteLength) {
1343
+ return;
1344
+ }
1345
+
1346
+ const framePayload = state.buffer.subarray(0, byteLength);
1347
+ state.buffer = state.buffer.subarray(byteLength);
1348
+ state.pendingBinaryFrame = null;
1349
+ handleAgentBinaryFrame(socket, state, header, framePayload);
1350
+ continue;
1351
+ }
1352
+
1353
+ const newlineIndex = state.buffer.indexOf(0x0a);
1354
+ if (newlineIndex < 0) {
1355
+ if (state.buffer.length > MAX_LINE_CHARS) {
1356
+ writeJsonLine(socket, { type: 'error', error: 'message-too-large' });
1357
+ socket.destroy();
1358
+ }
1359
+ return;
1360
+ }
1269
1361
 
1270
- let newlineIndex = state.buffer.indexOf('\n');
1271
- while (newlineIndex >= 0) {
1272
- const line = state.buffer.slice(0, newlineIndex);
1273
- state.buffer = state.buffer.slice(newlineIndex + 1);
1362
+ const lineBuffer = state.buffer.subarray(0, newlineIndex);
1363
+ state.buffer = state.buffer.subarray(newlineIndex + 1);
1274
1364
 
1275
1365
  try {
1276
- const message = parseJsonLine(line);
1366
+ const message = parseJsonLine(lineBuffer.toString('utf8'));
1367
+ if (message?.type === 'frame.binary') {
1368
+ const frameKind = safeString(message.frameKind || message.kind || message.frameType, 40).toLowerCase();
1369
+ const maxBytes = frameKind === 'thumbnail'
1370
+ ? MAX_THUMBNAIL_BINARY_BYTES
1371
+ : MAX_STREAM_BINARY_BYTES;
1372
+ const byteLength = Number(message.byteLength ?? message.payloadBytes ?? message.dataLength);
1373
+ if (!Number.isFinite(byteLength) || byteLength < 1 || byteLength > maxBytes) {
1374
+ writeJsonLine(socket, { type: 'error', error: 'invalid-binary-frame' });
1375
+ logWarn('remote', 'invalid binary frame header from agent.');
1376
+ continue;
1377
+ }
1378
+
1379
+ state.pendingBinaryFrame = {
1380
+ header: message,
1381
+ byteLength: Math.floor(byteLength)
1382
+ };
1383
+ continue;
1384
+ }
1385
+
1277
1386
  handleAgentMessage(socket, state, message);
1278
1387
  } catch (err) {
1279
1388
  writeJsonLine(socket, { type: 'error', error: 'invalid-json' });
1280
1389
  logWarn('remote', `invalid agent message: ${err?.message || err}`);
1281
1390
  }
1282
-
1283
- newlineIndex = state.buffer.indexOf('\n');
1284
1391
  }
1285
1392
  });
1286
1393
 
@@ -5,10 +5,13 @@ import net from 'node:net';
5
5
  import { spawn } from 'node:child_process';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { readFileSync } from 'node:fs';
8
9
 
9
10
  const BRIDGE_TOKEN = 'remote-http-smoke-token';
10
11
  const PAIR_TOKEN = 'remote-http-pair-token';
11
12
  const SYNTHETIC_COUNT = Number(process.env.REMOTE_HTTP_SMOKE_COUNT || 500);
13
+ const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
14
+ const LOCAL_BRIDGE_PACKAGE = JSON.parse(readFileSync(path.join(LOCAL_BRIDGE_DIR, 'package.json'), 'utf8'));
12
15
 
13
16
  function wait(ms) {
14
17
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -155,7 +158,7 @@ async function runSyntheticEnabledSmoke() {
155
158
  assert.equal(status.started, true);
156
159
  assert.equal(status.port, remoteHubPort);
157
160
  assert.equal(status.managerPackage, '@mindexec/cli');
158
- assert.equal(status.managerVersion, '0.2.25');
161
+ assert.equal(status.managerVersion, LOCAL_BRIDGE_PACKAGE.version);
159
162
  assert.equal(status.agentPackage, '@mindexec/remote');
160
163
  assert.equal(status.canvasPagination, 'none');
161
164
  assert.equal(status.canvasDeviceListMode, 'all-devices');
@@ -8,6 +8,16 @@ function writeJsonLine(socket, payload) {
8
8
  socket.write(`${JSON.stringify(payload)}\n`);
9
9
  }
10
10
 
11
+ function writeBinaryFrame(socket, header, payload) {
12
+ const framePayload = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
13
+ socket.write(`${JSON.stringify({
14
+ ...header,
15
+ type: 'frame.binary',
16
+ byteLength: framePayload.length
17
+ })}\n`);
18
+ socket.write(framePayload);
19
+ }
20
+
11
21
  function wait(ms) {
12
22
  return new Promise(resolve => setTimeout(resolve, ms));
13
23
  }
@@ -34,6 +44,7 @@ const hub = createRemoteHub({
34
44
  REMOTE_HUB_TASK_TIMEOUT_MS: '120'
35
45
  }
36
46
  });
47
+ const smokePngFrame = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', 'base64');
37
48
 
38
49
  try {
39
50
  await hub.start();
@@ -115,6 +126,33 @@ try {
115
126
  assert.equal(thumbnailDevice.latestThumbnail.streamId, 'smoke-thumb');
116
127
  assert.equal(thumbnailDevice.counters.thumbnailFramesReceived, 1);
117
128
 
129
+ const binaryThumbnailCommand = hub.requestThumbnail('smoke-device', {
130
+ streamId: 'smoke-thumb-binary',
131
+ maxWidth: 320,
132
+ maxHeight: 180,
133
+ quality: 50
134
+ });
135
+ assert.equal(binaryThumbnailCommand.ok, true);
136
+ writeBinaryFrame(socket, {
137
+ frameKind: 'thumbnail',
138
+ commandId: binaryThumbnailCommand.commandId,
139
+ streamId: 'smoke-thumb-binary',
140
+ frameSeq: 4,
141
+ width: 2,
142
+ height: 1,
143
+ mimeType: 'image/png',
144
+ capturedAt: new Date().toISOString()
145
+ }, smokePngFrame);
146
+
147
+ const binaryThumbnailDevice = await waitFor(() => {
148
+ const current = hub.listDevices();
149
+ return current[0]?.latestThumbnail?.frameSeq === 4 ? current[0] : null;
150
+ });
151
+ assert.equal(binaryThumbnailDevice.latestThumbnail.streamId, 'smoke-thumb-binary');
152
+ assert.equal(binaryThumbnailDevice.latestThumbnail.transport, 'binary');
153
+ assert.equal(binaryThumbnailDevice.latestThumbnail.byteLength, smokePngFrame.length);
154
+ assert.equal(binaryThumbnailDevice.counters.thumbnailFramesReceived, 2);
155
+
118
156
  const liveCommand = hub.startLiveStream('smoke-device', {
119
157
  streamId: 'smoke-live',
120
158
  fps: 5,
@@ -145,6 +183,28 @@ try {
145
183
  assert.equal(liveDevice.latestLiveFrame.mode, 'remote-fast');
146
184
  assert.equal(liveDevice.counters.liveFramesReceived, 1);
147
185
 
186
+ writeBinaryFrame(socket, {
187
+ frameKind: 'stream',
188
+ commandId: liveCommand.commandId,
189
+ streamId: 'smoke-live',
190
+ frameSeq: 5,
191
+ width: 2,
192
+ height: 1,
193
+ mimeType: 'image/png',
194
+ mode: 'remote-fast',
195
+ fps: 5,
196
+ capturedAt: new Date().toISOString()
197
+ }, smokePngFrame);
198
+
199
+ const binaryLiveDevice = await waitFor(() => {
200
+ const current = hub.listDevices();
201
+ return current[0]?.latestLiveFrame?.frameSeq === 5 ? current[0] : null;
202
+ });
203
+ assert.equal(binaryLiveDevice.latestLiveFrame.streamId, 'smoke-live');
204
+ assert.equal(binaryLiveDevice.latestLiveFrame.transport, 'binary');
205
+ assert.equal(binaryLiveDevice.latestLiveFrame.byteLength, smokePngFrame.length);
206
+ assert.equal(binaryLiveDevice.counters.liveFramesReceived, 2);
207
+
148
208
  const staleFrameBefore = liveDevice.counters.liveFramesDropped;
149
209
  writeJsonLine(socket, {
150
210
  type: 'stream.frame',
@@ -2282,10 +2282,16 @@ body.is-panning .mind-map-text-overlay-v2-card.is-passive .mind-map-text-overlay
2282
2282
  }
2283
2283
 
2284
2284
  .template-card__title-block {
2285
+ display: grid;
2286
+ grid-template-columns: auto minmax(0, 1fr);
2287
+ column-gap: 8px;
2288
+ row-gap: 2px;
2289
+ align-items: baseline;
2285
2290
  min-width: 0;
2286
2291
  }
2287
2292
 
2288
2293
  .template-card__eyebrow {
2294
+ grid-column: 1 / -1;
2289
2295
  color: #64748b;
2290
2296
  font-size: 10px;
2291
2297
  font-weight: 800;
@@ -2294,20 +2300,27 @@ body.is-panning .mind-map-text-overlay-v2-card.is-passive .mind-map-text-overlay
2294
2300
  }
2295
2301
 
2296
2302
  .template-card__title {
2297
- margin-top: 3px;
2303
+ min-width: 0;
2304
+ margin-top: 0;
2298
2305
  color: #0f172a;
2299
2306
  font-size: 20px;
2300
2307
  font-weight: 800;
2301
2308
  line-height: 1.15;
2302
- overflow-wrap: anywhere;
2309
+ overflow: hidden;
2310
+ text-overflow: ellipsis;
2311
+ white-space: nowrap;
2303
2312
  }
2304
2313
 
2305
2314
  .template-card__summary {
2306
- margin-top: 5px;
2315
+ min-width: 0;
2316
+ margin-top: 0;
2307
2317
  color: #475569;
2308
2318
  font-size: 12px;
2309
- line-height: 1.45;
2310
- overflow-wrap: anywhere;
2319
+ font-weight: 700;
2320
+ line-height: 1.2;
2321
+ overflow: hidden;
2322
+ text-overflow: ellipsis;
2323
+ white-space: nowrap;
2311
2324
  }
2312
2325
 
2313
2326
  .template-card__prompt {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-LXAJ4vavBiH0gW7sIe3OgKH5osbdIL5IfUaOrJB056A=",
4
+ "hash": "sha256-mHCtlUxUtnRLOtm/6e2rao/b9Nu5+xYIG8Cxx8aFLNI=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -131,7 +131,7 @@
131
131
  "MindExecution.Plugins.Directory.y74f55e8x3.dll": "MindExecution.Plugins.Directory.dll",
132
132
  "MindExecution.Plugins.PlanMaster.wbclikss3p.dll": "MindExecution.Plugins.PlanMaster.dll",
133
133
  "MindExecution.Plugins.YouTube.fwync0c60s.dll": "MindExecution.Plugins.YouTube.dll",
134
- "MindExecution.Shared.eemv6jbvss.dll": "MindExecution.Shared.dll",
134
+ "MindExecution.Shared.araen3jk1e.dll": "MindExecution.Shared.dll",
135
135
  "MindExecution.Web.3x7btjkiy5.dll": "MindExecution.Web.dll",
136
136
  "dotnet.js": "dotnet.js",
137
137
  "dotnet.native.qc8g39g30v.js": "dotnet.native.js",
@@ -282,7 +282,7 @@
282
282
  "MindExecution.Kernel.z56elxihok.dll": "sha256-STATJelRGcW9SDGgsw6YmQi6tkQje52dy4lyT3QU4qs=",
283
283
  "MindExecution.Plugins.Concept.tkxbh2hokn.dll": "sha256-vYcH7WM686ptbay3iIx5k+zChLgpxvgKxpvHGaDv0kY=",
284
284
  "MindExecution.Plugins.PlanMaster.wbclikss3p.dll": "sha256-rBPdSa0fGB1Y73RXymqsLzyGXwfr5BQRFZS8wyAmiSw=",
285
- "MindExecution.Shared.eemv6jbvss.dll": "sha256-2hrK2TD/R6bpVKmTXaHSI1BbfnDRJR7uxXZAaIW1zyM=",
285
+ "MindExecution.Shared.araen3jk1e.dll": "sha256-9sBLG9hGRCI2aJVwzd1tbHfl+LMk3hc9oVp5EubnQdo=",
286
286
  "MindExecution.Web.3x7btjkiy5.dll": "sha256-1yXNI95k4vlq26Y8YlgIi69ufjSYk2OffuR6A864dUQ="
287
287
  },
288
288
  "lazyAssembly": {
@@ -7,8 +7,8 @@
7
7
  <title>MindExec | Run your ideas as AI task graphs</title>
8
8
  <meta name="description" content="MindExec is an AI execution canvas for solo builders, researchers, developers, and creators. Start with free browser tools, then move serious work into saved MindCanvas projects." />
9
9
  <base href="/" />
10
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260610-template-card-v460" />
11
- <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260610-template-card-v460" />
10
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/app.css?v=20260613-template-card-inline-summary-v461" />
11
+ <link rel="stylesheet" href="_content/MindExecution.Shared/css/mind-map-overrides.css?v=20260613-template-card-inline-summary-v461" />
12
12
  <!-- ?쇄뼹??Font Awesome (local) ?쇄뼹??-->
13
13
  <link rel="stylesheet" href="_content/MindExecution.Shared/lib/font-awesome/css/all.min.css" />
14
14
  <!-- ?꿎뼯??-->
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "P5rJ7sPB",
2
+ "version": "aXuUUujO",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -42,7 +42,7 @@
42
42
  "url": "_content/MindExecution.Shared/css/app.css"
43
43
  },
44
44
  {
45
- "hash": "sha256-SwiYCegnTRFedhTd9xEwvhhTJc7L4RbYTrP8KnQ+yjE=",
45
+ "hash": "sha256-BQdCDRJQX7PiT/7AaeT11SXri099O+sl5lQoyjpQ+Q0=",
46
46
  "url": "_content/MindExecution.Shared/css/mind-map-overrides.css"
47
47
  },
48
48
  {
@@ -442,8 +442,8 @@
442
442
  "url": "_framework/MindExecution.Plugins.YouTube.fwync0c60s.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-2hrK2TD/R6bpVKmTXaHSI1BbfnDRJR7uxXZAaIW1zyM=",
446
- "url": "_framework/MindExecution.Shared.eemv6jbvss.dll"
445
+ "hash": "sha256-9sBLG9hGRCI2aJVwzd1tbHfl+LMk3hc9oVp5EubnQdo=",
446
+ "url": "_framework/MindExecution.Shared.araen3jk1e.dll"
447
447
  },
448
448
  {
449
449
  "hash": "sha256-1yXNI95k4vlq26Y8YlgIi69ufjSYk2OffuR6A864dUQ=",
@@ -770,7 +770,7 @@
770
770
  "url": "_framework/Websocket.Client.vapounvmnl.dll"
771
771
  },
772
772
  {
773
- "hash": "sha256-MqNNAhxn9vl7mzZu+B/8gKky8GXVA4nw1FBdOJi3v5w=",
773
+ "hash": "sha256-0+5TBCVcw2eTOUeLr7CCWVwpXjz7zRWuHpJoskMb2jI=",
774
774
  "url": "_framework/blazor.boot.json"
775
775
  },
776
776
  {
@@ -834,7 +834,7 @@
834
834
  "url": "image-manifest.json"
835
835
  },
836
836
  {
837
- "hash": "sha256-bjvQNTq7IFWd435tpUl5xUScla/vHBE+JHSWSDH464c=",
837
+ "hash": "sha256-2DrUTlHi/psz7d4TwdXV/IjMTlDk12gJ0x81ae3fV2U=",
838
838
  "url": "index.html"
839
839
  },
840
840
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: P5rJ7sPB */
1
+ /* Manifest version: aXuUUujO */
2
2
  // Hosted deployments should prefer the network over stale offline caches.
3
3
  // This service worker immediately clears old Blazor offline caches and unregisters itself.
4
4