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 +1 -1
- package/src/public/index.html +389 -45
- package/src/server.js +181 -10
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; }
|
|
@@ -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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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 || ' '}</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
|
|
682
|
+
// Syntax highlighting - simple and safe approach
|
|
666
683
|
function highlightSyntax(line, language) {
|
|
667
|
-
|
|
684
|
+
// First escape HTML to prevent XSS
|
|
685
|
+
const escaped = escapeHtml(line);
|
|
668
686
|
|
|
669
|
-
// Skip
|
|
670
|
-
if (
|
|
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
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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;
|
|
681
711
|
}
|
|
682
712
|
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
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
|
+
}
|
|
686
727
|
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
695
|
-
|
|
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
|
+
}
|
|
696
811
|
|
|
697
|
-
return
|
|
812
|
+
return result;
|
|
698
813
|
}
|
|
699
814
|
|
|
700
815
|
// Helper functions
|
|
701
816
|
function escapeHtml(text) {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
817
|
+
return text
|
|
818
|
+
.replace(/&/g, '&')
|
|
819
|
+
.replace(/</g, '<')
|
|
820
|
+
.replace(/>/g, '>')
|
|
821
|
+
.replace(/"/g, '"')
|
|
822
|
+
.replace(/'/g, ''');
|
|
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
|
-
//
|
|
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
|
|
@@ -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}
|
|
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) {
|