kiro-mobile-bridge 1.0.8 → 1.0.10

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 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
- - 📝 **Editor Panel** - Browse file explorer and open file
12
- - 🔄 Real-time updates via WebSocket
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 chat
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 HTML with adaptive polling (1s active → 3s idle), broadcasts changes
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiro-mobile-bridge",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
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",
@@ -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: #888; font-size: 14px; transition: transform 0.15s; }
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: #dcb67a;"></span>
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