kiro-mobile-bridge 1.0.5 → 1.0.7

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.7",
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; }
63
- .editor-line { display: flex; min-height: 1.6em; }
62
+ .editor-code { margin: 0; padding: 8px 0; font-family: Consolas, 'Courier New', monospace; font-size: 12px; line-height: 1.5; color: #d4d4d4; background: #1e1e1e; }
63
+ .editor-line { display: flex; min-height: 18px; }
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: 12px; 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;
@@ -839,6 +1056,22 @@
839
1056
  background: rgba(255, 255, 255, 0.1) !important;
840
1057
  }
841
1058
 
1059
+ /* Hide notification bars and change acceptance UI */
1060
+ [class*="notification"], [class*="Notification"],
1061
+ [class*="toast"], [class*="Toast"],
1062
+ [class*="banner"], [class*="Banner"],
1063
+ [class*="change-accepted"], [class*="changeAccepted"],
1064
+ [class*="revert"], [class*="Revert"],
1065
+ [class*="view-all"], [class*="viewAll"],
1066
+ [class*="status-bar-notification"],
1067
+ div[role="status"], div[role="alert"] {
1068
+ display: none !important;
1069
+ visibility: hidden !important;
1070
+ opacity: 0 !important;
1071
+ height: 0 !important;
1072
+ overflow: hidden !important;
1073
+ }
1074
+
842
1075
  /* Model selector chevron/arrow icon */
843
1076
  [class*="model-selector"] svg, [class*="modelSelector"] svg,
