pinokiod 3.193.0 → 3.195.0

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": "pinokiod",
3
- "version": "3.193.0",
3
+ "version": "3.195.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1163,20 +1163,216 @@ document.addEventListener("DOMContentLoaded", async () => {
1163
1163
  dropOverlay.className = "terminal-drop-overlay"
1164
1164
  dropOverlay.textContent = "Drop files to upload"
1165
1165
  terminalContainer.appendChild(dropOverlay)
1166
- const filterUniqueResources = (urls) => {
1166
+ const dedupeClipboardFiles = (inputs) => {
1167
1167
  const seen = new Set()
1168
- return (Array.isArray(urls) ? urls : []).filter((item) => {
1169
- if (!item || typeof item.href !== "string") {
1170
- return false
1168
+ const results = []
1169
+ inputs.forEach((file) => {
1170
+ if (!file || !(file instanceof File) || !(file.size > 0)) {
1171
+ return
1171
1172
  }
1172
- const key = item.href.trim()
1173
- if (!key || seen.has(key)) {
1174
- return false
1173
+ const signature = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
1174
+ if (seen.has(signature)) {
1175
+ return
1176
+ }
1177
+ seen.add(signature)
1178
+ results.push(file)
1179
+ })
1180
+ return results
1181
+ }
1182
+ const collectClipboardFiles = (clipboardData) => {
1183
+ const files = []
1184
+ const seen = new Set()
1185
+ if (!clipboardData) {
1186
+ return { files, hasFileFlavor: false }
1187
+ }
1188
+ let hasFileFlavor = false
1189
+ try {
1190
+ const types = Array.from(clipboardData.types || [])
1191
+ hasFileFlavor = types.some((type) => type === "Files" || type === "application/x-moz-file")
1192
+ } catch (_) {}
1193
+ const pushIfUnique = (file) => {
1194
+ if (!file || !(file instanceof File) || !(file.size > 0)) {
1195
+ return
1196
+ }
1197
+ const key = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
1198
+ if (seen.has(key)) {
1199
+ return
1200
+ }
1201
+ seen.add(key)
1202
+ files.push(file)
1203
+ }
1204
+ try {
1205
+ Array.from(clipboardData.files || []).forEach(pushIfUnique)
1206
+ } catch (_) {}
1207
+ try {
1208
+ const items = clipboardData.items ? Array.from(clipboardData.items) : []
1209
+ items.forEach((item) => {
1210
+ if (!item || item.kind !== "file" || typeof item.getAsFile !== "function") {
1211
+ return
1212
+ }
1213
+ try {
1214
+ pushIfUnique(item.getAsFile())
1215
+ } catch (_) {}
1216
+ })
1217
+ } catch (_) {}
1218
+ return { files, hasFileFlavor }
1219
+ }
1220
+ const readClipboardFilesFallback = async () => {
1221
+ if (!navigator.clipboard || typeof navigator.clipboard.read !== "function") {
1222
+ return []
1223
+ }
1224
+ try {
1225
+ const clipboardItems = await navigator.clipboard.read()
1226
+ const collected = []
1227
+ const seen = new Set()
1228
+ let index = 0
1229
+ for (const item of clipboardItems) {
1230
+ if (!item || !Array.isArray(item.types)) {
1231
+ index += 1
1232
+ continue
1233
+ }
1234
+ const types = item.types
1235
+ const preferredType = types.find((type) => type && !type.startsWith("text/"))
1236
+ || types.find((type) => /^image\//i.test(type))
1237
+ || types[0]
1238
+ if (!preferredType) {
1239
+ index += 1
1240
+ continue
1241
+ }
1242
+ try {
1243
+ const blob = await item.getType(preferredType)
1244
+ if (!blob) {
1245
+ index += 1
1246
+ continue
1247
+ }
1248
+ let name = typeof item.name === "string" && item.name ? item.name : ""
1249
+ if (!name) {
1250
+ const ext = preferredType && preferredType.includes("/") ? preferredType.split("/").pop() : ""
1251
+ const safeExt = ext ? ext.replace(/[^a-z0-9]/gi, "").toLowerCase() : ""
1252
+ name = `clipboard-${Date.now()}-${index}${safeExt ? `.${safeExt}` : ""}`
1253
+ }
1254
+ const file = blob instanceof File ? blob : new File([blob], name, {
1255
+ type: blob.type || preferredType,
1256
+ lastModified: Date.now()
1257
+ })
1258
+ const key = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
1259
+ if (seen.has(key)) {
1260
+ index += 1
1261
+ continue
1262
+ }
1263
+ seen.add(key)
1264
+ collected.push(file)
1265
+ } catch (error) {
1266
+ if (error && error.name !== "NotAllowedError") {
1267
+ console.warn("Failed to extract clipboard blob", error)
1268
+ }
1269
+ }
1270
+ index += 1
1271
+ }
1272
+ return collected
1273
+ } catch (error) {
1274
+ if (error && error.name !== "NotAllowedError") {
1275
+ console.warn("navigator.clipboard.read() failed", error)
1276
+ }
1277
+ return []
1278
+ }
1279
+ }
1280
+ const extractUrlsFromClipboard = (clipboardData) => {
1281
+ const urls = []
1282
+ if (!clipboardData) {
1283
+ return urls
1284
+ }
1285
+ const seen = new Set()
1286
+ const pushUrl = (href, nameHint) => {
1287
+ if (!href) {
1288
+ return
1289
+ }
1290
+ let resolved
1291
+ try {
1292
+ resolved = new URL(href, window.location.href)
1293
+ } catch (_) {
1294
+ try {
1295
+ resolved = new URL(href)
1296
+ } catch (_) {
1297
+ return
1298
+ }
1299
+ }
1300
+ if (!/^https?:$/i.test(resolved.protocol)) {
1301
+ return
1302
+ }
1303
+ const key = resolved.href
1304
+ if (seen.has(key)) {
1305
+ return
1175
1306
  }
1176
1307
  seen.add(key)
1308
+ urls.push({ href: resolved.href, name: nameHint || null })
1309
+ }
1310
+ let html = ""
1311
+ let uriList = ""
1312
+ if (typeof clipboardData.getData === "function") {
1313
+ try { html = clipboardData.getData("text/html") || "" } catch (_) {}
1314
+ try { uriList = clipboardData.getData("text/uri-list") || "" } catch (_) {}
1315
+ }
1316
+ if (uriList) {
1317
+ uriList.split(/\r?\n/).forEach((line) => {
1318
+ const trimmed = line.trim()
1319
+ if (!trimmed || trimmed.startsWith("#")) {
1320
+ return
1321
+ }
1322
+ pushUrl(trimmed, null)
1323
+ })
1324
+ }
1325
+ if (html) {
1326
+ try {
1327
+ const parser = new DOMParser()
1328
+ const doc = parser.parseFromString(html, "text/html")
1329
+ doc.querySelectorAll("img[src]").forEach((img) => {
1330
+ const src = img.getAttribute("src")
1331
+ const nameHint = img.getAttribute("alt") || img.getAttribute("title") || null
1332
+ pushUrl(src, nameHint)
1333
+ })
1334
+ doc.querySelectorAll("a[href]").forEach((anchor) => {
1335
+ const href = anchor.getAttribute("href")
1336
+ const nameHint = anchor.getAttribute("download") || (anchor.textContent ? anchor.textContent.trim() : null)
1337
+ pushUrl(href, nameHint)
1338
+ })
1339
+ } catch (error) {
1340
+ console.warn("Failed to parse clipboard HTML", error)
1341
+ }
1342
+ }
1343
+ return urls
1344
+ }
1345
+ const isLikelyImagePaste = (clipboardData) => {
1346
+ if (!clipboardData) {
1347
+ return false
1348
+ }
1349
+ const types = Array.from(clipboardData.types || [])
1350
+ if (types.some((type) => type && type.toLowerCase().startsWith("image/"))) {
1177
1351
  return true
1178
- })
1352
+ }
1353
+ if (typeof clipboardData.getData === "function") {
1354
+ try {
1355
+ const html = clipboardData.getData("text/html") || ""
1356
+ if (html && /<img\b/i.test(html)) {
1357
+ return true
1358
+ }
1359
+ } catch (_) {}
1360
+ try {
1361
+ const uriList = clipboardData.getData("text/uri-list") || ""
1362
+ if (uriList && uriList.split(/\r?\n/).some((line) => {
1363
+ const trimmed = line.trim()
1364
+ if (!trimmed || trimmed.startsWith("#")) {
1365
+ return false
1366
+ }
1367
+ return /^https?:\/\//i.test(trimmed)
1368
+ })) {
1369
+ return true
1370
+ }
1371
+ } catch (_) {}
1372
+ }
1373
+ return false
1179
1374
  }
1375
+ let suppressClipboardText = false
1180
1376
  let dragDepth = 0
1181
1377
  const prevent = (event) => {
1182
1378
  event.preventDefault()
@@ -1203,7 +1399,20 @@ document.addEventListener("DOMContentLoaded", async () => {
1203
1399
  let remoteResources = []
1204
1400
  try {
1205
1401
  const extra = await this.collectFilesFromDataTransfer(event.dataTransfer)
1206
- remoteResources = filterUniqueResources(extra && extra.urls)
1402
+ if (extra && Array.isArray(extra.urls) && extra.urls.length) {
1403
+ const seenUrls = new Set()
1404
+ remoteResources = extra.urls.filter((item) => {
1405
+ if (!item || typeof item.href !== "string") {
1406
+ return false
1407
+ }
1408
+ const key = item.href.trim()
1409
+ if (!key || seenUrls.has(key)) {
1410
+ return false
1411
+ }
1412
+ seenUrls.add(key)
1413
+ return true
1414
+ })
1415
+ }
1207
1416
  } catch (error) {
1208
1417
  console.warn("Failed to collect files from drop payload", error)
1209
1418
  }
@@ -1223,35 +1432,41 @@ document.addEventListener("DOMContentLoaded", async () => {
1223
1432
  this.term.focus()
1224
1433
  })
1225
1434
  terminalContainer.addEventListener("paste", async (event) => {
1226
- const clipboard = event.clipboardData || window.clipboardData || null
1227
- if (!clipboard) {
1228
- return
1229
- }
1230
- const files = Array.from(clipboard.files || [])
1231
- let remoteResources = []
1232
- try {
1233
- const extra = await this.collectFilesFromDataTransfer(clipboard)
1234
- remoteResources = filterUniqueResources(extra && extra.urls)
1235
- } catch (error) {
1236
- console.warn("Failed to collect clipboard resources", error)
1435
+ const clipboard = event && event.clipboardData ? event.clipboardData : null
1436
+ const { files: directFiles, hasFileFlavor } = collectClipboardFiles(clipboard)
1437
+ let files = directFiles
1438
+ let remoteUrls = []
1439
+ if (files.length === 0) {
1440
+ remoteUrls = extractUrlsFromClipboard(clipboard)
1441
+ if (remoteUrls.length === 0 && !hasFileFlavor && isLikelyImagePaste(clipboard)) {
1442
+ files = await readClipboardFilesFallback()
1443
+ }
1237
1444
  }
1238
- if (!files.length && (!remoteResources || remoteResources.length === 0)) {
1445
+ console.log('[clipboard paste][shell]', {
1446
+ files: files.map((file) => ({ name: file.name, size: file.size, type: file.type })),
1447
+ remoteUrls,
1448
+ hasFileFlavor,
1449
+ types: clipboard ? Array.from(clipboard.types || []) : []
1450
+ })
1451
+ if (files.length === 0 && remoteUrls.length === 0) {
1452
+ suppressClipboardText = false
1239
1453
  return
1240
1454
  }
1241
1455
  event.preventDefault()
1242
1456
  event.stopPropagation()
1457
+ suppressClipboardText = true
1243
1458
  try {
1244
- if (remoteResources && remoteResources.length > 0) {
1245
- await this.uploadRemoteResources(remoteResources, dropOverlay)
1459
+ if (remoteUrls.length > 0) {
1460
+ await this.uploadRemoteResources(remoteUrls, dropOverlay)
1461
+ }
1462
+ if (files.length > 0) {
1463
+ await this.uploadFiles(files, dropOverlay)
1246
1464
  }
1247
1465
  } catch (error) {
1248
- console.warn("Clipboard remote upload failed", error)
1249
- }
1250
- if (files.length > 0) {
1251
- await this.uploadFiles(files, dropOverlay)
1466
+ console.warn("Clipboard upload failed", error)
1252
1467
  }
1253
1468
  this.term.focus()
1254
- })
1469
+ }, true)
1255
1470
  term.attachCustomKeyEventHandler(event => {
1256
1471
  if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
1257
1472
  const selection = term.getSelection();
@@ -1261,16 +1476,20 @@ document.addEventListener("DOMContentLoaded", async () => {
1261
1476
  }
1262
1477
  }
1263
1478
  if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
1264
- navigator.clipboard.readText().then((text) => {
1265
- this.socket.run({
1266
- //key: "\x1b[200~" + text + "\x1b[201~",
1267
- key: text,
1268
- id: shell_id,
1269
- paste: true
1270
- })
1271
- this.captureTextInput(text)
1479
+ if (!suppressClipboardText) {
1480
+ navigator.clipboard.readText().then((text) => {
1481
+ console.log('[clipboard paste][shell][text]', text)
1482
+ this.socket.run({
1483
+ //key: "\x1b[200~" + text + "\x1b[201~",
1484
+ key: text,
1485
+ id: shell_id,
1486
+ paste: true
1487
+ })
1488
+ this.captureTextInput(text)
1272
1489
 
1273
- })
1490
+ })
1491
+ }
1492
+ suppressClipboardText = false
1274
1493
  return false
1275
1494
  }
1276
1495
  return true;
@@ -767,6 +767,7 @@ document.addEventListener("DOMContentLoaded", async () => {
767
767
  } else if (packet.type === "result") {
768
768
  if (packet.id === "terminal.upload") {
769
769
  const uploaded = Array.isArray(packet.data && packet.data.files) ? packet.data.files : []
770
+ console.log('[terminal.upload][terminal][result]', uploaded)
770
771
  if (uploaded.length > 0) {
771
772
  const mappedFiles = uploaded.map((file) => {
772
773
  const displayPath = file.displayPath || file.homeRelativePath ? `~/${(file.homeRelativePath || '').replace(/^\/+/, '')}` : (file.path || '')
@@ -1256,99 +1257,252 @@ document.addEventListener("DOMContentLoaded", async () => {
1256
1257
  dropOverlay.className = "terminal-drop-overlay"
1257
1258
  dropOverlay.textContent = "Drop files to upload"
1258
1259
  terminalContainer.appendChild(dropOverlay)
1259
- const filterUniqueResources = (urls) => {
1260
+ const dedupeClipboardFiles = (inputs) => {
1260
1261
  const seen = new Set()
1261
- return (Array.isArray(urls) ? urls : []).filter((item) => {
1262
- if (!item || typeof item.href !== "string") {
1263
- return false
1262
+ const results = []
1263
+ inputs.forEach((file) => {
1264
+ if (!file || !(file instanceof File) || !(file.size > 0)) {
1265
+ return
1264
1266
  }
1265
- const key = item.href.trim()
1266
- if (!key || seen.has(key)) {
1267
- return false
1267
+ const signature = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
1268
+ if (seen.has(signature)) {
1269
+ return
1268
1270
  }
1269
- seen.add(key)
1270
- return true
1271
+ seen.add(signature)
1272
+ results.push(file)
1271
1273
  })
1274
+ return results
1272
1275
  }
1273
- let dragDepth = 0
1274
- const prevent = (event) => {
1275
- event.preventDefault()
1276
- event.stopPropagation()
1277
- }
1278
- terminalContainer.addEventListener("dragenter", (event) => {
1279
- prevent(event)
1280
- dragDepth += 1
1281
- dropOverlay.classList.add("active")
1282
- })
1283
- terminalContainer.addEventListener("dragover", prevent)
1284
- terminalContainer.addEventListener("dragleave", (event) => {
1285
- prevent(event)
1286
- dragDepth = Math.max(0, dragDepth - 1)
1287
- if (dragDepth === 0) {
1288
- dropOverlay.classList.remove("active")
1276
+ const collectClipboardFiles = (clipboardData) => {
1277
+ const files = []
1278
+ const seen = new Set()
1279
+ if (!clipboardData) {
1280
+ return { files, hasFileFlavor: false }
1289
1281
  }
1290
- })
1291
- terminalContainer.addEventListener("drop", async (event) => {
1292
- prevent(event)
1293
- dragDepth = 0
1294
- dropOverlay.classList.remove("active")
1295
- const files = Array.from(event.dataTransfer ? event.dataTransfer.files || [] : [])
1296
- let remoteResources = []
1282
+ let hasFileFlavor = false
1297
1283
  try {
1298
- const extra = await this.collectFilesFromDataTransfer(event.dataTransfer)
1299
- remoteResources = filterUniqueResources(extra && extra.urls)
1300
- } catch (error) {
1301
- console.warn("Failed to collect files from drop payload", error)
1284
+ const types = Array.from(clipboardData.types || [])
1285
+ hasFileFlavor = types.some((type) => type === "Files" || type === "application/x-moz-file")
1286
+ } catch (_) {}
1287
+ const pushIfUnique = (file) => {
1288
+ if (!file || !(file instanceof File) || !(file.size > 0)) {
1289
+ return
1290
+ }
1291
+ const key = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
1292
+ if (seen.has(key)) {
1293
+ return
1294
+ }
1295
+ seen.add(key)
1296
+ files.push(file)
1302
1297
  }
1303
- if (!files.length && (!remoteResources || remoteResources.length === 0)) {
1304
- n.Noty({
1305
- text: "Dropped item did not include an accessible file",
1306
- type: "error"
1298
+ try {
1299
+ Array.from(clipboardData.files || []).forEach(pushIfUnique)
1300
+ } catch (_) {}
1301
+ try {
1302
+ const items = clipboardData.items ? Array.from(clipboardData.items) : []
1303
+ items.forEach((item) => {
1304
+ if (!item || item.kind !== "file" || typeof item.getAsFile !== "function") {
1305
+ return
1306
+ }
1307
+ try {
1308
+ pushIfUnique(item.getAsFile())
1309
+ } catch (_) {}
1307
1310
  })
1308
- return
1311
+ } catch (_) {}
1312
+ return { files, hasFileFlavor }
1313
+ }
1314
+ const readClipboardFilesFallback = async () => {
1315
+ if (!navigator.clipboard || typeof navigator.clipboard.read !== "function") {
1316
+ return []
1309
1317
  }
1310
1318
  try {
1311
- if (remoteResources && remoteResources.length > 0) {
1312
- await this.uploadRemoteResources(remoteResources, dropOverlay)
1319
+ const clipboardItems = await navigator.clipboard.read()
1320
+ const collected = []
1321
+ const seen = new Set()
1322
+ let index = 0
1323
+ for (const item of clipboardItems) {
1324
+ if (!item || !Array.isArray(item.types)) {
1325
+ index += 1
1326
+ continue
1327
+ }
1328
+ const types = item.types
1329
+ const preferredType = types.find((type) => type && !type.startsWith("text/"))
1330
+ || types.find((type) => /^image\//i.test(type))
1331
+ || types[0]
1332
+ if (!preferredType) {
1333
+ index += 1
1334
+ continue
1335
+ }
1336
+ try {
1337
+ const blob = await item.getType(preferredType)
1338
+ if (!blob) {
1339
+ index += 1
1340
+ continue
1341
+ }
1342
+ let name = typeof item.name === "string" && item.name ? item.name : ""
1343
+ if (!name) {
1344
+ const ext = preferredType && preferredType.includes("/") ? preferredType.split("/").pop() : ""
1345
+ const safeExt = ext ? ext.replace(/[^a-z0-9]/gi, "").toLowerCase() : ""
1346
+ name = `clipboard-${Date.now()}-${index}${safeExt ? `.${safeExt}` : ""}`
1347
+ }
1348
+ const file = blob instanceof File ? blob : new File([blob], name, {
1349
+ type: blob.type || preferredType,
1350
+ lastModified: Date.now()
1351
+ })
1352
+ const key = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
1353
+ if (seen.has(key)) {
1354
+ index += 1
1355
+ continue
1356
+ }
1357
+ seen.add(key)
1358
+ collected.push(file)
1359
+ } catch (error) {
1360
+ if (error && error.name !== "NotAllowedError") {
1361
+ console.warn("Failed to extract clipboard blob", error)
1362
+ }
1363
+ }
1364
+ index += 1
1313
1365
  }
1366
+ return collected
1314
1367
  } catch (error) {
1315
- console.warn("Remote upload failed", error)
1368
+ if (error && error.name !== "NotAllowedError") {
1369
+ console.warn("navigator.clipboard.read() failed", error)
1370
+ }
1371
+ return []
1316
1372
  }
1317
- if (files.length > 0) {
1318
- await this.uploadFiles(files, dropOverlay)
1373
+ }
1374
+ const extractUrlsFromClipboard = (clipboardData) => {
1375
+ const urls = []
1376
+ if (!clipboardData) {
1377
+ return urls
1319
1378
  }
1320
- this.term.focus()
1321
- })
1322
- terminalContainer.addEventListener("paste", async (event) => {
1323
- const clipboard = event.clipboardData || window.clipboardData || null
1324
- if (!clipboard) {
1325
- return
1379
+ const seen = new Set()
1380
+ const pushUrl = (href, nameHint) => {
1381
+ if (!href) {
1382
+ return
1383
+ }
1384
+ let resolved
1385
+ try {
1386
+ resolved = new URL(href, window.location.href)
1387
+ } catch (_) {
1388
+ try {
1389
+ resolved = new URL(href)
1390
+ } catch (_) {
1391
+ return
1392
+ }
1393
+ }
1394
+ if (!/^https?:$/i.test(resolved.protocol)) {
1395
+ return
1396
+ }
1397
+ const key = resolved.href
1398
+ if (seen.has(key)) {
1399
+ return
1400
+ }
1401
+ seen.add(key)
1402
+ urls.push({ href: resolved.href, name: nameHint || null })
1326
1403
  }
1327
- const files = Array.from(clipboard.files || [])
1328
- let remoteResources = []
1329
- try {
1330
- const extra = await this.collectFilesFromDataTransfer(clipboard)
1331
- remoteResources = filterUniqueResources(extra && extra.urls)
1332
- } catch (error) {
1333
- console.warn("Failed to collect clipboard resources", error)
1404
+ let html = ""
1405
+ let uriList = ""
1406
+ if (typeof clipboardData.getData === "function") {
1407
+ try { html = clipboardData.getData("text/html") || "" } catch (_) {}
1408
+ try { uriList = clipboardData.getData("text/uri-list") || "" } catch (_) {}
1409
+ }
1410
+ if (uriList) {
1411
+ uriList.split(/\r?\n/).forEach((line) => {
1412
+ const trimmed = line.trim()
1413
+ if (!trimmed || trimmed.startsWith("#")) {
1414
+ return
1415
+ }
1416
+ pushUrl(trimmed, null)
1417
+ })
1418
+ }
1419
+ if (html) {
1420
+ try {
1421
+ const parser = new DOMParser()
1422
+ const doc = parser.parseFromString(html, "text/html")
1423
+ doc.querySelectorAll("img[src]").forEach((img) => {
1424
+ const src = img.getAttribute("src")
1425
+ const nameHint = img.getAttribute("alt") || img.getAttribute("title") || null
1426
+ pushUrl(src, nameHint)
1427
+ })
1428
+ doc.querySelectorAll("a[href]").forEach((anchor) => {
1429
+ const href = anchor.getAttribute("href")
1430
+ const nameHint = anchor.getAttribute("download") || (anchor.textContent ? anchor.textContent.trim() : null)
1431
+ pushUrl(href, nameHint)
1432
+ })
1433
+ } catch (error) {
1434
+ console.warn("Failed to parse clipboard HTML", error)
1435
+ }
1436
+ }
1437
+ return urls
1438
+ }
1439
+ const isLikelyImagePaste = (clipboardData) => {
1440
+ if (!clipboardData) {
1441
+ return false
1442
+ }
1443
+ const types = Array.from(clipboardData.types || [])
1444
+ if (types.some((type) => type && type.toLowerCase().startsWith("image/"))) {
1445
+ return true
1446
+ }
1447
+ if (typeof clipboardData.getData === "function") {
1448
+ try {
1449
+ const html = clipboardData.getData("text/html") || ""
1450
+ if (html && /<img\b/i.test(html)) {
1451
+ return true
1452
+ }
1453
+ } catch (_) {}
1454
+ try {
1455
+ const uriList = clipboardData.getData("text/uri-list") || ""
1456
+ if (uriList && uriList.split(/\r?\n/).some((line) => {
1457
+ const trimmed = line.trim()
1458
+ if (!trimmed || trimmed.startsWith("#")) {
1459
+ return false
1460
+ }
1461
+ return /^https?:\/\//i.test(trimmed)
1462
+ })) {
1463
+ return true
1464
+ }
1465
+ } catch (_) {}
1466
+ }
1467
+ return false
1468
+ }
1469
+ let suppressClipboardText = false
1470
+ terminalContainer.addEventListener("paste", async (event) => {
1471
+ const clipboard = event && event.clipboardData ? event.clipboardData : null
1472
+ const { files: directFiles, hasFileFlavor } = collectClipboardFiles(clipboard)
1473
+ let files = directFiles
1474
+ let remoteUrls = []
1475
+ if (files.length === 0) {
1476
+ remoteUrls = extractUrlsFromClipboard(clipboard)
1477
+ if (remoteUrls.length === 0 && !hasFileFlavor && isLikelyImagePaste(clipboard)) {
1478
+ files = await readClipboardFilesFallback()
1479
+ }
1334
1480
  }
1335
- if (!files.length && (!remoteResources || remoteResources.length === 0)) {
1481
+ console.log('[clipboard paste][terminal]', {
1482
+ files: files.map((file) => ({ name: file.name, size: file.size, type: file.type })),
1483
+ remoteUrls,
1484
+ hasFileFlavor,
1485
+ types: clipboard ? Array.from(clipboard.types || []) : []
1486
+ })
1487
+ if (files.length === 0 && remoteUrls.length === 0) {
1488
+ suppressClipboardText = false
1336
1489
  return
1337
1490
  }
1338
1491
  event.preventDefault()
1339
1492
  event.stopPropagation()
1493
+ suppressClipboardText = true
1340
1494
  try {
1341
- if (remoteResources && remoteResources.length > 0) {
1342
- await this.uploadRemoteResources(remoteResources, dropOverlay)
1495
+ if (remoteUrls.length > 0) {
1496
+ await this.uploadRemoteResources(remoteUrls, dropOverlay)
1497
+ }
1498
+ if (files.length > 0) {
1499
+ await this.uploadFiles(files, dropOverlay)
1343
1500
  }
1344
1501
  } catch (error) {
1345
- console.warn("Clipboard remote upload failed", error)
1346
- }
1347
- if (files.length > 0) {
1348
- await this.uploadFiles(files, dropOverlay)
1502
+ console.warn("Clipboard upload failed", error)
1349
1503
  }
1350
1504
  this.term.focus()
1351
- })
1505
+ }, true)
1352
1506
  term.attachCustomKeyEventHandler(event => {
1353
1507
  if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
1354
1508
  const selection = term.getSelection();
@@ -1358,14 +1512,16 @@ document.addEventListener("DOMContentLoaded", async () => {
1358
1512
  }
1359
1513
  }
1360
1514
  if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
1361
- navigator.clipboard.readText().then((text) => {
1362
- this.socket.run({
1363
- //key: "\x1b[200~" + text + "\x1b[201~",
1364
- key: text,
1365
- id: shell_id,
1366
- paste: true
1367
- })
1368
- this.captureTextInput(text)
1515
+ if (!suppressClipboardText) {
1516
+ navigator.clipboard.readText().then((text) => {
1517
+ console.log('[clipboard paste][terminal][text]', text)
1518
+ this.socket.run({
1519
+ //key: "\x1b[200~" + text + "\x1b[201~",
1520
+ key: text,
1521
+ id: shell_id,
1522
+ paste: true
1523
+ })
1524
+ this.captureTextInput(text)
1369
1525
 
1370
1526
 
1371
1527
  // this.socket.run({
@@ -1373,7 +1529,9 @@ document.addEventListener("DOMContentLoaded", async () => {
1373
1529
  // key: text,
1374
1530
  // id: shell_id
1375
1531
  // })
1376
- })
1532
+ })
1533
+ }
1534
+ suppressClipboardText = false
1377
1535
  return false
1378
1536
  }
1379
1537
  return true;