xterm-input-panel 1.1.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # xterm-input-panel
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Improve terminal interaction reliability, including InputPanel state persistence and ghostty virtual cursor behavior.
8
+
3
9
  ## 1.1.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xterm-input-panel",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "dependencies": {
@@ -10,7 +10,7 @@ import { iconSend } from './icons.js'
10
10
  export class InputMethodTab extends LitElement {
11
11
  static get properties() {
12
12
  return {
13
- _inputValue: { state: true },
13
+ value: { type: String, attribute: false },
14
14
  }
15
15
  }
16
16
 
@@ -84,15 +84,26 @@ export class InputMethodTab extends LitElement {
84
84
  }
85
85
  `
86
86
 
87
- declare _inputValue: string
87
+ declare value: string
88
88
 
89
89
  constructor() {
90
90
  super()
91
- this._inputValue = ''
91
+ this.value = ''
92
+ }
93
+
94
+ private _emitInputChange() {
95
+ this.dispatchEvent(
96
+ new CustomEvent('input-panel:input-change', {
97
+ detail: { value: this.value },
98
+ bubbles: true,
99
+ composed: true,
100
+ })
101
+ )
92
102
  }
93
103
 
94
104
  private _onInput(e: Event) {
95
- this._inputValue = (e.target as HTMLTextAreaElement).value
105
+ this.value = (e.target as HTMLTextAreaElement).value
106
+ this._emitInputChange()
96
107
  }
97
108
 
98
109
  private _onKeyDown(e: KeyboardEvent) {
@@ -104,7 +115,7 @@ export class InputMethodTab extends LitElement {
104
115
  }
105
116
 
106
117
  private _send() {
107
- const text = this._inputValue.trim()
118
+ const text = this.value.trim()
108
119
  if (!text) return
109
120
 
110
121
  // Dispatch to terminal
@@ -116,7 +127,8 @@ export class InputMethodTab extends LitElement {
116
127
  })
117
128
  )
118
129
 
119
- this._inputValue = ''
130
+ this.value = ''
131
+ this._emitInputChange()
120
132
  }
121
133
 
122
134
  render() {
@@ -124,7 +136,7 @@ export class InputMethodTab extends LitElement {
124
136
  <div class="input-area">
125
137
  <textarea
126
138
  placeholder="Type command and press Ctrl+Enter to send..."
127
- .value=${this._inputValue}
139
+ .value=${this.value}
128
140
  @input=${this._onInput}
129
141
  @keydown=${this._onKeyDown}
130
142
  ></textarea>
@@ -17,10 +17,11 @@ function resetAddonState() {
17
17
 
18
18
  // Remove any <input-panel> elements leaked into body or containers
19
19
  document.querySelectorAll('input-panel').forEach((el) => el.remove())
20
+ localStorage.removeItem('xtermInputPanelState')
20
21
  }
21
22
 
22
23
  /** Create a real xterm Terminal + InputPanelAddon, mount into container */
23
- function setupTerminal(container: HTMLElement) {
24
+ function setupTerminal(container: HTMLElement, opts?: { stateKey?: string }) {
24
25
  const terminal = new Terminal({
25
26
  cols: 80,
26
27
  rows: 10,
@@ -29,7 +30,7 @@ function setupTerminal(container: HTMLElement) {
29
30
  })
30
31
 
31
32
  const inputHandler = fn()
32
- const addon = new InputPanelAddon({ onInput: inputHandler })
33
+ const addon = new InputPanelAddon({ onInput: inputHandler, stateKey: opts?.stateKey })
33
34
  terminal.loadAddon(addon)
34
35
  terminal.open(container)
35
36
 
@@ -108,8 +109,8 @@ export const SingletonMigration: StoryObj = {
108
109
  resetAddonState()
109
110
  const containerA = canvasElement.querySelector('#term-a') as HTMLElement
110
111
  const containerB = canvasElement.querySelector('#term-b') as HTMLElement
111
- const { addon: addonA } = setupTerminal(containerA)
112
- const { addon: addonB } = setupTerminal(containerB)
112
+ const { addon: addonA } = setupTerminal(containerA, { stateKey: 'story-term-a' })
113
+ const { addon: addonB } = setupTerminal(containerB, { stateKey: 'story-term-b' })
113
114
 
114
115
  // Open A — panel should be inside container A
115
116
  addonA.open()
@@ -382,8 +383,8 @@ export const SharedMountTarget: StoryObj = {
382
383
  // Set shared mount target BEFORE creating terminals
383
384
  InputPanelAddon.mountTarget = wrapper
384
385
 
385
- const { addon: addonA } = setupTerminal(containerA)
386
- const { addon: addonB } = setupTerminal(containerB)
386
+ const { addon: addonA } = setupTerminal(containerA, { stateKey: 'switch-term-a' })
387
+ const { addon: addonB } = setupTerminal(containerB, { stateKey: 'switch-term-b' })
387
388
 
388
389
  // Open A — panel should be in the shared wrapper, NOT container A
389
390
  addonA.open()
@@ -411,3 +412,146 @@ export const SharedMountTarget: StoryObj = {
411
412
  InputPanelAddon.mountTarget = null
412
413
  },
413
414
  }
415
+
416
+ /**
417
+ * Persist panel runtime state between close/open cycles:
418
+ * - active tab (input mode)
419
+ * - input draft text in Input tab
420
+ */
421
+ export const PersistPanelSessionState: StoryObj = {
422
+ render: () => html`<div id="term-container" style="width:100%;height:100%;"></div>`,
423
+ play: async ({ canvasElement }) => {
424
+ resetAddonState()
425
+ const container = canvasElement.querySelector('#term-container') as HTMLElement
426
+ const { addon } = setupTerminal(container)
427
+
428
+ addon.open()
429
+ const panel = container.querySelector('input-panel') as HTMLElement & {
430
+ activeTab: string
431
+ updateComplete: Promise<void>
432
+ shadowRoot: ShadowRoot
433
+ }
434
+ await panel.updateComplete
435
+
436
+ const inputTab = panel.querySelector('input-method-tab') as HTMLElement & {
437
+ updateComplete: Promise<void>
438
+ shadowRoot: ShadowRoot
439
+ }
440
+ await inputTab.updateComplete
441
+ const textarea = inputTab.shadowRoot.querySelector('textarea') as HTMLTextAreaElement
442
+ textarea.value = 'echo keep'
443
+ textarea.dispatchEvent(new Event('input', { bubbles: true }))
444
+ await inputTab.updateComplete
445
+
446
+ const keysBtn = panel.shadowRoot.querySelectorAll('.tab-btn')[1] as HTMLButtonElement
447
+ keysBtn.click()
448
+ await panel.updateComplete
449
+ expect(panel.activeTab).toBe('keys')
450
+
451
+ addon.close()
452
+ addon.open()
453
+
454
+ const reopened = container.querySelector('input-panel') as HTMLElement & {
455
+ activeTab: string
456
+ updateComplete: Promise<void>
457
+ }
458
+ await reopened.updateComplete
459
+ expect(reopened.activeTab).toBe('keys')
460
+
461
+ const reopenedInputTab = reopened.querySelector('input-method-tab') as HTMLElement & {
462
+ updateComplete: Promise<void>
463
+ shadowRoot: ShadowRoot
464
+ }
465
+ await reopenedInputTab.updateComplete
466
+ const reopenedTextarea = reopenedInputTab.shadowRoot.querySelector(
467
+ 'textarea'
468
+ ) as HTMLTextAreaElement
469
+ expect(reopenedTextarea.value).toBe('echo keep')
470
+
471
+ addon.close()
472
+ },
473
+ }
474
+
475
+ /**
476
+ * Persist session state across terminal-instance switching:
477
+ * A(input draft + keys) -> B -> A should restore A state.
478
+ */
479
+ export const PersistStateAcrossTerminalSwitch: StoryObj = {
480
+ render: () => html`
481
+ <div style="display:flex;gap:8px;height:100%;">
482
+ <div id="term-a" style="flex:1;"></div>
483
+ <div id="term-b" style="flex:1;"></div>
484
+ </div>
485
+ `,
486
+ play: async ({ canvasElement }) => {
487
+ type PanelEl = HTMLElement & {
488
+ activeTab: string
489
+ updateComplete: Promise<void>
490
+ }
491
+
492
+ resetAddonState()
493
+ const containerA = canvasElement.querySelector('#term-a') as HTMLElement
494
+ const containerB = canvasElement.querySelector('#term-b') as HTMLElement
495
+ const { addon: addonA } = setupTerminal(containerA, { stateKey: 'switch-term-a' })
496
+ const { addon: addonB } = setupTerminal(containerB, { stateKey: 'switch-term-b' })
497
+
498
+ addonA.open()
499
+ let panelA = containerA.querySelector('input-panel') as PanelEl
500
+ expect(panelA).not.toBeNull()
501
+ await panelA.updateComplete
502
+
503
+ const inputTabA = panelA.querySelector('input-method-tab') as HTMLElement & {
504
+ updateComplete: Promise<void>
505
+ shadowRoot: ShadowRoot
506
+ }
507
+ await inputTabA.updateComplete
508
+ const textareaA = inputTabA.shadowRoot.querySelector('textarea') as HTMLTextAreaElement
509
+ textareaA.value = 'echo keep-on-a'
510
+ textareaA.dispatchEvent(new Event('input', { bubbles: true }))
511
+ await inputTabA.updateComplete
512
+
513
+ const keysBtnA = panelA.shadowRoot!.querySelectorAll('.tab-btn')[1] as HTMLButtonElement
514
+ keysBtnA.click()
515
+ await panelA.updateComplete
516
+ expect(panelA.activeTab).toBe('keys')
517
+ const persistedRaw = localStorage.getItem('xtermInputPanelState')
518
+ expect(persistedRaw).not.toBeNull()
519
+ const persisted = JSON.parse(persistedRaw ?? '{}') as {
520
+ sessions?: Record<string, { activeTab?: string; inputDraft?: string }>
521
+ }
522
+ expect(persisted.sessions?.['switch-term-a']?.activeTab).toBe('keys')
523
+ expect(persisted.sessions?.['switch-term-a']?.inputDraft).toBe('echo keep-on-a')
524
+
525
+ addonB.syncFocusLifecycle()
526
+ const panelB = containerB.querySelector('input-panel') as PanelEl
527
+ expect(panelB).not.toBeNull()
528
+ await panelB.updateComplete
529
+ expect(panelB.activeTab).toBe('keys')
530
+ const inputTabB = panelB.querySelector('input-method-tab') as HTMLElement & {
531
+ updateComplete: Promise<void>
532
+ shadowRoot: ShadowRoot
533
+ }
534
+ await inputTabB.updateComplete
535
+ const textareaB = inputTabB.shadowRoot.querySelector('textarea') as HTMLTextAreaElement
536
+ expect(textareaB.value).toBe('')
537
+
538
+ addonA.syncFocusLifecycle()
539
+ panelA = containerA.querySelector('input-panel') as PanelEl
540
+ expect(panelA).not.toBeNull()
541
+ await panelA.updateComplete
542
+ expect(panelA.activeTab).toBe('keys')
543
+
544
+ const reopenedInputTabA = panelA.querySelector('input-method-tab') as HTMLElement & {
545
+ updateComplete: Promise<void>
546
+ shadowRoot: ShadowRoot
547
+ }
548
+ await reopenedInputTabA.updateComplete
549
+ const reopenedTextareaA = reopenedInputTabA.shadowRoot.querySelector(
550
+ 'textarea'
551
+ ) as HTMLTextAreaElement
552
+ expect(reopenedTextareaA.value).toBe('echo keep-on-a')
553
+
554
+ addonA.close()
555
+ addonB.close()
556
+ },
557
+ }
@@ -1,12 +1,13 @@
1
1
  import type { ITerminalAddon, Terminal } from '@xterm/xterm'
2
2
  import { iconKeyboard, iconMousePointer2 } from './icons.js'
3
- import type { InputPanelLayout } from './input-panel.js'
3
+ import type { InputPanelLayout, InputPanelTab } from './input-panel.js'
4
4
  import type { HostPlatform } from './platform.js'
5
5
 
6
6
  const SENSITIVITY = 1.5
7
7
  const EDGE_SCROLL_ZONE = 30
8
8
  const EDGE_SCROLL_INTERVAL = 50
9
9
  const EDGE_SCROLL_OVERSHOOT = 15
10
+ const STATE_STORAGE_KEY = 'xtermInputPanelState'
10
11
 
11
12
  function isTouchDevice(): boolean {
12
13
  return 'ontouchstart' in window || navigator.maxTouchPoints > 0
@@ -25,6 +26,84 @@ export interface InputPanelSettingsPayload {
25
26
  historyLimit: number
26
27
  }
27
28
 
29
+ interface InputPanelSessionState {
30
+ activeTab: InputPanelTab
31
+ inputDraft: string
32
+ }
33
+
34
+ interface InputMethodTabElement extends HTMLElement {
35
+ value: string
36
+ }
37
+
38
+ interface InputPanelElement extends HTMLElement {
39
+ activeTab: InputPanelTab
40
+ }
41
+
42
+ function isInputPanelTab(value: unknown): value is InputPanelTab {
43
+ return (
44
+ value === 'input' ||
45
+ value === 'keys' ||
46
+ value === 'shortcuts' ||
47
+ value === 'trackpad' ||
48
+ value === 'settings'
49
+ )
50
+ }
51
+
52
+ interface InputPanelStateStore {
53
+ lastActiveTab?: InputPanelTab
54
+ sessions?: Record<string, Partial<InputPanelSessionState>>
55
+ }
56
+
57
+ function isRecord(value: unknown): value is Record<string, unknown> {
58
+ return typeof value === 'object' && value !== null
59
+ }
60
+
61
+ function loadPanelStateStore(): InputPanelStateStore {
62
+ try {
63
+ const raw = localStorage.getItem(STATE_STORAGE_KEY)
64
+ if (!raw) return {}
65
+ const parsed = JSON.parse(raw)
66
+ return isRecord(parsed) ? (parsed as InputPanelStateStore) : {}
67
+ } catch {
68
+ return {}
69
+ }
70
+ }
71
+
72
+ function savePanelStateStore(store: InputPanelStateStore): void {
73
+ try {
74
+ localStorage.setItem(STATE_STORAGE_KEY, JSON.stringify(store))
75
+ } catch {
76
+ /* ignore */
77
+ }
78
+ }
79
+
80
+ function loadPanelSessionState(sessionKey: string): InputPanelSessionState | null {
81
+ const store = loadPanelStateStore()
82
+ const sessions = store.sessions
83
+ if (!sessions) return null
84
+ const rawState = sessions[sessionKey]
85
+ if (!isRecord(rawState)) return null
86
+
87
+ const state: InputPanelSessionState = {
88
+ activeTab: 'input',
89
+ inputDraft: '',
90
+ }
91
+
92
+ if (isInputPanelTab(rawState.activeTab)) {
93
+ state.activeTab = rawState.activeTab
94
+ }
95
+ if (typeof rawState.inputDraft === 'string') {
96
+ state.inputDraft = rawState.inputDraft
97
+ }
98
+
99
+ return state
100
+ }
101
+
102
+ function loadLastActiveTab(): InputPanelTab {
103
+ const store = loadPanelStateStore()
104
+ return isInputPanelTab(store.lastActiveTab) ? store.lastActiveTab : 'input'
105
+ }
106
+
28
107
  /**
29
108
  * xterm.js addon that provides full InputPanel integration.
30
109
  *
@@ -56,6 +135,7 @@ export interface InputPanelSettingsPayload {
56
135
  * ```
57
136
  */
58
137
  export class InputPanelAddon implements ITerminalAddon {
138
+ private static _lastActiveTab: InputPanelTab = 'input'
59
139
  // ── Singleton state ──
60
140
 
61
141
  /** The currently active (open) addon instance, or null. */
@@ -301,6 +381,9 @@ export class InputPanelAddon implements ITerminalAddon {
301
381
  private _onSettingsChange: ((settings: InputPanelSettingsPayload) => Promise<void> | void) | null
302
382
  private _platform: HostPlatform
303
383
  private _defaultLayout: InputPanelLayout
384
+ private _panelSessionState: InputPanelSessionState
385
+ private _stateKey: string
386
+ private _hasOwnPersistedState: boolean
304
387
 
305
388
  constructor(opts?: {
306
389
  onInput?: (data: string) => void
@@ -312,6 +395,7 @@ export class InputPanelAddon implements ITerminalAddon {
312
395
  onSettingsChange?: (settings: InputPanelSettingsPayload) => Promise<void> | void
313
396
  platform?: HostPlatform
314
397
  defaultLayout?: InputPanelLayout
398
+ stateKey?: string
315
399
  }) {
316
400
  this._onInput = opts?.onInput ?? (() => {})
317
401
  this._onOpenCb = opts?.onOpen ?? null
@@ -322,6 +406,19 @@ export class InputPanelAddon implements ITerminalAddon {
322
406
  this._onSettingsChange = opts?.onSettingsChange ?? null
323
407
  this._platform = opts?.platform ?? 'common'
324
408
  this._defaultLayout = opts?.defaultLayout ?? 'floating'
409
+ this._stateKey = opts?.stateKey?.trim() ? opts.stateKey : 'default'
410
+ this._hasOwnPersistedState = false
411
+ this._panelSessionState = {
412
+ activeTab: 'input',
413
+ inputDraft: '',
414
+ }
415
+ const persistedState = loadPanelSessionState(this._stateKey)
416
+ if (persistedState) {
417
+ this._panelSessionState = persistedState
418
+ this._hasOwnPersistedState = true
419
+ } else {
420
+ this._panelSessionState.activeTab = InputPanelAddon._lastActiveTab || loadLastActiveTab()
421
+ }
325
422
  }
326
423
 
327
424
  get isOpen(): boolean {
@@ -348,12 +445,52 @@ export class InputPanelAddon implements ITerminalAddon {
348
445
  this._defaultLayout = layout
349
446
  }
350
447
 
448
+ private _persistPanelState(): void {
449
+ InputPanelAddon._lastActiveTab = this._panelSessionState.activeTab
450
+ const store = loadPanelStateStore()
451
+ const nextSessions = {
452
+ ...(store.sessions ?? {}),
453
+ [this._stateKey]: {
454
+ activeTab: this._panelSessionState.activeTab,
455
+ inputDraft: this._panelSessionState.inputDraft,
456
+ },
457
+ }
458
+ savePanelStateStore({
459
+ ...store,
460
+ lastActiveTab: this._panelSessionState.activeTab,
461
+ sessions: nextSessions,
462
+ })
463
+ }
464
+
351
465
  /**
352
466
  * Resolve the mount target for this addon instance.
353
467
  * Priority: static mountTarget > terminal container > document.body
354
468
  */
355
469
  private _getMountTarget(): HTMLElement {
356
- return InputPanelAddon._mountTarget ?? this._terminal?.element?.parentElement ?? document.body
470
+ return InputPanelAddon._mountTarget ?? this._getTerminalHostElement() ?? document.body
471
+ }
472
+
473
+ /**
474
+ * Resolve the visual host element used for overlay UI (cursor/panel).
475
+ *
476
+ * xterm renderer usually exposes `.xterm` as `terminal.element`, while
477
+ * ghostty may expose the mount container itself.
478
+ */
479
+ private _getTerminalHostElement(): HTMLElement | null {
480
+ const termElement = this._terminal?.element
481
+ if (!(termElement instanceof HTMLElement)) return null
482
+ if (termElement.classList.contains('xterm')) {
483
+ return termElement.parentElement instanceof HTMLElement ? termElement.parentElement : termElement
484
+ }
485
+ return termElement
486
+ }
487
+
488
+ private _detectTerminalEngine(): 'xterm' | 'non-xterm' {
489
+ const termElement = this._terminal?.element
490
+ if (termElement instanceof HTMLElement && termElement.classList.contains('xterm')) {
491
+ return 'xterm'
492
+ }
493
+ return 'non-xterm'
357
494
  }
358
495
 
359
496
  activate(terminal: Terminal): void {
@@ -431,13 +568,21 @@ export class InputPanelAddon implements ITerminalAddon {
431
568
 
432
569
  this._suppressKeyboard()
433
570
 
571
+ if (!this._hasOwnPersistedState) {
572
+ const fallbackActiveTab = loadLastActiveTab()
573
+ this._panelSessionState.activeTab = fallbackActiveTab
574
+ InputPanelAddon._lastActiveTab = fallbackActiveTab
575
+ }
576
+
434
577
  // Build the element tree
435
- const panel = document.createElement('input-panel')
578
+ const panel = document.createElement('input-panel') as InputPanelElement
436
579
  panel.setAttribute('layout', this._defaultLayout)
437
580
  this._applyPanelThemeBindings(panel)
581
+ panel.activeTab = this._panelSessionState.activeTab
438
582
 
439
- const inputTab = document.createElement('input-method-tab')
583
+ const inputTab = document.createElement('input-method-tab') as InputMethodTabElement
440
584
  inputTab.setAttribute('slot', 'input')
585
+ inputTab.value = this._panelSessionState.inputDraft
441
586
  panel.appendChild(inputTab)
442
587
 
443
588
  const keysTab = document.createElement('virtual-keyboard-tab')
@@ -498,8 +643,21 @@ export class InputPanelAddon implements ITerminalAddon {
498
643
  }
499
644
  }
500
645
  })
646
+ this._on(inputTab, 'input-panel:input-change', (e) => {
647
+ const value = (e as CustomEvent).detail?.value
648
+ if (typeof value === 'string') {
649
+ this._panelSessionState.inputDraft = value
650
+ this._hasOwnPersistedState = true
651
+ this._persistPanelState()
652
+ }
653
+ })
501
654
  this._on(panel, 'input-panel:tab-change', (e) => {
502
655
  const tab = (e as CustomEvent).detail?.tab
656
+ if (isInputPanelTab(tab)) {
657
+ this._panelSessionState.activeTab = tab
658
+ this._hasOwnPersistedState = true
659
+ this._persistPanelState()
660
+ }
503
661
  if (tab === 'trackpad') this._showCursor()
504
662
  else this._hideCursor()
505
663
  })
@@ -590,7 +748,7 @@ export class InputPanelAddon implements ITerminalAddon {
590
748
  }
591
749
 
592
750
  private _applyPanelThemeBindings(panel: HTMLElement): void {
593
- const scope = this._terminal?.element?.parentElement ?? this._getMountTarget()
751
+ const scope = this._getTerminalHostElement() ?? this._getMountTarget()
594
752
  const style = getComputedStyle(scope)
595
753
 
596
754
  const background = this._readThemeVar(
@@ -667,6 +825,27 @@ export class InputPanelAddon implements ITerminalAddon {
667
825
  this._isOpen ? this.close() : this.open()
668
826
  }
669
827
 
828
+ /**
829
+ * Sync addon singleton state when host marks this terminal as active.
830
+ *
831
+ * Lifecycle:
832
+ * 1) Always refresh `_lastFocused` so FAB targets the current terminal.
833
+ * 2) If another terminal owns an open panel, migrate panel ownership here.
834
+ * 3) If this panel is already open, keep terminal focus in sync.
835
+ */
836
+ syncFocusLifecycle(): void {
837
+ InputPanelAddon._lastFocused = this
838
+
839
+ if (InputPanelAddon._active && InputPanelAddon._active !== this) {
840
+ this.open()
841
+ return
842
+ }
843
+
844
+ if (this._isOpen) {
845
+ this._focusTerminal()
846
+ }
847
+ }
848
+
670
849
  // ── Terminal focus ──
671
850
 
672
851
  private _focusTerminal(): void {
@@ -681,10 +860,13 @@ export class InputPanelAddon implements ITerminalAddon {
681
860
  // ── Cursor overlay ──
682
861
 
683
862
  private _createCursor(): void {
684
- const container = this._terminal?.element?.parentElement
863
+ const container = this._getTerminalHostElement()
685
864
  if (!container) return
686
865
 
687
866
  const el = document.createElement('div')
867
+ el.setAttribute('data-input-panel-cursor', 'virtual-mouse')
868
+ el.setAttribute('data-terminal-engine', this._detectTerminalEngine())
869
+ el.setAttribute('aria-hidden', 'true')
688
870
  el.style.cssText =
689
871
  'position:absolute;z-index:10;pointer-events:none;opacity:0;transition:opacity 0.15s;color:#fff;'
690
872
  const pointer = iconMousePointer2(20)
@@ -707,7 +889,7 @@ export class InputPanelAddon implements ITerminalAddon {
707
889
  private _showCursor(): void {
708
890
  if (!this._cursorEl) this._createCursor()
709
891
  if (this._cursorEl) {
710
- const container = this._terminal?.element?.parentElement
892
+ const container = this._getTerminalHostElement()
711
893
  if (container) {
712
894
  const rect = container.getBoundingClientRect()
713
895
  this._cursorPos = { x: rect.width / 2, y: rect.height / 2 }
@@ -722,7 +904,7 @@ export class InputPanelAddon implements ITerminalAddon {
722
904
  }
723
905
 
724
906
  private _moveCursor(dx: number, dy: number): void {
725
- const container = this._terminal?.element?.parentElement
907
+ const container = this._getTerminalHostElement()
726
908
  if (!container) return
727
909
  const rect = container.getBoundingClientRect()
728
910
  this._cursorPos.x = Math.max(0, Math.min(rect.width, this._cursorPos.x + dx * SENSITIVITY))
@@ -733,7 +915,7 @@ export class InputPanelAddon implements ITerminalAddon {
733
915
  // ── Mouse event dispatch ──
734
916
 
735
917
  private _getClientCoords(): { clientX: number; clientY: number } | null {
736
- const container = this._terminal?.element?.parentElement
918
+ const container = this._getTerminalHostElement()
737
919
  if (!container) return null
738
920
  const rect = container.getBoundingClientRect()
739
921
  return {
@@ -743,7 +925,7 @@ export class InputPanelAddon implements ITerminalAddon {
743
925
  }
744
926
 
745
927
  private _resolveTarget(clientX: number, clientY: number): Element {
746
- const container = this._terminal?.element?.parentElement
928
+ const container = this._getTerminalHostElement()
747
929
  if (!container) return document.body
748
930
  const el = document.elementFromPoint(clientX, clientY)
749
931
  if (el && container.contains(el)) return el
@@ -814,7 +996,7 @@ export class InputPanelAddon implements ITerminalAddon {
814
996
  // ── Edge scroll (during drag selection) ──
815
997
 
816
998
  private _updateEdgeScroll(): void {
817
- const container = this._terminal?.element?.parentElement
999
+ const container = this._getTerminalHostElement()
818
1000
  if (!container || !this._isDragging) {
819
1001
  this._stopEdgeScroll()
820
1002
  return
@@ -829,7 +1011,7 @@ export class InputPanelAddon implements ITerminalAddon {
829
1011
 
830
1012
  this._stopEdgeScroll()
831
1013
  this._edgeScrollTimer = setInterval(() => {
832
- const container = this._terminal?.element?.parentElement
1014
+ const container = this._getTerminalHostElement()
833
1015
  if (!container || !this._isDragging) {
834
1016
  this._stopEdgeScroll()
835
1017
  return
@@ -914,7 +1096,7 @@ export class InputPanelAddon implements ITerminalAddon {
914
1096
  button.style.border = '1px solid transparent'
915
1097
  button.style.borderRadius = '4px'
916
1098
  button.style.background = 'transparent'
917
- button.style.color = 'var(--muted-foreground, #888)'
1099
+ button.style.color = 'var(--foreground, #fff)'
918
1100
  button.style.fontFamily = 'inherit'
919
1101
  button.style.fontSize = '12px'
920
1102
  button.style.textAlign = 'left'