kiro-mobile-bridge 1.0.8 → 1.0.11
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/README.md +9 -5
- package/package.json +1 -1
- package/src/public/index.html +258 -11
- package/src/routes/api.js +257 -76
- package/src/server.js +33 -17
- package/src/services/cdp.js +70 -16
- package/src/services/click.js +325 -74
- package/src/services/message.js +9 -1
- package/src/services/snapshot.js +64 -25
- package/src/utils/constants.js +116 -0
- package/src/utils/hash.js +12 -0
- package/src/utils/network.js +72 -2
- package/src/utils/security.js +160 -0
package/README.md
CHANGED
|
@@ -8,8 +8,11 @@ A mobile web interface for monitoring Kiro IDE agent sessions from your phone ov
|
|
|
8
8
|
|
|
9
9
|
- 📱 Mobile-optimized web interface with tab navigation
|
|
10
10
|
- 💬 **Chat Panel** - View and send messages to Kiro's agent
|
|
11
|
-
- 📝 **
|
|
12
|
-
-
|
|
11
|
+
- 📝 **Code Panel** - Browse file explorer and view files with syntax highlighting
|
|
12
|
+
- 📋 **Tasks Panel** - View and navigate Kiro spec task files
|
|
13
|
+
- 🔄 Real-time updates via WebSocket with adaptive polling
|
|
14
|
+
- 🔍 Auto-discovers Kiro instances on ports 9000-9003, 9222, 9229
|
|
15
|
+
- 🎨 Preserves original Kiro styling
|
|
13
16
|
|
|
14
17
|
## Prerequisites
|
|
15
18
|
|
|
@@ -65,7 +68,8 @@ Open the Network URL on your phone to monitor Kiro.
|
|
|
65
68
|
|
|
66
69
|
1. Make sure your phone is on the **same WiFi network** as your computer
|
|
67
70
|
2. Open the **Network URL** (e.g., `http://192.168.1.100:3000`) in your phone's browser
|
|
68
|
-
3. The interface will automatically connect and show your Kiro
|
|
71
|
+
3. The interface will automatically connect and show your Kiro session
|
|
72
|
+
4. Use the tabs to switch between Chat, Code, and Tasks panels
|
|
69
73
|
|
|
70
74
|
|
|
71
75
|
#### How It Works
|
|
@@ -84,9 +88,9 @@ Open the Network URL on your phone to monitor Kiro.
|
|
|
84
88
|
└─────────────────┘
|
|
85
89
|
```
|
|
86
90
|
|
|
87
|
-
1. **Discovery**: Server scans ports 9000-9003 for Kiro instances (adaptive: 10s → 30s when stable)
|
|
91
|
+
1. **Discovery**: Server scans ports 9000-9003, 9222, 9229 for Kiro instances (adaptive: 10s → 30s when stable)
|
|
88
92
|
2. **Connection**: Connects to Kiro via CDP WebSocket
|
|
89
|
-
3. **Snapshots**: Captures chat
|
|
93
|
+
3. **Snapshots**: Captures chat, editor, and tasks with adaptive polling (1s active → 3s idle)
|
|
90
94
|
4. **Messages**: Injects text into Kiro's chat input via CDP
|
|
91
95
|
|
|
92
96
|
## Troubleshooting
|
package/package.json
CHANGED
package/src/public/index.html
CHANGED
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
.file-tree { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 4px 0; }
|
|
173
173
|
.file-tree-folder-header { display: flex; align-items: center; gap: 6px; padding: 6px 10px; cursor: pointer; user-select: none; }
|
|
174
174
|
.file-tree-folder-header:hover { background: rgba(255,255,255,0.08); }
|
|
175
|
-
.file-tree-folder-icon { color: #
|
|
175
|
+
.file-tree-folder-icon { color: #a78bfa; font-size: 14px; transition: transform 0.15s; }
|
|
176
176
|
.file-tree-folder-icon.expanded { transform: rotate(90deg); }
|
|
177
177
|
.file-tree-folder-name { font-size: 13px; color: #c5c5c5; }
|
|
178
178
|
.file-tree-folder-contents { padding-left: 16px; display: none; }
|
|
@@ -212,6 +212,11 @@
|
|
|
212
212
|
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #888; text-align: center; padding: 20px; }
|
|
213
213
|
.empty-state .codicon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
|
|
214
214
|
.empty-state p { font-size: 14px; max-width: 280px; }
|
|
215
|
+
|
|
216
|
+
/* =============================================================================
|
|
217
|
+
Mobile Bridge Customizations - Hide Follow and Revert buttons in snackbar
|
|
218
|
+
============================================================================= */
|
|
219
|
+
.kiro-snackbar-actions .kiro-button[data-variant="primary"] { display: none !important; }
|
|
215
220
|
</style>
|
|
216
221
|
</head>
|
|
217
222
|
<body>
|
|
@@ -707,14 +712,14 @@
|
|
|
707
712
|
}
|
|
708
713
|
|
|
709
714
|
/* Hide tooltips, popovers, and overlay elements */
|
|
715
|
+
/* IMPORTANT: Do NOT hide model-related elements - use negative lookahead patterns */
|
|
710
716
|
[role="tooltip"], [data-tooltip],
|
|
711
|
-
[class*="tooltip"]:not(button):not([role="button"]),
|
|
712
|
-
[class*="Tooltip"]:not(button):not([role="button"]),
|
|
713
|
-
[class*="popover"]:not(button):not([role="button"]),
|
|
714
|
-
[class*="Popover"]:not(button):not([role="button"]),
|
|
715
|
-
[class*="overlay"]:not(button):not([role="button"]):not([class*="dropdown"]),
|
|
716
|
-
[class*="Overlay"]:not(button):not([role="button"]):not([class*="dropdown"])
|
|
717
|
-
[class*="modal"], [class*="Modal"] {
|
|
717
|
+
[class*="tooltip"]:not(button):not([role="button"]):not([class*="model"]):not([class*="Model"]),
|
|
718
|
+
[class*="Tooltip"]:not(button):not([role="button"]):not([class*="model"]):not([class*="Model"]),
|
|
719
|
+
[class*="popover"]:not(button):not([role="button"]):not([class*="model"]):not([class*="Model"]):not([role="listbox"]):not([role="menu"]),
|
|
720
|
+
[class*="Popover"]:not(button):not([role="button"]):not([class*="model"]):not([class*="Model"]):not([role="listbox"]):not([role="menu"]),
|
|
721
|
+
[class*="overlay"]:not(button):not([role="button"]):not([class*="dropdown"]):not([class*="model"]):not([class*="Model"]),
|
|
722
|
+
[class*="Overlay"]:not(button):not([role="button"]):not([class*="dropdown"]):not([class*="model"]):not([class*="Model"]) {
|
|
718
723
|
display: none !important;
|
|
719
724
|
visibility: hidden !important;
|
|
720
725
|
opacity: 0 !important;
|
|
@@ -742,6 +747,7 @@
|
|
|
742
747
|
/* Dropdown menu items */
|
|
743
748
|
[class*="dropdown-item"], [class*="dropdownItem"], [class*="DropdownItem"],
|
|
744
749
|
[class*="model-option"], [class*="modelOption"], [class*="ModelOption"],
|
|
750
|
+
.kiro-dropdown-item,
|
|
745
751
|
[role="option"], [role="menuitem"] {
|
|
746
752
|
display: flex !important;
|
|
747
753
|
visibility: visible !important;
|
|
@@ -754,10 +760,33 @@
|
|
|
754
760
|
}
|
|
755
761
|
|
|
756
762
|
[class*="dropdown-item"]:hover, [class*="dropdownItem"]:hover,
|
|
763
|
+
.kiro-dropdown-item:hover,
|
|
757
764
|
[role="option"]:hover, [role="menuitem"]:hover {
|
|
758
765
|
background: rgba(255, 255, 255, 0.1) !important;
|
|
759
766
|
}
|
|
760
767
|
|
|
768
|
+
/* Hide model descriptions in dropdown - keep only name and credit */
|
|
769
|
+
.kiro-dropdown-item [class*="description"],
|
|
770
|
+
.kiro-dropdown-item [class*="Description"],
|
|
771
|
+
.kiro-dropdown-item [class*="subtitle"],
|
|
772
|
+
.kiro-dropdown-item [class*="Subtitle"],
|
|
773
|
+
.kiro-dropdown-item > div:last-child:not(:first-child),
|
|
774
|
+
.kiro-dropdown-menu [class*="description"],
|
|
775
|
+
.kiro-dropdown-menu [class*="Description"],
|
|
776
|
+
[class*="dropdown-item"] [class*="description"],
|
|
777
|
+
[class*="dropdown-item"] [class*="Description"],
|
|
778
|
+
[role="option"] [class*="description"],
|
|
779
|
+
[role="option"] [class*="Description"] {
|
|
780
|
+
display: none !important;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* Compact model dropdown items */
|
|
784
|
+
.kiro-dropdown-item,
|
|
785
|
+
.kiro-dropdown-menu > div {
|
|
786
|
+
padding: 8px 12px !important;
|
|
787
|
+
min-height: auto !important;
|
|
788
|
+
}
|
|
789
|
+
|
|
761
790
|
/* Model selector button */
|
|
762
791
|
[class*="model-selector"], [class*="modelSelector"], [class*="ModelSelector"],
|
|
763
792
|
[class*="model-dropdown"], [class*="modelDropdown"], [class*="ModelDropdown"],
|
|
@@ -1017,6 +1046,7 @@
|
|
|
1017
1046
|
|
|
1018
1047
|
removePlaceholderText();
|
|
1019
1048
|
fixContextWindowCircles();
|
|
1049
|
+
hideRevertButton();
|
|
1020
1050
|
|
|
1021
1051
|
requestAnimationFrame(() => {
|
|
1022
1052
|
requestAnimationFrame(() => {
|
|
@@ -1052,6 +1082,16 @@
|
|
|
1052
1082
|
makeInteractive();
|
|
1053
1083
|
}
|
|
1054
1084
|
|
|
1085
|
+
function hideRevertButton() {
|
|
1086
|
+
// Hide Revert button by finding buttons with "Revert" text in snackbar
|
|
1087
|
+
panels.chat.content.querySelectorAll('.kiro-snackbar button, .kiro-snackbar-actions button').forEach(btn => {
|
|
1088
|
+
const text = (btn.textContent || '').trim().toLowerCase();
|
|
1089
|
+
if (text === 'revert') {
|
|
1090
|
+
btn.style.display = 'none';
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1055
1095
|
function removePlaceholderText() {
|
|
1056
1096
|
const content = panels.chat.content;
|
|
1057
1097
|
content.querySelectorAll('[class*="placeholder"], [class*="Placeholder"]').forEach(el => {
|
|
@@ -1395,7 +1435,7 @@
|
|
|
1395
1435
|
html += `<div class="file-tree-folder" data-path="${escapeHtml(folderPath)}">
|
|
1396
1436
|
<div class="file-tree-folder-header" data-folder="${escapeHtml(folderPath)}">
|
|
1397
1437
|
<span class="file-tree-folder-icon codicon codicon-chevron-right ${isExpanded ? 'expanded' : ''}"></span>
|
|
1398
|
-
<span class="codicon codicon-folder${isExpanded ? '-opened' : ''}" style="color: #
|
|
1438
|
+
<span class="codicon codicon-folder${isExpanded ? '-opened' : ''}" style="color: #a78bfa;"></span>
|
|
1399
1439
|
<span class="file-tree-folder-name">${escapeHtml(folderName)}</span>
|
|
1400
1440
|
</div>
|
|
1401
1441
|
<div class="file-tree-folder-contents ${isExpanded ? 'expanded' : ''}">${renderTreeNode(node.folders[folderName], folderPath)}</div>
|
|
@@ -1561,10 +1601,67 @@
|
|
|
1561
1601
|
e.preventDefault(); e.stopPropagation();
|
|
1562
1602
|
const label = wrapper.querySelector('label');
|
|
1563
1603
|
await sendClickToKiro({ tag: 'div', text: label ? label.textContent.trim() : 'toggle', role: 'switch', isToggle: true });
|
|
1604
|
+
// Refresh snapshot after toggle to show updated state
|
|
1605
|
+
// Use longer delay (800ms) to allow server polling to capture the change
|
|
1606
|
+
// Then refresh again at 1500ms to ensure we have the latest state
|
|
1607
|
+
setTimeout(() => fetchChatSnapshot(selectedCascadeId), 800);
|
|
1608
|
+
setTimeout(() => fetchChatSnapshot(selectedCascadeId), 1500);
|
|
1564
1609
|
return false;
|
|
1565
1610
|
};
|
|
1566
1611
|
});
|
|
1567
1612
|
|
|
1613
|
+
// Notification banner buttons (View all, X) in snackbar and agent-outcome
|
|
1614
|
+
// Note: Revert and Follow buttons are hidden
|
|
1615
|
+
const notificationSelectors = [
|
|
1616
|
+
'.kiro-snackbar button',
|
|
1617
|
+
'.kiro-snackbar-actions button',
|
|
1618
|
+
'.kiro-snackbar-header button',
|
|
1619
|
+
'.agent-outcome-notification button',
|
|
1620
|
+
'.agent-outcome button',
|
|
1621
|
+
'[class*="notification"] button',
|
|
1622
|
+
'[class*="outcome"] button'
|
|
1623
|
+
];
|
|
1624
|
+
notificationSelectors.forEach(sel => {
|
|
1625
|
+
try {
|
|
1626
|
+
content.querySelectorAll(sel).forEach(btn => {
|
|
1627
|
+
const btnText = (btn.textContent || '').trim().toLowerCase();
|
|
1628
|
+
// Skip Follow and Revert buttons (hidden via CSS/JS)
|
|
1629
|
+
if (btnText.includes('follow') || btnText === 'revert') return;
|
|
1630
|
+
|
|
1631
|
+
btn.style.cursor = 'pointer';
|
|
1632
|
+
btn.onclick = async (e) => {
|
|
1633
|
+
e.preventDefault(); e.stopPropagation();
|
|
1634
|
+
|
|
1635
|
+
// Identify the button - for icon buttons use aria-label or detect close icon
|
|
1636
|
+
let buttonText = (btn.textContent || '').trim().substring(0, 50);
|
|
1637
|
+
const ariaLabel = btn.getAttribute('aria-label') || '';
|
|
1638
|
+
const isIconButton = btn.classList.contains('kiro-icon-button');
|
|
1639
|
+
const hasCloseIcon = btn.querySelector('.codicon-close, .codicon-x, [class*="close"]');
|
|
1640
|
+
|
|
1641
|
+
// For X/close icon buttons with no text
|
|
1642
|
+
if (!buttonText && (isIconButton || hasCloseIcon)) {
|
|
1643
|
+
buttonText = ariaLabel || 'dismiss';
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
const clickInfo = {
|
|
1647
|
+
tag: 'button',
|
|
1648
|
+
text: buttonText,
|
|
1649
|
+
ariaLabel: ariaLabel,
|
|
1650
|
+
role: btn.getAttribute('role') || 'button',
|
|
1651
|
+
className: btn.className || '',
|
|
1652
|
+
isNotificationButton: true,
|
|
1653
|
+
isIconButton: isIconButton || !buttonText
|
|
1654
|
+
};
|
|
1655
|
+
console.log('[Click] Notification button:', clickInfo.text || clickInfo.ariaLabel || 'icon-button');
|
|
1656
|
+
await sendClickToKiro(clickInfo);
|
|
1657
|
+
// Refresh after clicking notification buttons
|
|
1658
|
+
setTimeout(() => fetchChatSnapshot(selectedCascadeId), 500);
|
|
1659
|
+
return false;
|
|
1660
|
+
};
|
|
1661
|
+
});
|
|
1662
|
+
} catch(e) {}
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1568
1665
|
// Tabs
|
|
1569
1666
|
content.querySelectorAll('[role="tab"]').forEach(tab => {
|
|
1570
1667
|
const closeBtn = tab.querySelector('[aria-label="close"], [class*="close"]');
|
|
@@ -1590,6 +1687,118 @@
|
|
|
1590
1687
|
};
|
|
1591
1688
|
});
|
|
1592
1689
|
|
|
1690
|
+
// Model selector buttons and dropdown options
|
|
1691
|
+
const modelNames = ['claude', 'sonnet', 'opus', 'haiku', 'auto', 'gpt', 'model', 'gemini', 'llama'];
|
|
1692
|
+
const modelSelectorSelectors = [
|
|
1693
|
+
'[class*="model-selector"]', '[class*="modelSelector"]', '[class*="ModelSelector"]',
|
|
1694
|
+
'[class*="model-dropdown"]', '[class*="modelDropdown"]', '[class*="ModelDropdown"]',
|
|
1695
|
+
'[aria-label*="model" i]', '[aria-label*="Model"]',
|
|
1696
|
+
'[class*="select-model"]', '[class*="selectModel"]',
|
|
1697
|
+
'[role="combobox"]'
|
|
1698
|
+
];
|
|
1699
|
+
|
|
1700
|
+
// Handle model selector buttons
|
|
1701
|
+
modelSelectorSelectors.forEach(sel => {
|
|
1702
|
+
try {
|
|
1703
|
+
content.querySelectorAll(sel).forEach(el => {
|
|
1704
|
+
if (el.onclick) return;
|
|
1705
|
+
el.style.cursor = 'pointer';
|
|
1706
|
+
el.onclick = async (e) => {
|
|
1707
|
+
e.preventDefault(); e.stopPropagation();
|
|
1708
|
+
const clickInfo = {
|
|
1709
|
+
tag: el.tagName?.toLowerCase() || 'button',
|
|
1710
|
+
text: (el.textContent || '').trim().substring(0, 100),
|
|
1711
|
+
ariaLabel: el.getAttribute('aria-label') || '',
|
|
1712
|
+
role: el.getAttribute('role') || 'button',
|
|
1713
|
+
className: el.className || '',
|
|
1714
|
+
isModelSelector: true
|
|
1715
|
+
};
|
|
1716
|
+
await sendClickToKiro(clickInfo);
|
|
1717
|
+
// Refresh snapshot after clicking to show dropdown
|
|
1718
|
+
setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
|
|
1719
|
+
return false;
|
|
1720
|
+
};
|
|
1721
|
+
});
|
|
1722
|
+
} catch(e) {}
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
// Handle dropdown menu items (model options) - including Kiro-specific classes
|
|
1726
|
+
const dropdownItemSelectors = [
|
|
1727
|
+
'.kiro-dropdown-item',
|
|
1728
|
+
'.kiro-dropdown-menu > div',
|
|
1729
|
+
'[role="option"]', '[role="menuitem"]', '[role="listitem"]',
|
|
1730
|
+
'[class*="dropdown-item"]', '[class*="dropdownItem"]', '[class*="DropdownItem"]',
|
|
1731
|
+
'[class*="menu-item"]', '[class*="menuItem"]', '[class*="MenuItem"]'
|
|
1732
|
+
];
|
|
1733
|
+
|
|
1734
|
+
dropdownItemSelectors.forEach(sel => {
|
|
1735
|
+
try {
|
|
1736
|
+
content.querySelectorAll(sel).forEach(el => {
|
|
1737
|
+
if (el.onclick) return;
|
|
1738
|
+
// Skip if it's inside a tab
|
|
1739
|
+
if (el.closest('[role="tab"]')) return;
|
|
1740
|
+
|
|
1741
|
+
const elText = (el.textContent || '').toLowerCase();
|
|
1742
|
+
const isModelOption = modelNames.some(m => elText.includes(m));
|
|
1743
|
+
|
|
1744
|
+
// Only attach handler if it looks like a model option
|
|
1745
|
+
if (!isModelOption) return;
|
|
1746
|
+
|
|
1747
|
+
el.style.cursor = 'pointer';
|
|
1748
|
+
el.onclick = async (e) => {
|
|
1749
|
+
e.preventDefault(); e.stopPropagation();
|
|
1750
|
+
|
|
1751
|
+
// Extract just the model name for cleaner matching
|
|
1752
|
+
let modelText = (el.textContent || '').trim();
|
|
1753
|
+
// Try to get just the first line (model name)
|
|
1754
|
+
const firstLine = modelText.split('\n')[0].trim();
|
|
1755
|
+
if (firstLine.length > 5 && firstLine.length < 50) {
|
|
1756
|
+
modelText = firstLine;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const clickInfo = {
|
|
1760
|
+
tag: el.tagName?.toLowerCase() || 'div',
|
|
1761
|
+
text: modelText.substring(0, 50),
|
|
1762
|
+
ariaLabel: el.getAttribute('aria-label') || '',
|
|
1763
|
+
role: el.getAttribute('role') || 'option',
|
|
1764
|
+
className: el.className || '',
|
|
1765
|
+
isModelOption: true,
|
|
1766
|
+
isModelSelector: false
|
|
1767
|
+
};
|
|
1768
|
+
await sendClickToKiro(clickInfo);
|
|
1769
|
+
// Refresh snapshot after selecting option
|
|
1770
|
+
setTimeout(() => fetchChatSnapshot(selectedCascadeId), 500);
|
|
1771
|
+
return false;
|
|
1772
|
+
};
|
|
1773
|
+
});
|
|
1774
|
+
} catch(e) {}
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
// Also detect model-related buttons by text content
|
|
1778
|
+
content.querySelectorAll('button, [role="button"]').forEach(btn => {
|
|
1779
|
+
if (btn.onclick) return;
|
|
1780
|
+
const btnText = (btn.textContent || '').toLowerCase();
|
|
1781
|
+
const isModelRelated = modelNames.some(m => btnText.includes(m));
|
|
1782
|
+
|
|
1783
|
+
if (isModelRelated) {
|
|
1784
|
+
btn.style.cursor = 'pointer';
|
|
1785
|
+
btn.onclick = async (e) => {
|
|
1786
|
+
e.preventDefault(); e.stopPropagation();
|
|
1787
|
+
const clickInfo = {
|
|
1788
|
+
tag: 'button',
|
|
1789
|
+
text: (btn.textContent || '').trim().substring(0, 100),
|
|
1790
|
+
ariaLabel: btn.getAttribute('aria-label') || '',
|
|
1791
|
+
role: btn.getAttribute('role') || 'button',
|
|
1792
|
+
className: btn.className || '',
|
|
1793
|
+
isModelSelector: true
|
|
1794
|
+
};
|
|
1795
|
+
await sendClickToKiro(clickInfo);
|
|
1796
|
+
setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
|
|
1797
|
+
return false;
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1593
1802
|
// File links
|
|
1594
1803
|
const fileExtensions = /\.(ts|tsx|js|jsx|py|java|html|css|json|md|yaml|yml|xml|sql|go|rs|c|cpp|h|cs|rb|php|sh|vue|svelte)$/i;
|
|
1595
1804
|
content.querySelectorAll('a, code, span, [class*="file"], [class*="path"], [data-path]').forEach(el => {
|
|
@@ -1743,8 +1952,32 @@
|
|
|
1743
1952
|
// =============================================================================
|
|
1744
1953
|
// Kiro Communication
|
|
1745
1954
|
// =============================================================================
|
|
1955
|
+
|
|
1956
|
+
// Rate limiting for message sending
|
|
1957
|
+
let lastMessageTime = 0;
|
|
1958
|
+
const MESSAGE_RATE_LIMIT_MS = 1000; // Minimum 1 second between messages
|
|
1959
|
+
let pendingMessage = false;
|
|
1960
|
+
|
|
1746
1961
|
async function sendToKiro(message) {
|
|
1747
1962
|
if (!message || !selectedCascadeId) return;
|
|
1963
|
+
|
|
1964
|
+
// Rate limiting check
|
|
1965
|
+
const now = Date.now();
|
|
1966
|
+
if (now - lastMessageTime < MESSAGE_RATE_LIMIT_MS) {
|
|
1967
|
+
if (!pendingMessage) {
|
|
1968
|
+
showToast('Please wait before sending another message', 1500);
|
|
1969
|
+
}
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
if (pendingMessage) {
|
|
1974
|
+
showToast('Message already being sent', 1500);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
pendingMessage = true;
|
|
1979
|
+
lastMessageTime = now;
|
|
1980
|
+
|
|
1748
1981
|
try {
|
|
1749
1982
|
const r = await fetch(`/send/${selectedCascadeId}`, {
|
|
1750
1983
|
method: 'POST',
|
|
@@ -1756,10 +1989,12 @@
|
|
|
1756
1989
|
else showToast(result.error || 'Failed to send');
|
|
1757
1990
|
} catch (e) {
|
|
1758
1991
|
showToast('Failed to send');
|
|
1992
|
+
} finally {
|
|
1993
|
+
pendingMessage = false;
|
|
1759
1994
|
}
|
|
1760
1995
|
}
|
|
1761
1996
|
|
|
1762
|
-
async function sendClickToKiro(clickInfo) {
|
|
1997
|
+
async function sendClickToKiro(clickInfo, retryCount = 0) {
|
|
1763
1998
|
if (!selectedCascadeId) return;
|
|
1764
1999
|
|
|
1765
2000
|
const isNavigation = clickInfo.ariaLabel?.toLowerCase().includes('back') ||
|
|
@@ -1770,15 +2005,27 @@
|
|
|
1770
2005
|
if (isNavigation) navigationPending = true;
|
|
1771
2006
|
|
|
1772
2007
|
try {
|
|
1773
|
-
await fetch(`/click/${selectedCascadeId}`, {
|
|
2008
|
+
const response = await fetch(`/click/${selectedCascadeId}`, {
|
|
1774
2009
|
method: 'POST',
|
|
1775
2010
|
headers: { 'Content-Type': 'application/json' },
|
|
1776
2011
|
body: JSON.stringify(clickInfo)
|
|
1777
2012
|
});
|
|
1778
2013
|
|
|
2014
|
+
const result = await response.json();
|
|
2015
|
+
|
|
2016
|
+
// Handle retry for model option clicks (dropdown was opened, need to click option)
|
|
2017
|
+
if (result.needsRetry && clickInfo.isModelOption && retryCount < 2) {
|
|
2018
|
+
console.log('[Click] Dropdown opened, retrying option click in 350ms...');
|
|
2019
|
+
await new Promise(resolve => setTimeout(resolve, 350));
|
|
2020
|
+
return sendClickToKiro(clickInfo, retryCount + 1);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
1779
2023
|
if (isNavigation) setTimeout(() => fetchChatSnapshot(selectedCascadeId), 300);
|
|
2024
|
+
|
|
2025
|
+
return result;
|
|
1780
2026
|
} catch (e) {
|
|
1781
2027
|
navigationPending = false;
|
|
2028
|
+
return { success: false, error: e.message };
|
|
1782
2029
|
}
|
|
1783
2030
|
}
|
|
1784
2031
|
|