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 +1 -1
- package/src/public/index.html +388 -45
- package/src/server.js +113 -3
package/package.json
CHANGED
package/src/public/index.html
CHANGED
|
@@ -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,
|
|
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:
|
|
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
|
-
.
|
|
95
|
-
.
|
|
96
|
-
.
|
|
97
|
-
.
|
|
98
|
-
.
|
|
99
|
-
.
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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 || ' '}</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
|
|
682
|
+
// Syntax highlighting - simple and safe approach
|
|
667
683
|
function highlightSyntax(line, language) {
|
|
668
|
-
|
|
684
|
+
// First escape HTML to prevent XSS
|
|
685
|
+
const escaped = escapeHtml(line);
|
|
669
686
|
|
|
670
|
-
// Skip
|
|
671
|
-
if (
|
|
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
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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(/(<!--[\s\S]*?-->)/g, '<span class="tok-cmt">$1</span>');
|
|
707
|
+
// Strings in attributes
|
|
708
|
+
result = result.replace(/=("[^&]*?")/g, '=<span class="tok-str">$1</span>');
|
|
709
|
+
result = result.replace(/=('[^&]*?')/g, '=<span class="tok-str">$1</span>');
|
|
710
|
+
return result;
|
|
682
711
|
}
|
|
683
712
|
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
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(/("[^&]*?")/g, '<span class="tok-str">$1</span>');
|
|
720
|
+
result = result.replace(/('[^&]*?')/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
|
-
//
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
696
|
-
|
|
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: " or ')
|
|
744
|
+
if (escaped.slice(i, i + 6) === '"') {
|
|
745
|
+
const endIdx = escaped.indexOf('"', 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) === ''') {
|
|
754
|
+
const endIdx = escaped.indexOf(''', 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
|
|
812
|
+
return result;
|
|
699
813
|
}
|
|
700
814
|
|
|
701
815
|
// Helper functions
|
|
702
816
|
function escapeHtml(text) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
817
|
+
return text
|
|
818
|
+
.replace(/&/g, '&')
|
|
819
|
+
.replace(/</g, '<')
|
|
820
|
+
.replace(/>/g, '>')
|
|
821
|
+
.replace(/"/g, '"')
|
|
822
|
+
.replace(/'/g, ''');
|
|
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
|
-
//
|
|
1081
|
-
let
|
|
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
|
-
|
|
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) {
|