kiro-mobile-bridge 1.0.4 → 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.4",
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; }
@@ -435,6 +450,7 @@
435
450
  function selectCascade(cascadeId) {
436
451
  selectedCascadeId = cascadeId;
437
452
  currentStyles = null;
453
+ workspaceFiles = []; // Clear file cache when switching cascades
438
454
 
439
455
  if (cascadeId) {
440
456
  fetchStyles(cascadeId).then(() => {
@@ -539,6 +555,9 @@
539
555
 
540
556
  hideLoading('chat');
541
557
 
558
+ // Remove placeholder text that overlaps with input
559
+ removePlaceholderText();
560
+
542
561
  // Find the new inner scrollable element and scroll it
543
562
  requestAnimationFrame(() => {
544
563
  requestAnimationFrame(() => {
@@ -624,12 +643,10 @@
624
643
  const displayLines = lines.slice(startIdx);
625
644
  displayLines.forEach((line, idx) => {
626
645
  const lineNum = startLineNum + startIdx + idx;
627
- const highlighted = highlightSyntax(line, data.language);
628
- // Preserve whitespace - convert tabs and spaces
629
- let preservedLine = highlighted
630
- .replace(/\t/g, ' ')
631
- .replace(/^ +/, match => ' '.repeat(match.length));
632
- 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>`;
633
650
  });
634
651
  html += '</pre>';
635
652
 
@@ -662,46 +679,147 @@
662
679
  }
663
680
  }
664
681
 
665
- // Syntax highlighting (basic)
682
+ // Syntax highlighting - simple and safe approach
666
683
  function highlightSyntax(line, language) {
667
- let escaped = escapeHtml(line);
684
+ // First escape HTML to prevent XSS
685
+ const escaped = escapeHtml(line);
668
686
 
669
- // Skip syntax highlighting for CSS (too complex with # colors)
670
- if (language === 'css' || language === 'html') {
687
+ // Skip highlighting for very long lines or if no language
688
+ if (escaped.length > 1000 || !language) {
671
689
  return escaped;
672
690
  }
673
691
 
674
- // Comments - be careful not to match CSS color codes
675
- // Only match // comments and # comments that start at beginning or after whitespace
676
- escaped = escaped.replace(/(\/\/.*$)/gm, '<span class="token-comment">$1</span>');
677
- escaped = escaped.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="token-comment">$1</span>');
678
- // Python/shell comments - only if # is at start of line or after whitespace, not in middle of word
679
- if (language === 'python' || language === 'bash' || language === 'sh') {
680
- 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;
681
711
  }
682
712
 
683
- // Strings - match quoted strings
684
- escaped = escaped.replace(/(&quot;[^&]*?&quot;)/g, '<span class="token-string">$1</span>');
685
- 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
+ }
686
727
 
687
- // Keywords
688
- 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'];
689
- keywords.forEach(kw => {
690
- const regex = new RegExp(`\\b(${kw})\\b`, 'g');
691
- escaped = escaped.replace(regex, '<span class="token-keyword">$1</span>');
692
- });
728
+ // Build result by processing character by character for other languages
729
+ let result = '';
730
+ let i = 0;
693
731
 
694
- // Numbers - but not inside other tokens
695
- 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
+ }
696
811
 
697
- return escaped;
812
+ return result;
698
813
  }
699
814
 
700
815
  // Helper functions
701
816
  function escapeHtml(text) {
702
- const div = document.createElement('div');
703
- div.textContent = text;
704
- 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;');
705
823
  }
706
824
 
707
825
  function showEmptyState(panelName, icon, message) {
@@ -721,6 +839,74 @@
721
839
  if (content) content.classList.toggle('collapsed');
722
840
  }
723
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
+
724
910
  // Base styles for chat panel
725
911
  function getBaseStyles() {
726
912
  return `<style>
@@ -758,6 +944,19 @@
758
944
  background-color: #1e1e1e !important;
759
945
  }
760
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
+
761
960
  /* CRITICAL: Hide tooltips, popovers, and overlay elements - but NOT dropdown buttons */
762
961
  [role="tooltip"],
763
962
  [data-tooltip],
@@ -808,6 +1007,25 @@
808
1007
  color: #cccccc !important;
809
1008
  }
810
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
+
811
1029
  [class*="dropdown-item"]:hover, [class*="dropdownItem"]:hover,
812
1030
  [role="option"]:hover, [role="menuitem"]:hover {
813
1031
  background: rgba(255, 255, 255, 0.1) !important;
@@ -1218,8 +1436,8 @@
1218
1436
  function makeInteractive() {
1219
1437
  const content = panels.chat.content;
1220
1438
 
1221
- // Input fields
1222
- 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'];
1223
1441
  for (const selector of inputSelectors) {
1224
1442
  content.querySelectorAll(selector).forEach(el => {
1225
1443
  el.style.cursor = 'text';
@@ -1264,6 +1482,132 @@
1264
1482
  };
1265
1483
  });
1266
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
+
1267
1611
  // Tabs
1268
1612
  content.querySelectorAll('[role="tab"]').forEach(tab => {
1269
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
@@ -1848,8 +1927,6 @@ app.get('/files/:id', async (req, res) => {
1848
1927
  return res.status(404).json({ error: 'Cascade not found' });
1849
1928
  }
1850
1929
 
1851
- console.log(`[Files] Listing workspace files`);
1852
-
1853
1930
  try {
1854
1931
  const fs = await import('fs/promises');
1855
1932
  const path = await import('path');
@@ -1885,9 +1962,72 @@ app.get('/files/:id', async (req, res) => {
1885
1962
  '.cob': 'cobol', '.cbl': 'cobol'
1886
1963
  };
1887
1964
 
1965
+ // Try to get workspace root from Kiro/VS Code window title
1966
+ let workspaceRoot = null;
1967
+
1968
+ // Method 1: Try to extract from main window CDP (VS Code window title contains workspace path)
1969
+ if (mainWindowCDP.connection && mainWindowCDP.connection.rootContextId) {
1970
+ try {
1971
+ const titleScript = `document.title`;
1972
+ const titleResult = await mainWindowCDP.connection.call('Runtime.evaluate', {
1973
+ expression: titleScript,
1974
+ contextId: mainWindowCDP.connection.rootContextId,
1975
+ returnByValue: true
1976
+ });
1977
+
1978
+ const windowTitle = titleResult.result?.value || '';
1979
+ // VS Code/Kiro title format: "filename - foldername - Kiro" or "foldername - Kiro"
1980
+ // Extract the workspace folder name from title
1981
+ const titleParts = windowTitle.split(' - ');
1982
+ if (titleParts.length >= 2) {
1983
+ // The folder name is usually the second-to-last part before "Kiro"
1984
+ const folderName = titleParts[titleParts.length - 2].trim();
1985
+
1986
+ // Try to find this folder in common locations
1987
+ const possibleRoots = [
1988
+ process.cwd(),
1989
+ path.join(process.env.HOME || process.env.USERPROFILE || '', folderName),
1990
+ path.join(process.env.HOME || process.env.USERPROFILE || '', 'projects', folderName),
1991
+ path.join(process.env.HOME || process.env.USERPROFILE || '', 'dev', folderName),
1992
+ path.join(process.env.HOME || process.env.USERPROFILE || '', 'code', folderName),
1993
+ path.join(process.env.HOME || process.env.USERPROFILE || '', 'workspace', folderName),
1994
+ // Windows common paths
1995
+ path.join('C:', 'Users', process.env.USERNAME || '', folderName),
1996
+ path.join('C:', 'gab', folderName),
1997
+ path.join('C:', 'dev', folderName),
1998
+ path.join('C:', 'projects', folderName),
1999
+ ];
2000
+
2001
+ for (const root of possibleRoots) {
2002
+ try {
2003
+ const stat = await fs.stat(root);
2004
+ if (stat.isDirectory()) {
2005
+ // Verify it looks like a project (has package.json, .git, or src folder)
2006
+ const hasPackageJson = await fs.access(path.join(root, 'package.json')).then(() => true).catch(() => false);
2007
+ const hasGit = await fs.access(path.join(root, '.git')).then(() => true).catch(() => false);
2008
+ const hasSrc = await fs.access(path.join(root, 'src')).then(() => true).catch(() => false);
2009
+
2010
+ if (hasPackageJson || hasGit || hasSrc) {
2011
+ workspaceRoot = root;
2012
+ break;
2013
+ }
2014
+ }
2015
+ } catch (e) {
2016
+ // Path doesn't exist, try next
2017
+ }
2018
+ }
2019
+ }
2020
+ } catch (e) {
2021
+ // CDP call failed, continue with fallback
2022
+ }
2023
+ }
2024
+
2025
+ // Method 2: Fallback to current working directory
2026
+ if (!workspaceRoot) {
2027
+ workspaceRoot = process.cwd();
2028
+ }
2029
+
1888
2030
  const files = [];
1889
- // Use parent directory as workspace root (kiro-mobile-bridge is inside the workspace)
1890
- const workspaceRoot = path.dirname(__dirname);
1891
2031
 
1892
2032
  // Recursive function to collect files
1893
2033
  async function collectFiles(dir, relativePath = '', depth = 0) {
@@ -1897,8 +2037,8 @@ app.get('/files/:id', async (req, res) => {
1897
2037
  const entries = await fs.readdir(dir, { withFileTypes: true });
1898
2038
 
1899
2039
  for (const entry of entries) {
1900
- // Skip hidden files/folders EXCEPT .kiro, and common non-code directories
1901
- if ((entry.name.startsWith('.') && entry.name !== '.kiro') ||
2040
+ // Skip hidden files/folders EXCEPT .kiro and .github, and common non-code directories
2041
+ if ((entry.name.startsWith('.') && entry.name !== '.kiro' && entry.name !== '.github') ||
1902
2042
  entry.name === 'node_modules' ||
1903
2043
  entry.name === 'dist' ||
1904
2044
  entry.name === 'build' ||
@@ -1935,7 +2075,7 @@ app.get('/files/:id', async (req, res) => {
1935
2075
  // Sort files: by path for easier browsing
1936
2076
  files.sort((a, b) => a.path.localeCompare(b.path));
1937
2077
 
1938
- console.log(`[Files] Found ${files.length} code files`);
2078
+ console.log(`[Files] Found ${files.length} files in ${workspaceRoot}`);
1939
2079
  res.json({ files, workspaceRoot });
1940
2080
 
1941
2081
  } catch (err) {
@@ -2119,6 +2259,37 @@ async function clickElement(cdp, clickInfo) {
2119
2259
  let isCloseButton = info.isCloseButton || (info.ariaLabel && info.ariaLabel.toLowerCase() === 'close');
2120
2260
  let isToggle = info.isToggle || info.role === 'switch';
2121
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
+ }
2122
2293
 
2123
2294
  // Handle toggle/switch clicks
2124
2295
  if (isToggle && !element) {