844
1077
  [class*="model-dropdown"] svg, button[class*="dropdown"] svg {
@@ -1219,8 +1452,8 @@
1219
1452
  function makeInteractive() {
1220
1453
  const content = panels.chat.content;
1221
1454
 
1222
- // Input fields
1223
- const inputSelectors = ['[data-lexical-editor="true"]', '[contenteditable="true"][role="textbox"]', '[contenteditable="true"]', 'textarea', '.ProseMirror'];
1455
+ // Input fields - support both Lexical and ProseMirror/TipTap editors
1456
+ const inputSelectors = ['[data-lexical-editor="true"]', '[contenteditable="true"][role="textbox"]', '[contenteditable="true"]', 'textarea', '.ProseMirror', '.tiptap'];
1224
1457
  for (const selector of inputSelectors) {
1225
1458
  content.querySelectorAll(selector).forEach(el => {
1226
1459
  el.style.cursor = 'text';
@@ -1265,6 +1498,132 @@
1265
1498
  };
1266
1499
  });
1267
1500
 
1501
+ // Send button - find and make it work
1502
+ // CONFIRMED via Playwriter: The send button is <button data-variant="submit"> with codicon-arrow-up icon
1503
+
1504
+ // Helper function to attach send handler to a button
1505
+ const attachSendHandler = (btn) => {
1506
+ if (!btn || btn.dataset.sendHandlerAttached) return;
1507
+ btn.dataset.sendHandlerAttached = 'true';
1508
+
1509
+ // Remove disabled so button is always clickable and highlighted
1510
+ btn.removeAttribute('disabled');
1511
+ btn.style.cursor = 'pointer';
1512
+
1513
+ btn.onclick = async (e) => {
1514
+ e.preventDefault();
1515
+ e.stopPropagation();
1516
+
1517
+ // Find the input field and get its text
1518
+ const inputSelectors = ['.tiptap', '.ProseMirror', '[data-lexical-editor="true"]', '[contenteditable="true"]', 'textarea'];
1519
+ let inputText = '';
1520
+ let inputEl = null;
1521
+
1522
+ for (const inputSel of inputSelectors) {
1523
+ const input = content.querySelector(inputSel);
1524
+ if (input) {
1525
+ inputEl = input;
1526
+ inputText = input.textContent || input.innerText || input.value || '';
1527
+ if (inputText.trim()) break;
1528
+ }
1529
+ }
1530
+
1531
+ if (inputText.trim()) {
1532
+ await sendToKiro(inputText.trim());
1533
+ // Clear the input after sending
1534
+ if (inputEl) {
1535
+ if (inputEl.textContent !== undefined) {
1536
+ inputEl.textContent = '';
1537
+ inputEl.innerHTML = '';
1538
+ } else if (inputEl.value !== undefined) {
1539
+ inputEl.value = '';
1540
+ }
1541
+ }
1542
+ showToast('Sent!', 1500, true);
1543
+ } else {
1544
+ showToast('Type a message first', 1500);
1545
+ }
1546
+ return false;
1547
+ };
1548
+ };
1549
+
1550
+ // PRIMARY METHOD: Find the submit button by data-variant="submit" (CONFIRMED via Playwriter)
1551
+ const submitBtn = content.querySelector('button[data-variant="submit"]');
1552
+ if (submitBtn) {
1553
+ attachSendHandler(submitBtn);
1554
+ }
1555
+
1556
+ // FALLBACK Method 1: Find buttons with codicon-arrow-up (Kiro's send icon)
1557
+ content.querySelectorAll('button').forEach(btn => {
1558
+ if (btn.dataset.sendHandlerAttached) return;
1559
+ const hasArrowUp = btn.querySelector('.codicon-arrow-up, .codicon-send, [class*="arrow-up"]');
1560
+ if (hasArrowUp) {
1561
+ attachSendHandler(btn);
1562
+ }
1563
+ });
1564
+
1565
+ // FALLBACK Method 2: Find SVGs that look like arrows
1566
+ content.querySelectorAll('svg').forEach(svg => {
1567
+ const svgClass = (svg.getAttribute('class') || '').toLowerCase();
1568
+ const svgParent = svg.closest('button');
1569
+
1570
+ if (svgClass.includes('arrow') || svgClass.includes('send') || svgClass.includes('lucide')) {
1571
+ if (svgParent) {
1572
+ attachSendHandler(svgParent);
1573
+ }
1574
+ }
1575
+ });
1576
+
1577
+ // FALLBACK Method 3: Find buttons by various selectors
1578
+ const sendButtonSelectors = [
1579
+ 'button[type="submit"]',
1580
+ 'button[aria-label*="send" i]',
1581
+ 'button[aria-label*="submit" i]',
1582
+ '[class*="send-button"]',
1583
+ '[class*="sendButton"]',
1584
+ '[class*="submit-button"]',
1585
+ '[class*="submitButton"]'
1586
+ ];
1587
+
1588
+ for (const selector of sendButtonSelectors) {
1589
+ try {
1590
+ content.querySelectorAll(selector).forEach(el => {
1591
+ const btn = el.closest('button') || el;
1592
+ if (btn.tagName === 'BUTTON') {
1593
+ attachSendHandler(btn);
1594
+ }
1595
+ });
1596
+ } catch(e) {}
1597
+ }
1598
+
1599
+ // Method 3: Find ALL buttons and attach handler to ones that look like send buttons
1600
+ // This is more reliable than using getBoundingClientRect which doesn't work before layout
1601
+ content.querySelectorAll('button').forEach(btn => {
1602
+ if (btn.dataset.sendHandlerAttached) return;
1603
+
1604
+ const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
1605
+ const className = (btn.className || '').toLowerCase();
1606
+ const innerHTML = btn.innerHTML.toLowerCase();
1607
+
1608
+ // Skip buttons that are clearly NOT send buttons
1609
+ if (ariaLabel.includes('context') || ariaLabel.includes('model') ||
1610
+ ariaLabel.includes('close') || ariaLabel.includes('menu') ||
1611
+ className.includes('context') || className.includes('model') ||
1612
+ className.includes('dropdown') || className.includes('toggle') ||
1613
+ className.includes('close') || className.includes('menu')) {
1614
+ return;
1615
+ }
1616
+
1617
+ // Check if button contains an arrow SVG (common for send buttons)
1618
+ const hasSvg = btn.querySelector('svg');
1619
+ const hasArrowPath = btn.querySelector('path, line, polyline');
1620
+
1621
+ // If button has an SVG with arrow-like elements, it's likely a send button
1622
+ if (hasSvg && hasArrowPath) {
1623
+ attachSendHandler(btn);
1624
+ }
1625
+ });
1626
+
1268
1627
  // Tabs
1269
1628
  content.querySelectorAll('[role="tab"]').forEach(tab => {
1270
1629
  const closeBtn = tab.querySelector('[aria-label="close"], [class*="close"]');
@@ -1274,7 +1633,21 @@
1274
1633
  closeBtn.onclick = async (e) => {
1275
1634
  e.preventDefault();
1276
1635
  e.stopPropagation();
1277
- await sendClickToKiro({ tag: 'button', text: 'close', ariaLabel: 'close', role: 'button', isCloseButton: true });
1636
+
1637
+ // FIXED: Include tab label to identify which tab's close button to click
1638
+ const labelEl = tab.querySelector('.kiro-tabs-item-label, [class*="label"]');
1639
+ const tabLabel = labelEl ? labelEl.textContent.trim() : tab.textContent.trim();
1640
+
1641
+ console.log('[Tab Close] Closing tab:', tabLabel);
1642
+
1643
+ await sendClickToKiro({
1644
+ tag: 'button',
1645
+ text: 'close',
1646
+ ariaLabel: 'close',
1647
+ role: 'button',
1648
+ isCloseButton: true,
1649
+ parentTabLabel: tabLabel // NEW: Identify which tab this close button belongs to
1650
+ });
1278
1651
  return false;
1279
1652
  };
1280
1653
  }
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
@@ -2151,6 +2230,11 @@ app.post('/click/:id', async (req, res) => {
2151
2230
  const clickInfo = req.body;
2152
2231
  console.log(`[Click] Attempting click:`, clickInfo.text?.substring(0, 30) || clickInfo.ariaLabel || clickInfo.tag);
2153
2232
 
2233
+ // Log tab close operations for debugging
2234
+ if (clickInfo.isCloseButton && clickInfo.parentTabLabel) {
2235
+ console.log(`[Click] Closing tab: "${clickInfo.parentTabLabel}"`);
2236
+ }
2237
+
2154
2238
  try {
2155
2239
  const result = await clickElement(cascade.cdp, clickInfo);
2156
2240
  res.json(result);
@@ -2180,6 +2264,37 @@ async function clickElement(cdp, clickInfo) {
2180
2264
  let isCloseButton = info.isCloseButton || (info.ariaLabel && info.ariaLabel.toLowerCase() === 'close');
2181
2265
  let isToggle = info.isToggle || info.role === 'switch';
2182
2266
  let isDropdown = info.isDropdown || info.ariaHaspopup;
2267
+ let isSendButton = info.isSendButton || (info.ariaLabel && info.ariaLabel.toLowerCase().includes('send'));
2268
+
2269
+ // Handle send button clicks
2270
+ if (isSendButton && !element) {
2271
+ // Try to find the send button (arrow-right icon button)
2272
+ const sendSelectors = [
2273
+ 'svg.lucide-arrow-right',
2274
+ 'svg[class*="arrow-right"]',
2275
+ '[data-tooltip-id*="send"]',
2276
+ 'button[type="submit"]',
2277
+ 'button[aria-label*="send" i]',
2278
+ 'button[aria-label*="submit" i]',
2279
+ '[class*="send-button"]',
2280
+ '[class*="sendButton"]',
2281
+ '[class*="submit-button"]',
2282
+ '[class*="submitButton"]'
2283
+ ];
2284
+
2285
+ for (const sel of sendSelectors) {
2286
+ try {
2287
+ const el = targetDoc.querySelector(sel);
2288
+ if (el) {
2289
+ element = el.closest('button') || el;
2290
+ if (element && !element.disabled) {
2291
+ matchMethod = 'send-button-' + sel.split('[')[0];
2292
+ break;
2293
+ }
2294
+ }
2295
+ } catch(e) {}
2296
+ }
2297
+ }
2183
2298
 
2184
2299
  // Handle toggle/switch clicks
2185
2300
  if (isToggle && !element) {
@@ -2230,16 +2345,38 @@ async function clickElement(cdp, clickInfo) {
2230
2345
  // Handle close button clicks explicitly
2231
2346
  if (isCloseButton) {
2232
2347
  const closeButtons = targetDoc.querySelectorAll('[aria-label="close"], .kiro-tabs-item-close, [class*="close"]');
2233
- for (const btn of closeButtons) {
2234
- // Find the close button in the currently selected tab or matching context
2235
- const parentTab = btn.closest('[role="tab"]');
2236
- if (parentTab && parentTab.getAttribute('aria-selected') === 'true') {
2237
- element = btn;
2238
- matchMethod = 'close-button-selected-tab';
2239
- break;
2348
+
2349
+ // FIXED: If parentTabLabel is provided, find the close button in that specific tab
2350
+ if (info.parentTabLabel) {
2351
+ const searchLabel = info.parentTabLabel.trim().toLowerCase();
2352
+
2353
+ for (const btn of closeButtons) {
2354
+ const parentTab = btn.closest('[role="tab"]');
2355
+ if (parentTab) {
2356
+ const labelEl = parentTab.querySelector('.kiro-tabs-item-label, [class*="label"]');
2357
+ const tabLabel = labelEl ? labelEl.textContent.trim().toLowerCase() : parentTab.textContent.trim().toLowerCase();
2358
+
2359
+ // Match the tab by its label
2360
+ if (tabLabel.includes(searchLabel) || searchLabel.includes(tabLabel)) {
2361
+ element = btn;
2362
+ matchMethod = 'close-button-by-tab-label';
2363
+ break;
2364
+ }
2365
+ }
2366
+ }
2367
+ } else {
2368
+ // Fallback: Original logic - find close button in selected tab
2369
+ for (const btn of closeButtons) {
2370
+ const parentTab = btn.closest('[role="tab"]');
2371
+ if (parentTab && parentTab.getAttribute('aria-selected') === 'true') {
2372
+ element = btn;
2373
+ matchMethod = 'close-button-selected-tab';
2374
+ break;
2375
+ }
2240
2376
  }
2241
2377
  }
2242
- // If no selected tab close button, find any close button
2378
+
2379
+ // If still not found, use first close button as last resort
2243
2380
  if (!element && closeButtons.length > 0) {
2244
2381
  element = closeButtons[0];
2245
2382
  matchMethod = 'close-button-first';