pinokiod 3.331.0 → 3.332.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.
@@ -836,6 +836,48 @@ class Api {
836
836
  memory.next = null
837
837
  }
838
838
 
839
+ // snapshot minimal memory state before executing this step
840
+ let sanitizedInput = input
841
+ if (sanitizedInput && typeof sanitizedInput === "object") {
842
+ const clone = Array.isArray(sanitizedInput) ? [...sanitizedInput] : { ...sanitizedInput }
843
+ if ("stdout" in clone) delete clone.stdout
844
+ if ("response" in clone) delete clone.response
845
+ sanitizedInput = clone
846
+ }
847
+
848
+ const memoryLog = {
849
+ ts: Date.now(),
850
+ step: i,
851
+ input: sanitizedInput,
852
+ args,
853
+ global: memory.global,
854
+ local: memory.local,
855
+ port
856
+ }
857
+ try {
858
+ this.ondata({
859
+ id,
860
+ type: "memory",
861
+ data: {
862
+ raw: JSON.stringify(memoryLog) + "\r\n"
863
+ },
864
+ rpc: rawrpc,
865
+ rawrpc
866
+ })
867
+ if (i === 0 && request.path) {
868
+ // also emit using the script path id so it is guaranteed to hit disk logs
869
+ this.ondata({
870
+ id: request.path,
871
+ type: "memory",
872
+ data: {
873
+ raw: JSON.stringify(memoryLog) + "\r\n"
874
+ },
875
+ rpc: rawrpc,
876
+ rawrpc
877
+ })
878
+ }
879
+ } catch (_) {}
880
+
839
881
 
840
882
  this.state = memory
841
883
  this.executing = request
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.331.0",
3
+ "version": "3.332.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -124,7 +124,17 @@
124
124
  if (!this.outputEl) return
125
125
  const text = this.outputEl.textContent || ''
126
126
  if (text.length > MAX_VIEWER_CHARS) {
127
+ // Preserve the user's scroll position while trimming large buffers
128
+ const distanceFromBottom = Math.max(
129
+ 0,
130
+ this.outputEl.scrollHeight - this.outputEl.clientHeight - this.outputEl.scrollTop
131
+ )
127
132
  this.outputEl.textContent = text.slice(text.length - MAX_VIEWER_CHARS)
133
+ const nextScrollTop = Math.max(
134
+ 0,
135
+ this.outputEl.scrollHeight - this.outputEl.clientHeight - distanceFromBottom
136
+ )
137
+ this.outputEl.scrollTop = nextScrollTop
128
138
  }
129
139
  }
