kiro-mobile-bridge 1.0.5 → 1.0.6

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": "kiro-mobile-bridge",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "A simple mobile web interface for monitoring Kiro IDE agent sessions from your phone over LAN",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -59,11 +59,11 @@
59
59
  .editor-search-close { color: #888; font-size: 16px; cursor: pointer; padding: 4px; border-radius: 4px; }
60
60
  .editor-search-close:hover { color: #fff; background: rgba(255,255,255,0.1); }
61
61
  .editor-content { flex: 1; overflow: auto; -webkit-overflow-scrolling: touch; background: #1e1e1e; }
62
- .editor-code { margin: 0; padding: 8px 0; font-family: 'Cascadia Code', 'Fira Code', Menlo, Monaco, "Courier New", monospace; font-size: 12px; line-height: 1.6; color: #d4d4d4; white-space: pre; overflow-x: auto; background: #1e1e1e; }
62
+ .editor-code { margin: 0; padding: 8px 0; font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'SF Mono', Consolas, 'Liberation Mono', Menlo, Monaco, 'Courier New', monospace; font-size: 14px; line-height: 1.6; color: #d4d4d4; background: #1e1e1e; }
63
63
  .editor-line { display: flex; min-height: 1.6em; }
64
64
  .editor-line:hover { background: rgba(255,255,255,0.04); }
65
- .editor-line-num { width: 45px; min-width: 45px; text-align: right; padding-right: 12px; color: #6e7681; user-select: none; flex-shrink: 0; font-size: 12px; }
66
- .editor-line-code { flex: 1; padding-right: 12px; white-space: pre; tab-size: 2; }
65
+ .editor-line-num { width: 50px; min-width: 50px; text-align: right; padding-right: 16px; color: #858585; user-select: none; flex-shrink: 0; font-size: 14px; font-family: inherit; }
66
+ .editor-line-code { flex: 1; padding-right: 12px; white-space: pre; overflow-x: auto; tab-size: 2; font-family: inherit; }
67
67
  .search-highlight { background: #ffd500; color: #000; border-radius: 2px; padding: 0 1px; }
68
68
  .search-highlight.current { background: #ff6b00; color: #fff; outline: 2px solid #ff6b00; }
69
69
 
@@ -90,14 +90,29 @@
90
90
  .file-tree-empty { padding: 20px; text-align: center; color: #666; font-size: 13px; }
91
91
  .file-tree-loading { padding: 20px; text-align: center; color: #888; display: flex; flex-direction: column; align-items: center; gap: 8px; }
92
92
 
93
- /* Syntax Highlighting */
94
- .tok-kw { color: #569cd6; }
95
- .tok-str { color: #ce9178; }
96
- .tok-num { color: #b5cea8; }
97
- .tok-cmt { color: #6a9955; }
98
- .tok-fn { color: #dcdcaa; }
99
- .tok-type { color: #4ec9b0; }
100
-
93
+ /* Syntax Highlighting - Kiro Dark Theme */
94
+ .token-keyword { color: #c586c0; font-weight: normal; }
95
+ .token-string { color: #ce9178; }
96
+ .token-number { color: #b5cea8; }
97
+ .token-comment { color: #6a9955; font-style: italic; }
98
+ .token-function { color: #dcdcaa; }
99
+ .token-type { color: #4ec9b0; }
100
+ .token-operator { color: #d4d4d4; }
101
+ .token-property { color: #9cdcfe; }
102
+ .token-variable { color: #9cdcfe; }
103
+ .token-constant { color: #4fc1ff; }
104
+ .token-class { color: #4ec9b0; }
105
+ .token-punctuation { color: #d4d4d4; }
106
+ .token-tag { color: #569cd6; }
107
+ .token-attr-name { color: #9cdcfe; }
108
+ .token-attr-value { color: #ce9178; }
109
+ /* Kiro theme token classes */
110
+ .tok-kw { color: #c586c0; } /* Keywords - purple/pink */
111
+ .tok-str { color: #ce9178; } /* Strings - orange */
112
+ .tok-num { color: #b5cea8; } /* Numbers - light green */
113
+ .tok-cmt { color: #6a9955; font-style: italic; } /* Comments - green italic */
114
+ .tok-fn { color: #dcdcaa; } /* Functions - yellow */
115
+ .tok-type { color: #4ec9b0; } /* Types - teal */
101
116
  /* Syntax Highlighting */
102
117
  .token-keyword { color: #569cd6; font-weight: 500; }
103
118
  .token-string { color: #ce9178; }
@@ -540,6 +555,9 @@
540
555
 
541
556
  hideLoading('chat');
542
557
 
558
+ // Remove placeholder text that overlaps with input
559
+ removePlaceholderText();
560
+
543
561
  // Find the new inner scrollable element and scroll it
544
562
  requestAnimationFrame(() => {
545
563
  requestAnimationFrame(() => {
@@ -625,12 +643,10 @@
625
643
  const displayLines = lines.slice(startIdx);
626
644
  displayLines.forEach((line, idx) => {
627
645
  const lineNum = startLineNum + startIdx + idx;
628
- const highlighted = highlightSyntax(line, data.language);
629
- // Preserve whitespace - convert tabs and spaces
630
- let preservedLine = highlighted
631
- .replace(/\t/g, ' ')
632
- .replace(/^ +/, match => ' '.repeat(match.length));
633
- html += `<div class="editor-line"><span class="editor-line-num">${lineNum}</span><span class="editor-line-code">${preservedLine || '&nbsp;'}</span></div>`;
646
+ // Convert tabs to spaces first, then highlight
647
+ const lineWithSpaces = line.replace(/\t/g, ' ');
648
+ const highlighted = highlightSyntax(lineWithSpaces, data.language);
649
+ html += `<div class="editor-line"><span class="editor-line-num">${lineNum}</span><span class="editor-line-code">${highlighted || ' '}</span></div>`;
634
650
  });
635
651
  html += '</pre>';
636
652
 
@@ -663,46 +679,147 @@
663
679
  }
664
680
  }
665
681
 
666
- // Syntax highlighting (basic)
682
+ // Syntax highlighting - simple and safe approach
667
683
  function highlightSyntax(line, language) {
668
- let escaped = escapeHtml(line);
684
+ // First escape HTML to prevent XSS
685
+ const escaped = escapeHtml(line);
669
686
 
670
- // Skip syntax highlighting for CSS (too complex with # colors)
671
- if (language === 'css' || language === 'html') {
687
+ // Skip highlighting for very long lines or if no language
688
+ if (escaped.length > 1000 || !language) {
672
689
  return escaped;
673
690
  }
674
691
 
675
- // Comments - be careful not to match CSS color codes
676
- // Only match // comments and # comments that start at beginning or after whitespace
677
- escaped = escaped.replace(/(\/\/.*$)/gm, '<span class="token-comment">$1</span>');
678
- escaped = escaped.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="token-comment">$1</span>');
679
- // Python/shell comments - only if # is at start of line or after whitespace, not in middle of word
680
- if (language === 'python' || language === 'bash' || language === 'sh') {
681
- escaped = escaped.replace(/(^|\s)(#.*)$/gm, '$1<span class="token-comment">$2</span>');
692
+ // Language detection
693
+ const isJS = ['javascript', 'typescript', 'jsx', 'tsx', 'js', 'ts'].includes(language);
694
+ const isPython = language === 'python' || language === 'py';
695
+ const isJSON = language === 'json';
696
+ const isYAML = language === 'yaml' || language === 'yml';
697
+ const isShell = ['bash', 'sh', 'shell', 'zsh'].includes(language);
698
+ const isHTML = language === 'html' || language === 'xml';
699
+ const isCSS = language === 'css' || language === 'scss' || language === 'sass' || language === 'less';
700
+ const isMarkdown = language === 'markdown' || language === 'md';
701
+
702
+ // For HTML files, use a simpler approach - just colorize without spans that could break
703
+ if (isHTML) {
704
+ let result = escaped;
705
+ // Comments
706
+ result = result.replace(/(&lt;!--[\s\S]*?--&gt;)/g, '<span class="tok-cmt">$1</span>');
707
+ // Strings in attributes
708
+ result = result.replace(/=(&quot;[^&]*?&quot;)/g, '=<span class="tok-str">$1</span>');
709
+ result = result.replace(/=(&#39;[^&]*?&#39;)/g, '=<span class="tok-str">$1</span>');
710
+ return result;
682
711
  }
683
712
 
684
- // Strings - match quoted strings
685
- escaped = escaped.replace(/(&quot;[^&]*?&quot;)/g, '<span class="token-string">$1</span>');
686
- escaped = escaped.replace(/(&#39;[^&]*?&#39;)/g, '<span class="token-string">$1</span>');
713
+ // For CSS files
714
+ if (isCSS) {
715
+ let result = escaped;
716
+ // Comments
717
+ result = result.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="tok-cmt">$1</span>');
718
+ // Strings
719
+ result = result.replace(/(&quot;[^&]*?&quot;)/g, '<span class="tok-str">$1</span>');
720
+ result = result.replace(/(&#39;[^&]*?&#39;)/g, '<span class="tok-str">$1</span>');
721
+ // Numbers with units
722
+ result = result.replace(/:\s*([0-9]+(?:\.[0-9]+)?(?:px|em|rem|%|vh|vw|s|ms)?)/g, ': <span class="tok-num">$1</span>');
723
+ // Hex colors
724
+ result = result.replace(/(#[0-9a-fA-F]{3,8})\b/g, '<span class="tok-num">$1</span>');
725
+ return result;
726
+ }
687
727
 
688
- // Keywords
689
- const keywords = ['const', 'let', 'var', 'function', 'class', 'return', 'if', 'else', 'for', 'while', 'import', 'export', 'from', 'async', 'await', 'try', 'catch', 'throw', 'new', 'this', 'super', 'extends', 'implements', 'interface', 'type', 'enum', 'public', 'private', 'protected', 'static', 'readonly', 'def', 'self', 'None', 'True', 'False'];
690
- keywords.forEach(kw => {
691
- const regex = new RegExp(`\\b(${kw})\\b`, 'g');
692
- escaped = escaped.replace(regex, '<span class="token-keyword">$1</span>');
693
- });
728
+ // Build result by processing character by character for other languages
729
+ let result = '';
730
+ let i = 0;
694
731
 
695
- // Numbers - but not inside other tokens
696
- escaped = escaped.replace(/\b(\d+\.?\d*)\b/g, '<span class="token-number">$1</span>');
732
+ while (i < escaped.length) {
733
+ // Check for comments
734
+ if (isJS && escaped.slice(i, i + 2) === '//') {
735
+ result += '<span class="tok-cmt">' + escaped.slice(i) + '</span>';
736
+ break;
737
+ }
738
+ if ((isPython || isShell || isYAML) && escaped[i] === '#') {
739
+ result += '<span class="tok-cmt">' + escaped.slice(i) + '</span>';
740
+ break;
741
+ }
742
+
743
+ // Check for strings (escaped quotes: &quot; or &#39;)
744
+ if (escaped.slice(i, i + 6) === '&quot;') {
745
+ const endIdx = escaped.indexOf('&quot;', i + 6);
746
+ if (endIdx !== -1) {
747
+ const str = escaped.slice(i, endIdx + 6);
748
+ result += '<span class="tok-str">' + str + '</span>';
749
+ i = endIdx + 6;
750
+ continue;
751
+ }
752
+ }
753
+ if (escaped.slice(i, i + 5) === '&#39;') {
754
+ const endIdx = escaped.indexOf('&#39;', i + 5);
755
+ if (endIdx !== -1) {
756
+ const str = escaped.slice(i, endIdx + 5);
757
+ result += '<span class="tok-str">' + str + '</span>';
758
+ i = endIdx + 5;
759
+ continue;
760
+ }
761
+ }
762
+
763
+ // Check for numbers
764
+ if (/[0-9]/.test(escaped[i]) && (i === 0 || /[^a-zA-Z_]/.test(escaped[i - 1]))) {
765
+ let numEnd = i;
766
+ while (numEnd < escaped.length && /[0-9.]/.test(escaped[numEnd])) {
767
+ numEnd++;
768
+ }
769
+ if (numEnd > i) {
770
+ result += '<span class="tok-num">' + escaped.slice(i, numEnd) + '</span>';
771
+ i = numEnd;
772
+ continue;
773
+ }
774
+ }
775
+
776
+ // Check for keywords (word boundary)
777
+ if (/[a-zA-Z_]/.test(escaped[i]) && (i === 0 || /[^a-zA-Z0-9_]/.test(escaped[i - 1]))) {
778
+ let wordEnd = i;
779
+ while (wordEnd < escaped.length && /[a-zA-Z0-9_]/.test(escaped[wordEnd])) {
780
+ wordEnd++;
781
+ }
782
+ const word = escaped.slice(i, wordEnd);
783
+
784
+ // Define keywords per language
785
+ let keywords = [];
786
+ if (isJS) {
787
+ keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'import', 'export', 'from', 'async', 'await', 'try', 'catch', 'throw', 'new', 'this', 'class', 'extends', 'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'switch', 'case', 'break', 'continue', 'default', 'static', 'get', 'set'];
788
+ } else if (isPython) {
789
+ keywords = ['def', 'class', 'return', 'if', 'elif', 'else', 'for', 'while', 'import', 'from', 'as', 'try', 'except', 'raise', 'with', 'async', 'await', 'None', 'True', 'False', 'self', 'and', 'or', 'not', 'in', 'is', 'lambda', 'pass', 'break', 'continue'];
790
+ } else if (isJSON) {
791
+ keywords = ['true', 'false', 'null'];
792
+ } else if (isYAML) {
793
+ keywords = ['true', 'false', 'null', 'yes', 'no'];
794
+ } else if (!isMarkdown) {
795
+ keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'import', 'export', 'true', 'false', 'null', 'class', 'public', 'private', 'static', 'void', 'int', 'string', 'bool'];
796
+ }
797
+
798
+ if (keywords.includes(word)) {
799
+ result += '<span class="tok-kw">' + word + '</span>';
800
+ } else {
801
+ result += word;
802
+ }
803
+ i = wordEnd;
804
+ continue;
805
+ }
806
+
807
+ // Default: just add the character
808
+ result += escaped[i];
809
+ i++;
810
+ }
697
811
 
698
- return escaped;
812
+ return result;
699
813
  }
700
814
 
701
815
  // Helper functions
702
816
  function escapeHtml(text) {
703
- const div = document.createElement('div');
704
- div.textContent = text;
705
- return div.innerHTML;
817
+ return text
818
+ .replace(/&/g, '&amp;')
819
+ .replace(/</g, '&lt;')
820
+ .replace(/>/g, '&gt;')
821
+ .replace(/"/g, '&quot;')
822
+ .replace(/'/g, '&#39;');
706
823
  }
707
824
 
708
825
  function showEmptyState(panelName, icon, message) {
@@ -722,6 +839,74 @@
722
839
  if (content) content.classList.toggle('collapsed');
723
840
  }
724
841
 
842
+ // Remove placeholder text that overlaps with input field
843
+ function removePlaceholderText() {
844
+ const content = panels.chat.content;
845
+
846
+ // Method 1: Find elements by class name containing "placeholder"
847
+ content.querySelectorAll('[class*="placeholder"], [class*="Placeholder"]').forEach(el => {
848
+ // Don't remove actual input elements
849
+ if (el.matches('[contenteditable], textarea, input, [data-lexical-editor]')) return;
850
+ if (el.querySelector('[contenteditable], textarea, input, [data-lexical-editor]')) return;
851
+
852
+ // Hide it
853
+ el.style.display = 'none';
854
+ el.style.visibility = 'hidden';
855
+ el.style.opacity = '0';
856
+ });
857
+
858
+ // Method 2: Find elements with data-placeholder attribute
859
+ content.querySelectorAll('[data-placeholder]').forEach(el => {
860
+ // Remove the data-placeholder attribute to prevent CSS ::before content
861
+ if (!el.matches('[contenteditable], [data-lexical-editor]')) {
862
+ el.style.display = 'none';
863
+ }
864
+ });
865
+
866
+ // Method 3: Find any element containing placeholder-like text and hide it
867
+ const placeholderTexts = ['ask a question', 'describe a task', 'type a message', 'enter a message'];
868
+
869
+ const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, null, false);
870
+ const nodesToHide = [];
871
+
872
+ while (walker.nextNode()) {
873
+ const text = (walker.currentNode.textContent || '').toLowerCase();
874
+ for (const placeholder of placeholderTexts) {
875
+ if (text.includes(placeholder)) {
876
+ const parent = walker.currentNode.parentElement;
877
+ if (parent && !parent.matches('[contenteditable], textarea, input, [data-lexical-editor]')) {
878
+ nodesToHide.push(parent);
879
+ }
880
+ break;
881
+ }
882
+ }
883
+ }
884
+
885
+ nodesToHide.forEach(el => {
886
+ el.style.display = 'none';
887
+ el.style.visibility = 'hidden';
888
+ });
889
+
890
+ // Method 4: Find absolutely positioned elements inside input containers and hide them
891
+ // These are often placeholder overlays
892
+ const inputContainers = content.querySelectorAll('[class*="input"], [class*="composer"], [class*="editor"]');
893
+ inputContainers.forEach(container => {
894
+ container.querySelectorAll('*').forEach(el => {
895
+ const style = window.getComputedStyle(el);
896
+ // If it's absolutely positioned and not the input itself, it might be a placeholder
897
+ if (style.position === 'absolute' &&
898
+ !el.matches('[contenteditable], textarea, input, [data-lexical-editor], button, svg')) {
899
+ const text = (el.textContent || '').toLowerCase();
900
+ if (text.includes('ask') || text.includes('task') || text.includes('question') ||
901
+ text.includes('describe') || text.includes('message') || text.includes('type')) {
902
+ el.style.display = 'none';
903
+ el.style.visibility = 'hidden';
904
+ }
905
+ }
906
+ });
907
+ });
908
+ }
909
+
725
910
  // Base styles for chat panel
726
911
  function getBaseStyles() {
727
912
  return `<style>
@@ -759,6 +944,19 @@
759
944
  background-color: #1e1e1e !important;
760
945
  }
761
946
 
947
+ /* ========== HIDE PLACEHOLDER TEXT IN INPUT AREA ========== */
948
+ /* Placeholder removal is handled via JavaScript in removePlaceholderText() */
949
+ /* Keep input elements visible and interactive */
950
+ [contenteditable="true"],
951
+ [data-lexical-editor="true"],
952
+ .ProseMirror,
953
+ textarea {
954
+ display: block !important;
955
+ visibility: visible !important;
956
+ opacity: 1 !important;
957
+ pointer-events: auto !important;
958
+ }
959
+
762
960
  /* CRITICAL: Hide tooltips, popovers, and overlay elements - but NOT dropdown buttons */
763
961
  [role="tooltip"],
764
962
  [data-tooltip],
@@ -809,6 +1007,25 @@
809
1007
  color: #cccccc !important;
810
1008
  }
811
1009
 
1010
+ /* Hide model descriptions in dropdown - only show model name */
1011
+ [class*="model-description"], [class*="modelDescription"], [class*="ModelDescription"],
1012
+ [class*="model-info"], [class*="modelInfo"], [class*="ModelInfo"],
1013
+ [class*="dropdown-item"] > span:not(:first-child),
1014
+ [class*="model-option"] > span:not(:first-child),
1015
+ [role="option"] > div:last-child,
1016
+ [role="option"] > span:last-child:not(:first-child),
1017
+ [role="menuitem"] > div:last-child,
1018
+ [class*="credit"], [class*="Credit"] {
1019
+ display: none !important;
1020
+ }
1021
+
1022
+ /* Simplify model dropdown items - just show the name */
1023
+ [role="option"], [role="menuitem"] {
1024
+ flex-direction: row !important;
1025
+ align-items: center !important;
1026
+ gap: 8px !important;
1027
+ }
1028
+
812
1029
  [class*="dropdown-item"]:hover, [class*="dropdownItem"]:hover,
813
1030
  [role="option"]:hover, [role="menuitem"]:hover {
814
1031
  background: rgba(255, 255, 255, 0.1) !important;
@@ -1219,8 +1436,8 @@
1219
1436
  function makeInteractive() {
1220
1437
  const content = panels.chat.content;
1221
1438
 
1222
- // Input fields
1223
- const inputSelectors = ['[data-lexical-editor="true"]', '[contenteditable="true"][role="textbox"]', '[contenteditable="true"]', 'textarea', '.ProseMirror'];
1439
+ // Input fields - support both Lexical and ProseMirror/TipTap editors
1440
+ const inputSelectors = ['[data-lexical-editor="true"]', '[contenteditable="true"][role="textbox"]', '[contenteditable="true"]', 'textarea', '.ProseMirror', '.tiptap'];
1224
1441
  for (const selector of inputSelectors) {
1225
1442
  content.querySelectorAll(selector).forEach(el => {
1226
1443
  el.style.cursor = 'text';
@@ -1265,6 +1482,132 @@
1265
1482
  };
1266
1483
  });
1267
1484
 
1485
+ // Send button - find and make it work
1486
+ // CONFIRMED via Playwriter: The send button is <button data-variant="submit"> with codicon-arrow-up icon
1487
+
1488
+ // Helper function to attach send handler to a button
1489
+ const attachSendHandler = (btn) => {
1490
+ if (!btn || btn.dataset.sendHandlerAttached) return;
1491
+ btn.dataset.sendHandlerAttached = 'true';
1492
+
1493
+ // Remove disabled so button is always clickable and highlighted
1494
+ btn.removeAttribute('disabled');
1495
+ btn.style.cursor = 'pointer';
1496
+
1497
+ btn.onclick = async (e) => {
1498
+ e.preventDefault();
1499
+ e.stopPropagation();
1500
+
1501
+ // Find the input field and get its text
1502
+ const inputSelectors = ['.tiptap', '.ProseMirror', '[data-lexical-editor="true"]', '[contenteditable="true"]', 'textarea'];
1503
+ let inputText = '';
1504
+ let inputEl = null;
1505
+
1506
+ for (const inputSel of inputSelectors) {
1507
+ const input = content.querySelector(inputSel);
1508
+ if (input) {
1509
+ inputEl = input;
1510
+ inputText = input.textContent || input.innerText || input.value || '';
1511
+ if (inputText.trim()) break;
1512
+ }
1513
+ }
1514
+
1515
+ if (inputText.trim()) {
1516
+ await sendToKiro(inputText.trim());
1517
+ // Clear the input after sending
1518
+ if (inputEl) {
1519
+ if (inputEl.textContent !== undefined) {
1520
+ inputEl.textContent = '';
1521
+ inputEl.innerHTML = '';
1522
+ } else if (inputEl.value !== undefined) {
1523
+ inputEl.value = '';
1524
+ }
1525
+ }
1526
+ showToast('Sent!', 1500, true);
1527
+ } else {
1528
+ showToast('Type a message first', 1500);
1529
+ }
1530
+ return false;
1531
+ };
1532
+ };
1533
+
1534
+ // PRIMARY METHOD: Find the submit button by data-variant="submit" (CONFIRMED via Playwriter)
1535
+ const submitBtn = content.querySelector('button[data-variant="submit"]');
1536
+ if (submitBtn) {
1537
+ attachSendHandler(submitBtn);
1538
+ }
1539
+
1540
+ // FALLBACK Method 1: Find buttons with codicon-arrow-up (Kiro's send icon)
1541
+ content.querySelectorAll('button').forEach(btn => {
1542
+ if (btn.dataset.sendHandlerAttached) return;
1543
+ const hasArrowUp = btn.querySelector('.codicon-arrow-up, .codicon-send, [class*="arrow-up"]');
1544
+ if (hasArrowUp) {
1545
+ attachSendHandler(btn);
1546
+ }
1547
+ });
1548
+
1549
+ // FALLBACK Method 2: Find SVGs that look like arrows
1550
+ content.querySelectorAll('svg').forEach(svg => {
1551
+ const svgClass = (svg.getAttribute('class') || '').toLowerCase();
1552
+ const svgParent = svg.closest('button');
1553
+
1554
+ if (svgClass.includes('arrow') || svgClass.includes('send') || svgClass.includes('lucide')) {
1555
+ if (svgParent) {
1556
+ attachSendHandler(svgParent);
1557
+ }
1558
+ }
1559
+ });
1560
+
1561
+ // FALLBACK Method 3: Find buttons by various selectors
1562
+ const sendButtonSelectors = [
1563
+ 'button[type="submit"]',
1564
+ 'button[aria-label*="send" i]',
1565
+ 'button[aria-label*="submit" i]',
1566
+ '[class*="send-button"]',
1567
+ '[class*="sendButton"]',
1568
+ '[class*="submit-button"]',
1569
+ '[class*="submitButton"]'
1570
+ ];
1571
+
1572
+ for (const selector of sendButtonSelectors) {
1573
+ try {
1574
+ content.querySelectorAll(selector).forEach(el => {
1575
+ const btn = el.closest('button') || el;
1576
+ if (btn.tagName === 'BUTTON') {
1577
+ attachSendHandler(btn);
1578
+ }
1579
+ });
1580
+ } catch(e) {}
1581
+ }
1582
+
1583
+ // Method 3: Find ALL buttons and attach handler to ones that look like send buttons
1584
+ // This is more reliable than using getBoundingClientRect which doesn't work before layout
1585
+ content.querySelectorAll('button').forEach(btn => {
1586
+ if (btn.dataset.sendHandlerAttached) return;
1587
+
1588
+ const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
1589
+ const className = (btn.className || '').toLowerCase();
1590
+ const innerHTML = btn.innerHTML.toLowerCase();
1591
+
1592
+ // Skip buttons that are clearly NOT send buttons
1593
+ if (ariaLabel.includes('context') || ariaLabel.includes('model') ||
1594
+ ariaLabel.includes('close') || ariaLabel.includes('menu') ||
1595
+ className.includes('context') || className.includes('model') ||
1596
+ className.includes('dropdown') || className.includes('toggle') ||
1597
+ className.includes('close') || className.includes('menu')) {
1598
+ return;
1599
+ }
1600
+
1601
+ // Check if button contains an arrow SVG (common for send buttons)
1602
+ const hasSvg = btn.querySelector('svg');
1603
+ const hasArrowPath = btn.querySelector('path, line, polyline');
1604
+
1605
+ // If button has an SVG with arrow-like elements, it's likely a send button
1606
+ if (hasSvg && hasArrowPath) {
1607
+ attachSendHandler(btn);
1608
+ }
1609
+ });
1610
+
1268
1611
  // Tabs
1269
1612
  content.querySelectorAll('[role="tab"]').forEach(tab => {
1270
1613
  const closeBtn = tab.querySelector('[aria-label="close"], [class*="close"]');
package/src/server.js CHANGED
@@ -559,6 +559,58 @@ async function captureSnapshot(cdp) {
559
559
  } catch(e) {}
560
560
  }
561
561
 
562
+ // REMOVE PLACEHOLDER TEXT from Lexical editor
563
+ // The placeholder is a sibling div to the contenteditable that overlays it
564
+ // We need to find and remove it to prevent text overlap on mobile
565
+ try {
566
+ // Method 1: Find placeholder by checking siblings of contenteditable
567
+ const editables = clone.querySelectorAll('[contenteditable="true"], [data-lexical-editor="true"]');
568
+ editables.forEach(editable => {
569
+ const parent = editable.parentElement;
570
+ if (parent) {
571
+ // Check all siblings
572
+ Array.from(parent.children).forEach(sibling => {
573
+ if (sibling === editable) return;
574
+ // Check if this sibling looks like a placeholder
575
+ const text = (sibling.textContent || '').toLowerCase();
576
+ if (text.includes('ask') || text.includes('question') || text.includes('task') ||
577
+ text.includes('describe') || text.includes('type') || text.includes('message')) {
578
+ sibling.remove();
579
+ }
580
+ });
581
+ }
582
+ });
583
+
584
+ // Method 2: Find by common placeholder class patterns
585
+ const placeholderSelectors = [
586
+ '[class*="placeholder"]',
587
+ '[class*="Placeholder"]',
588
+ '[data-placeholder]'
589
+ ];
590
+ placeholderSelectors.forEach(sel => {
591
+ clone.querySelectorAll(sel).forEach(el => {
592
+ // Don't remove the actual input
593
+ if (el.matches('[contenteditable], [data-lexical-editor], textarea, input')) return;
594
+ // Don't remove if it contains an input
595
+ if (el.querySelector('[contenteditable], [data-lexical-editor], textarea, input')) return;
596
+ el.remove();
597
+ });
598
+ });
599
+
600
+ // Method 3: Find any element with placeholder-like text that's positioned absolutely
601
+ clone.querySelectorAll('*').forEach(el => {
602
+ if (el.matches('[contenteditable], [data-lexical-editor], textarea, input, button, svg')) return;
603
+ const text = (el.textContent || '').toLowerCase().trim();
604
+ const style = el.getAttribute('style') || '';
605
+ // If it has placeholder text and is positioned (likely overlay)
606
+ if ((text.includes('ask a question') || text.includes('describe a task') ||
607
+ text === 'ask a question or describe a task...' || text === 'ask a question or describe a task') &&
608
+ (style.includes('position') || el.children.length === 0)) {
609
+ el.remove();
610
+ }
611
+ });
612
+ } catch(e) {}
613
+
562
614
  return {
563
615
  html: clone.outerHTML,
564
616
  bodyBg,
@@ -1077,10 +1129,20 @@ function createInjectMessageScript(messageText) {
1077
1129
  }
1078
1130
 
1079
1131
  // 6.1 Find input element (contenteditable or textarea)
1080
- // Try Kiro's Lexical editor first (contenteditable div)
1081
- let editors = [...targetDoc.querySelectorAll('[data-lexical-editor="true"][contenteditable="true"][role="textbox"]')]
1132
+ // UPDATED: Support both Lexical and ProseMirror/TipTap editors
1133
+ let editor = null;
1134
+
1135
+ // Try ProseMirror/TipTap editor first (confirmed via Playwriter debugging)
1136
+ let editors = [...targetDoc.querySelectorAll('.tiptap.ProseMirror[contenteditable="true"]')]
1082
1137
  .filter(el => el.offsetParent !== null);
1083
- let editor = editors.at(-1);
1138
+ editor = editors.at(-1);
1139
+
1140
+ // Fallback: Try Kiro's Lexical editor
1141
+ if (!editor) {
1142
+ editors = [...targetDoc.querySelectorAll('[data-lexical-editor="true"][contenteditable="true"][role="textbox"]')]
1143
+ .filter(el => el.offsetParent !== null);
1144
+ editor = editors.at(-1);
1145
+ }
1084
1146
 
1085
1147
  // Fallback: try any contenteditable in the cascade area
1086
1148
  if (!editor) {
@@ -1108,6 +1170,7 @@ function createInjectMessageScript(messageText) {
1108
1170
  }
1109
1171
 
1110
1172
  const isTextarea = editor.tagName.toLowerCase() === 'textarea';
1173
+ const isProseMirror = editor.classList.contains('ProseMirror') || editor.classList.contains('tiptap');
1111
1174
 
1112
1175
  // 6.2 Insert text into input element
1113
1176
  editor.focus();
@@ -1117,6 +1180,22 @@ function createInjectMessageScript(messageText) {
1117
1180
  editor.value = text;
1118
1181
  editor.dispatchEvent(new Event('input', { bubbles: true }));
1119
1182
  editor.dispatchEvent(new Event('change', { bubbles: true }));
1183
+ } else if (isProseMirror) {
1184
+ // For ProseMirror/TipTap, we need to use their specific API or simulate typing
1185
+ // First clear existing content
1186
+ editor.innerHTML = '';
1187
+
1188
+ // Create a paragraph with the text (ProseMirror structure)
1189
+ const p = targetDoc.createElement('p');
1190
+ p.textContent = text;
1191
+ editor.appendChild(p);
1192
+
1193
+ // Dispatch input event to trigger ProseMirror's update
1194
+ editor.dispatchEvent(new InputEvent('input', {
1195
+ bubbles: true,
1196
+ inputType: 'insertText',
1197
+ data: text
1198
+ }));
1120
1199
  } else {
1121
1200
  // For contenteditable, use execCommand or fallback to direct manipulation
1122
1201
  // First, select all and delete existing content
@@ -2180,6 +2259,37 @@ async function clickElement(cdp, clickInfo) {
2180
2259
  let isCloseButton = info.isCloseButton || (info.ariaLabel && info.ariaLabel.toLowerCase() === 'close');
2181
2260
  let isToggle = info.isToggle || info.role === 'switch';
2182
2261
  let isDropdown = info.isDropdown || info.ariaHaspopup;
2262
+ let isSendButton = info.isSendButton || (info.ariaLabel && info.ariaLabel.toLowerCase().includes('send'));
2263
+
2264
+ // Handle send button clicks
2265
+ if (isSendButton && !element) {
2266
+ // Try to find the send button (arrow-right icon button)
2267
+ const sendSelectors = [
2268
+ 'svg.lucide-arrow-right',
2269
+ 'svg[class*="arrow-right"]',
2270
+ '[data-tooltip-id*="send"]',
2271
+ 'button[type="submit"]',
2272
+ 'button[aria-label*="send" i]',
2273
+ 'button[aria-label*="submit" i]',
2274
+ '[class*="send-button"]',
2275
+ '[class*="sendButton"]',
2276
+ '[class*="submit-button"]',
2277
+ '[class*="submitButton"]'
2278
+ ];
2279
+
2280
+ for (const sel of sendSelectors) {
2281
+ try {
2282
+ const el = targetDoc.querySelector(sel);
2283
+ if (el) {
2284
+ element = el.closest('button') || el;
2285
+ if (element && !element.disabled) {
2286
+ matchMethod = 'send-button-' + sel.split('[')[0];
2287
+ break;
2288
+ }
2289
+ }
2290
+ } catch(e) {}
2291
+ }
2292
+ }
2183
2293
 
2184
2294
  // Handle toggle/switch clicks
2185
2295
  if (isToggle && !element) {