130
140
  appendChunk(chunk) {
package/server/socket.js CHANGED
@@ -9,6 +9,8 @@ class Socket {
9
9
  constructor(parent) {
10
10
  this.buffer = {}
11
11
  this.old_buffer = {}
12
+ this.rawLog = {}
13
+ this.logMeta = {}
12
14
  this.sessions = {}
13
15
  this.connected = {}
14
16
  this.active_shell = {}
@@ -271,7 +273,7 @@ class Socket {
271
273
  for(let key in this.buffer) {
272
274
  let buf = this.buffer[key]
273
275
  if (this.old_buffer[key] !== buf) {
274
- this.log_buffer(key, buf)
276
+ this.log_buffer(key, buf, this.logMeta[key])
275
277
  } else {
276
278
  // console.log(`State hasn't changed: ${key}`)
277
279
  }
@@ -323,6 +325,9 @@ class Socket {
323
325
  id = e.id
324
326
  }
325
327
 
328
+ const meta = this.extractMeta(e)
329
+ this.logMeta[id] = meta
330
+
326
331
  const subscribers = this.subscriptions.get(id) || new Set();
327
332
  if (subscribers.size > 0) {
328
333
  //const subscribers = this.subscriptions.get(e.id) || new Set();
@@ -343,10 +348,12 @@ class Socket {
343
348
  }
344
349
 
345
350
  if (e.data && e.data.type === "shell.kill") {
346
- this.log_buffer(id, this.buffer[id]).then(() => {
351
+ this.log_buffer(id, this.buffer[id], meta).then(() => {
347
352
  // when shell is killed, reset the buffer
348
353
  delete this.buffer[id]
349
354
  delete this.sessions[id]
355
+ delete this.rawLog[id]
356
+ delete this.logMeta[id]
350
357
  })
351
358
  }
352
359
 
@@ -356,6 +363,14 @@ class Socket {
356
363
  this.sessions[id] = "" + Date.now()
357
364
  }
358
365
  }
366
+ if (e.data && e.data.raw) {
367
+ if (e.type === "memory") {
368
+ console.log("[memory event id]", id, "caller", e.caller)
369
+ }
370
+ const tagged = this.tagLines(meta, e.data.raw)
371
+ this.rawLog[id] = (this.rawLog[id] || "") + (this.rawLog[id] ? "\n" : "") + tagged
372
+ this.log_buffer(id, this.buffer[id], meta)
373
+ }
359
374
  //if (e.data && e.data.raw) this.buffer[id] += e.data.raw
360
375
  if (e.data && e.data.buf) this.buffer[id] = e.data.buf
361
376
 
@@ -474,7 +489,43 @@ class Socket {
474
489
  }
475
490
  }
476
491
 
477
- async log_buffer(key, buf) {
492
+ logTag(meta) {
493
+ if (meta && meta.memory) {
494
+ return `[memory]`
495
+ }
496
+ const source = (meta && meta.source) ? meta.source : 'shell'
497
+ const method = (meta && meta.method) ? meta.method : (source === 'shell' ? 'shell' : '')
498
+ return `[${source}${method ? ' ' + method : ''}]`
499
+ }
500
+
501
+ tagLines(meta, text) {
502
+ const tag = this.logTag(meta)
503
+ return String(text || '').split(/\r?\n/).map((line) => `${tag} ${line}`).join("\n")
504
+ }
505
+
506
+ extractMeta(e) {
507
+ const meta = { source: 'shell', method: 'shell' }
508
+ if (e && e.type === "memory") {
509
+ meta.memory = true
510
+ }
511
+ if (e && e.kernel) {
512
+ meta.source = 'kernel'
513
+ meta.method = 'kernel'
514
+ }
515
+ if (e && (e.rpc || e.rawrpc)) {
516
+ meta.source = 'api'
517
+ const method = (e.rpc && e.rpc.method) || (e.rawrpc && e.rawrpc.method)
518
+ if (method) {
519
+ meta.method = method
520
+ } else {
521
+ meta.method = 'api'
522
+ }
523
+ }
524
+ return meta
525
+ }
526
+
527
+ async log_buffer(key, buf, meta) {
528
+ const resolvedMeta = meta || this.logMeta[key] || { source: 'shell', method: 'shell' }
478
529
 
479
530
  /*
480
531
 
@@ -512,7 +563,10 @@ class Socket {
512
563
  let session = this.sessions[key]
513
564
  //let logpath = path.resolve(cwd, "logs/dev", path.parse(relative).base)
514
565
  let logpath = path.resolve(cwd, "logs/dev", relative)
515
- await Util.log(logpath, buf, session)
566
+ const raw = this.rawLog[key] || ""
567
+ const tagged = buf ? this.tagLines(resolvedMeta, buf) : ""
568
+ const content = [raw, tagged].filter(Boolean).join("\n")
569
+ await Util.log(logpath, content, session)
516
570
  }
517
571
  } else if (relative.startsWith("api")) {
518
572
  // api
@@ -528,7 +582,10 @@ class Socket {
528
582
  cwd = root.root
529
583
  let session = this.sessions[key]
530
584
  let logpath = path.resolve(cwd, "logs/api", ...filepath_chunks)
531
- await Util.log(logpath, buf, session)
585
+ const raw = this.rawLog[key] || ""
586
+ const tagged = buf ? this.tagLines(resolvedMeta, buf) : ""
587
+ const content = [raw, tagged].filter(Boolean).join("\n")
588
+ await Util.log(logpath, content, session)
532
589
  }
533
590
  } else {
534
591
  // Only log SHELL
@@ -553,7 +610,10 @@ class Socket {
553
610
  cwd = root.root
554
611
  let session = this.sessions[key]
555
612
  let logpath = path.resolve(cwd, "logs/shell")
556
- await Util.log(logpath, buf, session)
613
+ const raw = this.rawLog[key] || ""
614
+ const tagged = buf ? this.tagLines(resolvedMeta, buf) : ""
615
+ const content = [raw, tagged].filter(Boolean).join("\n")
616
+ await Util.log(logpath, content, session)
557
617
  }
558
618
  }
559
619
  }
@@ -2070,6 +2070,46 @@ body.dark {
2070
2070
  color: var(--pinokio-modal-subtitle-color);
2071
2071
  }
2072
2072
 
2073
+ .pinokio-modal-tabs {
2074
+ display: inline-flex;
2075
+ gap: 8px;
2076
+ padding: 0 6px;
2077
+ }
2078
+
2079
+ .pinokio-modal-tab {
2080
+ border: none;
2081
+ background: transparent;
2082
+ color: var(--pinokio-modal-subtitle-color);
2083
+ padding: 8px 12px;
2084
+ border-radius: 10px;
2085
+ font-weight: 700;
2086
+ letter-spacing: 0.01em;
2087
+ cursor: pointer;
2088
+ transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease;
2089
+ }
2090
+
2091
+ .pinokio-modal-tab:hover {
2092
+ background: rgba(59, 130, 246, 0.12);
2093
+ color: var(--pinokio-modal-title-color);
2094
+ }
2095
+
2096
+ .pinokio-modal-tab:focus-visible {
2097
+ outline: 2px solid rgba(59, 130, 246, 0.4);
2098
+ outline-offset: 2px;
2099
+ }
2100
+
2101
+ .pinokio-modal-tab:disabled {
2102
+ opacity: 0.55;
2103
+ cursor: not-allowed;
2104
+ transform: none;
2105
+ }
2106
+
2107
+ .pinokio-modal-tab--active {
2108
+ background: rgba(59, 130, 246, 0.16);
2109
+ color: var(--pinokio-modal-title-color);
2110
+ transform: translateY(-1px);
2111
+ }
2112
+
2073
2113
  .pinokio-history-meta {
2074
2114
  display: flex;
2075
2115
  gap: 10px;
@@ -2281,6 +2321,42 @@ body.dark {
2281
2321
  font-style: italic;
2282
2322
  }
2283
2323
 
2324
+ .pinokio-history-inline-head {
2325
+ display: flex;
2326
+ align-items: center;
2327
+ justify-content: space-between;
2328
+ gap: 10px;
2329
+ margin-bottom: 10px;
2330
+ }
2331
+
2332
+ .pinokio-history-inline-actions {
2333
+ display: inline-flex;
2334
+ align-items: center;
2335
+ gap: 8px;
2336
+ }
2337
+
2338
+ .pinokio-history-inline-btn {
2339
+ border: 1px solid rgba(59, 130, 246, 0.35);
2340
+ background: transparent;
2341
+ color: var(--pinokio-modal-title-color);
2342
+ padding: 7px 12px;
2343
+ border-radius: 9px;
2344
+ font-weight: 600;
2345
+ cursor: pointer;
2346
+ transition: background 0.15s ease, border 0.15s ease, transform 0.15s ease;
2347
+ }
2348
+
2349
+ .pinokio-history-inline-btn:hover {
2350
+ background: rgba(59, 130, 246, 0.16);
2351
+ border-color: rgba(59, 130, 246, 0.5);
2352
+ transform: translateY(-1px);
2353
+ }
2354
+
2355
+ .pinokio-history-inline-btn:focus-visible {
2356
+ outline: 2px solid rgba(59, 130, 246, 0.45);
2357
+ outline-offset: 2px;
2358
+ }
2359
+
2284
2360
  .pinokio-fork-name-input {
2285
2361
  display: flex;
2286
2362
  flex-direction: column;
@@ -5642,6 +5718,23 @@ const rerenderMenuSection = (container, html) => {
5642
5718
  return value.split('/').map(encodeURIComponent).join('/')
5643
5719
  }
5644
5720
 
5721
+ const buildHistoryUrl = (param) => {
5722
+ if (!param) {
5723
+ return null
5724
+ }
5725
+ return `/info/git/HEAD/${encodeRepoPath(param)}`
5726
+ }
5727
+
5728
+ const resolveHistoryUrl = (repoParam, repoData, explicitUrl = null) => {
5729
+ let url = explicitUrl || (repoData && repoData.git_history_url) || historyUri || null
5730
+ if (!url && repoParam) {
5731
+ url = buildHistoryUrl(repoParam)
5732
+ } else if (repoParam && typeof url === 'string' && url.startsWith('/info/git/HEAD/')) {
5733
+ url = buildHistoryUrl(repoParam)
5734
+ }
5735
+ return url
5736
+ }
5737
+
5645
5738
  function escapeHtml(value) {
5646
5739
  if (value === null || value === undefined) {
5647
5740
  return ''
@@ -6745,6 +6838,12 @@ const rerenderMenuSection = (container, html) => {
6745
6838
  commitUrl = gitCommitUrl
6746
6839
  }
6747
6840
 
6841
+ let historyUrl = resolveHistoryUrl(
6842
+ repoParam,
6843
+ repoData,
6844
+ diffDataIsObject ? (diffData.git_history_url || null) : null
6845
+ )
6846
+
6748
6847
  const shouldForceRefresh = Boolean(diffDataIsObject && diffData.forceRefresh)
6749
6848
 
6750
6849
  if (!changes || changes.length === 0 || shouldForceRefresh) {
@@ -6770,6 +6869,7 @@ const rerenderMenuSection = (container, html) => {
6770
6869
  }
6771
6870
  repoStatusCache.set(repoParam, updatedRepo)
6772
6871
  repoData = updatedRepo
6872
+ historyUrl = resolveHistoryUrl(repoParam, updatedRepo)
6773
6873
  const listEntry = lastRepoList.find((entry) => entry.repoParam === repoParam)
6774
6874
  if (listEntry) {
6775
6875
  listEntry.changes = changes
@@ -6810,7 +6910,7 @@ const rerenderMenuSection = (container, html) => {
6810
6910
  await showGitHistoryModal({
6811
6911
  repoParam,
6812
6912
  repoName: repoDisplayName,
6813
- historyUrl: repoData && repoData.git_history_url ? repoData.git_history_url : null,
6913
+ historyUrl,
6814
6914
  })
6815
6915
  } catch (error) {
6816
6916
  console.error('Failed to open history for clean repository:', error)
@@ -6843,6 +6943,8 @@ const rerenderMenuSection = (container, html) => {
6843
6943
  </div>
6844
6944
  ` : ''
6845
6945
 
6946
+ const historyTabDisabled = historyUrl ? '' : ' disabled'
6947
+
6846
6948
  const modalHtml = `
6847
6949
  <div class="pinokio-modal-surface pinokio-modal-surface--diff">
6848
6950
  <div class="pinokio-modal-header">
@@ -6852,8 +6954,16 @@ const rerenderMenuSection = (container, html) => {
6852
6954
  <div class="pinokio-modal-subtitle">${changeSummary}</div>
6853
6955
  </div>
6854
6956
  </div>
6957
+ <div class="pinokio-modal-tabs">
6958
+ <button type="button" class="pinokio-modal-tab pinokio-modal-tab--active" data-tab-target="changes">
6959
+ <i class="fa-solid fa-code-compare"></i> Changes
6960
+ </button>
6961
+ <button type="button" class="pinokio-modal-tab"${historyTabDisabled} data-tab-target="history">
6962
+ <i class="fa-solid fa-clock-rotate-left"></i> History
6963
+ </button>
6964
+ </div>
6855
6965
  ${badgeFragments.length ? `<div class="pinokio-history-meta">${badgeFragments.join('')}</div>` : ''}
6856
- <div class="pinokio-modal-body pinokio-modal-body--diff">
6966
+ <div class="pinokio-modal-body pinokio-modal-body--diff" data-tab-panel="changes">
6857
6967
  <div class="pinokio-git-diff-main-container">
6858
6968
  <div class="pinokio-git-diff-file-list-panel">
6859
6969
  ${changes.map((change, index) => `
@@ -6868,6 +6978,9 @@ const rerenderMenuSection = (container, html) => {
6868
6978
  </div>
6869
6979
  </div>
6870
6980
  </div>
6981
+ <div class="pinokio-modal-body pinokio-modal-body--history" data-tab-panel="history" style="display:none">
6982
+ <div class="pinokio-git-diff-empty-state">${historyUrl ? 'Loading history…' : 'History unavailable'}</div>
6983
+ </div>
6871
6984
  ${commitSection}
6872
6985
  </div>
6873
6986
  `
@@ -6903,6 +7016,11 @@ const rerenderMenuSection = (container, html) => {
6903
7016
  if (!container) return
6904
7017
  const fileItems = container.querySelectorAll('.pinokio-git-diff-file-item-row')
6905
7018
  const diffViewer = container.querySelector('.pinokio-git-diff-viewer-panel')
7019
+ const tabButtons = container.querySelectorAll('.pinokio-modal-tab')
7020
+ const tabPanels = container.querySelectorAll('[data-tab-panel]')
7021
+ const historyPanel = container.querySelector('[data-tab-panel="history"]')
7022
+ const commitFooter = container.querySelector('.pinokio-modal-footer--commit')
7023
+ let historyLoaded = false
6906
7024
 
6907
7025
  const saveBtn = container.querySelector('.pinokio-save-version-btn')
6908
7026
  if (saveBtn) {
@@ -6938,6 +7056,108 @@ const rerenderMenuSection = (container, html) => {
6938
7056
  })
6939
7057
  })
6940
7058
 
7059
+ const renderHistoryInline = (historyData) => {
7060
+ if (!historyPanel) {
7061
+ return
7062
+ }
7063
+ const commits = Array.isArray(historyData.log) ? historyData.log : []
7064
+ const remoteName = historyData.remote || ''
7065
+ const currentRef = historyData.ref || 'HEAD'
7066
+ const metaBadges = [`<span class="pinokio-pill"><i class="fa-solid fa-code-branch"></i>${escapeHtml(currentRef)}</span>`]
7067
+ if (remoteName) {
7068
+ metaBadges.push(`<span class="pinokio-pill"><i class="fa-solid fa-cloud-arrow-up"></i>${escapeHtml(remoteName)}</span>`)
7069
+ }
7070
+ const commitMarkup = commits.length
7071
+ ? `<div class="pinokio-git-history-list">${commits.map(commit => createCommitItem(commit)).join('')}</div>`
7072
+ : '<div class="pinokio-history-empty">No commits found</div>'
7073
+
7074
+ historyPanel.innerHTML = `
7075
+ <div class="pinokio-history-inline-head">
7076
+ <div class="pinokio-history-meta">${metaBadges.join('')}</div>
7077
+ <div class="pinokio-history-inline-actions">
7078
+ <button type="button" class="pinokio-history-inline-btn" data-open-full-history>
7079
+ <i class="fa-solid fa-up-right-from-square"></i> Open full history
7080
+ </button>
7081
+ </div>
7082
+ </div>
7083
+ ${commitMarkup}
7084
+ `
7085
+
7086
+ historyPanel.querySelectorAll('.pinokio-git-commit-item').forEach(item => {
7087
+ item.addEventListener('click', async () => {
7088
+ const commitInfo = item.getAttribute('data-commit-info')
7089
+ if (commitInfo) {
7090
+ await showCommitDiffModal(commitInfo)
7091
+ }
7092
+ })
7093
+ })
7094
+
7095
+ const fullHistoryBtn = historyPanel.querySelector('[data-open-full-history]')
7096
+ if (fullHistoryBtn) {
7097
+ if (!historyUrl) {
7098
+ fullHistoryBtn.disabled = true
7099
+ } else {
7100
+ fullHistoryBtn.addEventListener('click', () => {
7101
+ Swal.close()
7102
+ showGitHistoryModal({ repoParam, repoName: repoDisplayName, historyUrl })
7103
+ })
7104
+ }
7105
+ }
7106
+ }
7107
+
7108
+ const loadHistory = async () => {
7109
+ if (historyLoaded || !historyPanel) {
7110
+ return
7111
+ }
7112
+ if (!historyUrl) {
7113
+ historyPanel.innerHTML = '<div class="pinokio-git-diff-empty-state">History unavailable</div>'
7114
+ return
7115
+ }
7116
+ historyPanel.innerHTML = '<div class="pinokio-git-diff-empty-state">Loading history…</div>'
7117
+ try {
7118
+ const response = await fetch(historyUrl)
7119
+ if (!response.ok) {
7120
+ throw new Error(`HTTP ${response.status}`)
7121
+ }
7122
+ const data = await response.json()
7123
+ historyLoaded = true
7124
+ renderHistoryInline(data)
7125
+ } catch (error) {
7126
+ historyLoaded = false
7127
+ console.error('Error loading inline history:', error)
7128
+ historyPanel.innerHTML = '<div class="pinokio-git-diff-empty-state">Unable to load history</div>'
7129
+ }
7130
+ }
7131
+
7132
+ const setActiveTab = (tabName) => {
7133
+ tabButtons.forEach((btn) => {
7134
+ const isActive = btn.getAttribute('data-tab-target') === tabName
7135
+ btn.classList.toggle('pinokio-modal-tab--active', isActive)
7136
+ })
7137
+ tabPanels.forEach((panel) => {
7138
+ const isTarget = panel.getAttribute('data-tab-panel') === tabName
7139
+ panel.style.display = isTarget ? '' : 'none'
7140
+ })
7141
+ if (commitFooter) {
7142
+ commitFooter.style.display = tabName === 'changes' ? '' : 'none'
7143
+ }
7144
+ if (tabName === 'history') {
7145
+ loadHistory()
7146
+ }
7147
+ }
7148
+
7149
+ tabButtons.forEach((btn) => {
7150
+ if (btn.disabled) {
7151
+ return
7152
+ }
7153
+ btn.addEventListener('click', () => {
7154
+ const target = btn.getAttribute('data-tab-target') || 'changes'
7155
+ setActiveTab(target)
7156
+ })
7157
+ })
7158
+
7159
+ setActiveTab('changes')
7160
+
6941
7161
  if (fileItems.length) {
6942
7162
  fileItems[0].click()
6943
7163
  }
@@ -6985,12 +7205,6 @@ const rerenderMenuSection = (container, html) => {
6985
7205
 
6986
7206
  const showGitHistoryModal = async (options = {}) => {
6987
7207
  const opts = options && typeof options === 'object' ? options : {}
6988
- const buildHistoryUrl = (param) => {
6989
- if (!param) {
6990
- return null
6991
- }
6992
- return `/info/git/HEAD/${encodeRepoPath(param)}`
6993
- }
6994
7208
 
6995
7209
  let repoParam = opts.repoParam ?? null
6996
7210
  let repoName = opts.repoName ?? null
@@ -7010,17 +7224,7 @@ const rerenderMenuSection = (container, html) => {
7010
7224
  historyUrl = repoData.git_history_url
7011
7225
  }
7012
7226
 
7013
- const fallbackHistoryUri = readDataAttr(fsStatusEl, 'data-history-uri')
7014
-
7015
- if (!historyUrl) {
7016
- if (repoParam) {
7017
- historyUrl = buildHistoryUrl(repoParam)
7018
- } else if (fallbackHistoryUri) {
7019
- historyUrl = fallbackHistoryUri
7020
- }
7021
- } else if (repoParam && typeof historyUrl === 'string' && historyUrl.startsWith('/info/git/HEAD/')) {
7022
- historyUrl = buildHistoryUrl(repoParam)
7023
- }
7227
+ historyUrl = resolveHistoryUrl(repoParam, repoData, historyUrl)
7024
7228
 
7025
7229
  if (!repoName) {
7026
7230
  if (repoParam) {
@@ -1407,6 +1407,16 @@ document.addEventListener("DOMContentLoaded", async () => {
1407
1407
  }
1408
1408
  return urls
1409
1409
  }
1410
+ const getClipboardPlainText = (clipboardData) => {
1411
+ if (!clipboardData || typeof clipboardData.getData !== "function") {
1412
+ return ""
1413
+ }
1414
+ try {
1415
+ return clipboardData.getData("text/plain") || ""
1416
+ } catch (_) {
1417
+ return ""
1418
+ }
1419
+ }
1410
1420
  const isLikelyImagePaste = (clipboardData) => {
1411
1421
  if (!clipboardData) {
1412
1422
  return false
@@ -1498,6 +1508,8 @@ document.addEventListener("DOMContentLoaded", async () => {
1498
1508
  })
1499
1509
  terminalContainer.addEventListener("paste", async (event) => {
1500
1510
  const clipboard = event && event.clipboardData ? event.clipboardData : null
1511
+ const plainText = getClipboardPlainText(clipboard)
1512
+ const hasPlainText = typeof plainText === "string" && plainText.trim().length > 0
1501
1513
  const { files: directFiles, hasFileFlavor } = collectClipboardFiles(clipboard)
1502
1514
  let files = directFiles
1503
1515
  let remoteUrls = []
@@ -1511,9 +1523,11 @@ document.addEventListener("DOMContentLoaded", async () => {
1511
1523
  files: files.map((file) => ({ name: file.name, size: file.size, type: file.type })),
1512
1524
  remoteUrls,
1513
1525
  hasFileFlavor,
1526
+ hasPlainText,
1514
1527
  types: clipboard ? Array.from(clipboard.types || []) : []
1515
1528
  })
1516
- if (files.length === 0 && remoteUrls.length === 0) {
1529
+ const shouldHandleUpload = files.length > 0 || (remoteUrls.length > 0 && !hasPlainText)
1530
+ if (!shouldHandleUpload) {
1517
1531
  suppressClipboardText = false
1518
1532
  return
1519
1533
  }
@@ -1521,7 +1535,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1521
1535
  event.stopPropagation()
1522
1536
  suppressClipboardText = true
1523
1537
  try {
1524
- if (remoteUrls.length > 0) {
1538
+ if (remoteUrls.length > 0 && !hasPlainText) {
1525
1539
  await this.uploadRemoteResources(remoteUrls, dropOverlay)
1526
1540
  }
1527
1541
  if (files.length > 0) {
@@ -1509,6 +1509,16 @@ document.addEventListener("DOMContentLoaded", async () => {
1509
1509
  }
1510
1510
  return urls
1511
1511
  }
1512
+ const getClipboardPlainText = (clipboardData) => {
1513
+ if (!clipboardData || typeof clipboardData.getData !== "function") {
1514
+ return ""
1515
+ }
1516
+ try {
1517
+ return clipboardData.getData("text/plain") || ""
1518
+ } catch (_) {
1519
+ return ""
1520
+ }
1521
+ }
1512
1522
  const isLikelyImagePaste = (clipboardData) => {
1513
1523
  if (!clipboardData) {
1514
1524
  return false
@@ -1600,6 +1610,8 @@ document.addEventListener("DOMContentLoaded", async () => {
1600
1610
  })
1601
1611
  terminalContainer.addEventListener("paste", async (event) => {
1602
1612
  const clipboard = event && event.clipboardData ? event.clipboardData : null
1613
+ const plainText = getClipboardPlainText(clipboard)
1614
+ const hasPlainText = typeof plainText === "string" && plainText.trim().length > 0
1603
1615
  const { files: directFiles, hasFileFlavor } = collectClipboardFiles(clipboard)
1604
1616
  let files = directFiles
1605
1617
  let remoteUrls = []
@@ -1613,9 +1625,11 @@ document.addEventListener("DOMContentLoaded", async () => {
1613
1625
  files: files.map((file) => ({ name: file.name, size: file.size, type: file.type })),
1614
1626
  remoteUrls,
1615
1627
  hasFileFlavor,
1628
+ hasPlainText,
1616
1629
  types: clipboard ? Array.from(clipboard.types || []) : []
1617
1630
  })
1618
- if (files.length === 0 && remoteUrls.length === 0) {
1631
+ const shouldHandleUpload = files.length > 0 || (remoteUrls.length > 0 && !hasPlainText)
1632
+ if (!shouldHandleUpload) {
1619
1633
  suppressClipboardText = false
1620
1634
  return
1621
1635
  }
@@ -1623,7 +1637,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1623
1637
  event.stopPropagation()
1624
1638
  suppressClipboardText = true
1625
1639
  try {
1626
- if (remoteUrls.length > 0) {
1640
+ if (remoteUrls.length > 0 && !hasPlainText) {
1627
1641
  await this.uploadRemoteResources(remoteUrls, dropOverlay)
1628
1642
  }
1629
1643
  if (files.length > 0) {