@cluesmith/codev 1.4.1 → 1.4.2

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.
@@ -651,6 +651,156 @@
651
651
  color: #ef4444;
652
652
  }
653
653
 
654
+ /* Activity Summary Modal (Spec 0059) */
655
+ .activity-dialog {
656
+ width: 600px;
657
+ max-width: 90vw;
658
+ max-height: 80vh;
659
+ display: flex;
660
+ flex-direction: column;
661
+ }
662
+
663
+ .activity-dialog-header {
664
+ display: flex;
665
+ justify-content: space-between;
666
+ align-items: center;
667
+ margin-bottom: 16px;
668
+ }
669
+
670
+ .activity-dialog-header h3 {
671
+ margin: 0;
672
+ }
673
+
674
+ .activity-close-btn {
675
+ background: none;
676
+ border: none;
677
+ font-size: 24px;
678
+ color: var(--text-muted);
679
+ cursor: pointer;
680
+ padding: 0 8px;
681
+ line-height: 1;
682
+ }
683
+
684
+ .activity-close-btn:hover {
685
+ color: var(--text-primary);
686
+ }
687
+
688
+ .activity-dialog-content {
689
+ flex: 1;
690
+ overflow-y: auto;
691
+ max-height: 50vh;
692
+ margin-bottom: 16px;
693
+ }
694
+
695
+ .activity-loading {
696
+ display: flex;
697
+ align-items: center;
698
+ justify-content: center;
699
+ gap: 12px;
700
+ padding: 40px 20px;
701
+ color: var(--text-muted);
702
+ }
703
+
704
+ .activity-spinner {
705
+ width: 20px;
706
+ height: 20px;
707
+ border: 2px solid var(--border);
708
+ border-top-color: var(--accent);
709
+ border-radius: 50%;
710
+ animation: spin 1s linear infinite;
711
+ }
712
+
713
+ @keyframes spin {
714
+ to { transform: rotate(360deg); }
715
+ }
716
+
717
+ .activity-empty {
718
+ text-align: center;
719
+ padding: 40px 20px;
720
+ color: var(--text-muted);
721
+ }
722
+
723
+ .activity-error {
724
+ text-align: center;
725
+ padding: 40px 20px;
726
+ color: #ef4444;
727
+ }
728
+
729
+ .activity-summary {
730
+ line-height: 1.6;
731
+ }
732
+
733
+ .activity-ai-summary {
734
+ background: var(--bg-tertiary);
735
+ border-left: 3px solid var(--accent);
736
+ padding: 12px 16px;
737
+ margin-bottom: 20px;
738
+ font-style: italic;
739
+ color: var(--text-secondary);
740
+ }
741
+
742
+ .activity-section {
743
+ margin-bottom: 16px;
744
+ }
745
+
746
+ .activity-section h4 {
747
+ font-size: 13px;
748
+ text-transform: uppercase;
749
+ color: var(--text-muted);
750
+ margin: 0 0 8px 0;
751
+ letter-spacing: 0.5px;
752
+ }
753
+
754
+ .activity-section ul {
755
+ margin: 0;
756
+ padding-left: 20px;
757
+ color: var(--text-secondary);
758
+ }
759
+
760
+ .activity-section li {
761
+ margin-bottom: 4px;
762
+ }
763
+
764
+ .activity-section p {
765
+ margin: 4px 0;
766
+ color: var(--text-secondary);
767
+ }
768
+
769
+ .activity-time-value {
770
+ font-size: 18px;
771
+ font-weight: 500;
772
+ color: var(--text-primary);
773
+ }
774
+
775
+ .activity-dialog-footer {
776
+ display: flex;
777
+ justify-content: flex-end;
778
+ gap: 8px;
779
+ padding-top: 12px;
780
+ border-top: 1px solid var(--border);
781
+ }
782
+
783
+ /* Activity Tab Styles (Spec 0059) */
784
+ .activity-tab-container {
785
+ padding: 24px;
786
+ max-width: 700px;
787
+ margin: 0 auto;
788
+ }
789
+
790
+ .activity-tab-container .activity-summary {
791
+ background: var(--bg-secondary);
792
+ border-radius: 8px;
793
+ padding: 20px;
794
+ }
795
+
796
+ .activity-tab-container .activity-actions {
797
+ margin-top: 20px;
798
+ padding-top: 16px;
799
+ border-top: 1px solid var(--border);
800
+ display: flex;
801
+ justify-content: flex-end;
802
+ }
803
+
654
804
  /* Projects Tab Styles (Spec 0045) */
655
805
  .projects-container {
656
806
  flex: 1;
@@ -1476,6 +1626,178 @@
1476
1626
  font-size: 12px;
1477
1627
  }
1478
1628
 
1629
+ /* File search styles (Spec 0058) */
1630
+ .files-search-container {
1631
+ display: flex;
1632
+ align-items: center;
1633
+ padding: 6px 8px;
1634
+ gap: 6px;
1635
+ border-bottom: 1px solid var(--border);
1636
+ }
1637
+
1638
+ .files-search-input {
1639
+ flex: 1;
1640
+ background: var(--bg-tertiary);
1641
+ border: 1px solid var(--border);
1642
+ border-radius: 4px;
1643
+ padding: 6px 10px;
1644
+ font-size: 12px;
1645
+ color: var(--text-primary);
1646
+ outline: none;
1647
+ }
1648
+
1649
+ .files-search-input:focus {
1650
+ border-color: var(--accent);
1651
+ }
1652
+
1653
+ .files-search-input::placeholder {
1654
+ color: var(--text-muted);
1655
+ }
1656
+
1657
+ .files-search-clear {
1658
+ background: transparent;
1659
+ border: none;
1660
+ color: var(--text-muted);
1661
+ cursor: pointer;
1662
+ font-size: 14px;
1663
+ padding: 2px 6px;
1664
+ border-radius: 4px;
1665
+ line-height: 1;
1666
+ }
1667
+
1668
+ .files-search-clear:hover {
1669
+ color: var(--text-primary);
1670
+ background: var(--bg-tertiary);
1671
+ }
1672
+
1673
+ .files-search-clear.hidden {
1674
+ display: none;
1675
+ }
1676
+
1677
+ .files-search-results {
1678
+ flex: 1;
1679
+ overflow-y: auto;
1680
+ }
1681
+
1682
+ .files-search-result {
1683
+ padding: 6px 12px;
1684
+ cursor: pointer;
1685
+ display: flex;
1686
+ flex-direction: column;
1687
+ gap: 2px;
1688
+ }
1689
+
1690
+ .files-search-result:hover,
1691
+ .files-search-result.selected {
1692
+ background: var(--bg-tertiary);
1693
+ }
1694
+
1695
+ .files-search-result-name {
1696
+ font-size: 12px;
1697
+ color: var(--text-primary);
1698
+ }
1699
+
1700
+ .files-search-result-path {
1701
+ font-size: 11px;
1702
+ color: var(--text-muted);
1703
+ overflow: hidden;
1704
+ text-overflow: ellipsis;
1705
+ white-space: nowrap;
1706
+ }
1707
+
1708
+ .files-search-highlight {
1709
+ color: var(--accent);
1710
+ font-weight: 500;
1711
+ }
1712
+
1713
+ /* Cmd+P Palette styles (Spec 0058) */
1714
+ .file-palette {
1715
+ position: fixed;
1716
+ inset: 0;
1717
+ z-index: 1000;
1718
+ display: flex;
1719
+ justify-content: center;
1720
+ padding-top: 80px;
1721
+ }
1722
+
1723
+ .file-palette.hidden {
1724
+ display: none;
1725
+ }
1726
+
1727
+ .file-palette-backdrop {
1728
+ position: absolute;
1729
+ inset: 0;
1730
+ background: rgba(0, 0, 0, 0.5);
1731
+ }
1732
+
1733
+ .file-palette-container {
1734
+ position: relative;
1735
+ width: 500px;
1736
+ max-width: 90vw;
1737
+ max-height: 450px;
1738
+ background: var(--bg-secondary);
1739
+ border: 1px solid var(--border);
1740
+ border-radius: 8px;
1741
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1742
+ display: flex;
1743
+ flex-direction: column;
1744
+ overflow: hidden;
1745
+ }
1746
+
1747
+ .file-palette-input {
1748
+ width: 100%;
1749
+ padding: 14px 16px;
1750
+ background: var(--bg-tertiary);
1751
+ border: none;
1752
+ border-bottom: 1px solid var(--border);
1753
+ font-size: 14px;
1754
+ color: var(--text-primary);
1755
+ outline: none;
1756
+ }
1757
+
1758
+ .file-palette-input::placeholder {
1759
+ color: var(--text-muted);
1760
+ }
1761
+
1762
+ .file-palette-results {
1763
+ flex: 1;
1764
+ overflow-y: auto;
1765
+ max-height: 380px;
1766
+ }
1767
+
1768
+ .file-palette-result {
1769
+ padding: 10px 16px;
1770
+ cursor: pointer;
1771
+ display: flex;
1772
+ flex-direction: column;
1773
+ gap: 2px;
1774
+ }
1775
+
1776
+ .file-palette-result:hover,
1777
+ .file-palette-result.selected {
1778
+ background: var(--bg-tertiary);
1779
+ }
1780
+
1781
+ .file-palette-result-name {
1782
+ font-size: 13px;
1783
+ color: var(--text-primary);
1784
+ }
1785
+
1786
+ .file-palette-result-path {
1787
+ font-size: 12px;
1788
+ color: var(--text-muted);
1789
+ overflow: hidden;
1790
+ text-overflow: ellipsis;
1791
+ white-space: nowrap;
1792
+ }
1793
+
1794
+ .file-palette-empty {
1795
+ padding: 16px;
1796
+ text-align: center;
1797
+ color: var(--text-muted);
1798
+ font-size: 13px;
1799
+ }
1800
+
1479
1801
  .dashboard-empty-state {
1480
1802
  color: var(--text-muted);
1481
1803
  font-size: 13px;
@@ -1518,6 +1840,11 @@
1518
1840
  <body>
1519
1841
  <header class="header">
1520
1842
  <h1>Agent Farm - {{PROJECT_NAME}}</h1>
1843
+ <div class="header-actions">
1844
+ <button class="btn activity-summary-btn" onclick="showActivitySummary()" title="What did I do today?">
1845
+ 🕐 Today
1846
+ </button>
1847
+ </div>
1521
1848
  </header>
1522
1849
 
1523
1850
  <main class="main">
@@ -1539,10 +1866,6 @@
1539
1866
  <span class="overflow-count" id="overflow-count">+0</span>
1540
1867
  </button>
1541
1868
  <div class="overflow-menu hidden" id="overflow-menu" role="menu"></div>
1542
- <div class="add-buttons">
1543
- <button class="add-btn" onclick="spawnBuilder()" title="Spawn worktree builder">+ 🔨</button>
1544
- <button class="add-btn" onclick="spawnShell()" title="New shell">+ >_</button>
1545
- </div>
1546
1869
  </div>
1547
1870
  <div class="tab-content" id="tab-content"></div>
1548
1871
  </div>
@@ -1596,6 +1919,7 @@
1596
1919
  <!-- Context menu -->
1597
1920
  <div class="context-menu hidden" id="context-menu" role="menu">
1598
1921
  <div class="context-menu-item" role="menuitem" tabindex="0" data-action="openContextTab" onclick="openContextTab()" onkeydown="handleContextMenuKeydown(event)">Open in New Tab</div>
1922
+ <div class="context-menu-item" role="menuitem" tabindex="-1" data-action="reloadContextTab" id="context-reload" onclick="reloadContextTab()" onkeydown="handleContextMenuKeydown(event)">Reload</div>
1599
1923
  <div class="context-menu-item" role="menuitem" tabindex="-1" data-action="closeActiveTab" onclick="closeActiveTab()" onkeydown="handleContextMenuKeydown(event)">Close</div>
1600
1924
  <div class="context-menu-item" role="menuitem" tabindex="-1" data-action="closeOtherTabs" onclick="closeOtherTabs()" onkeydown="handleContextMenuKeydown(event)">Close Others</div>
1601
1925
  <div class="context-menu-item danger" role="menuitem" tabindex="-1" data-action="closeAllTabs" onclick="closeAllTabs()" onkeydown="handleContextMenuKeydown(event)">Close All</div>
@@ -1604,6 +1928,40 @@
1604
1928
  <!-- Toast container -->
1605
1929
  <div class="toast-container" id="toast-container"></div>
1606
1930
 
1931
+ <!-- Activity Summary Modal (Spec 0059) -->
1932
+ <div class="dialog-overlay hidden" id="activity-modal">
1933
+ <div class="dialog activity-dialog">
1934
+ <div class="activity-dialog-header">
1935
+ <h3>Today's Summary</h3>
1936
+ <button class="activity-close-btn" onclick="closeActivityModal()" title="Close (Esc)">×</button>
1937
+ </div>
1938
+ <div class="activity-dialog-content" id="activity-content">
1939
+ <div class="activity-loading">
1940
+ <span class="activity-spinner"></span>
1941
+ Loading activity...
1942
+ </div>
1943
+ </div>
1944
+ <div class="activity-dialog-footer">
1945
+ <button class="btn" onclick="copyActivitySummary()">📋 Copy to Clipboard</button>
1946
+ <button class="btn" onclick="closeActivityModal()">Close</button>
1947
+ </div>
1948
+ </div>
1949
+ </div>
1950
+
1951
+ <!-- File search palette (Cmd+P) - Spec 0058 -->
1952
+ <div id="file-palette" class="file-palette hidden">
1953
+ <div class="file-palette-backdrop" onclick="closePalette()"></div>
1954
+ <div class="file-palette-container">
1955
+ <input type="text"
1956
+ id="palette-input"
1957
+ class="file-palette-input"
1958
+ placeholder="Search files by name..."
1959
+ oninput="onPaletteInput(this.value)"
1960
+ onkeydown="onPaletteKeydown(event)" />
1961
+ <div id="palette-results" class="file-palette-results"></div>
1962
+ </div>
1963
+ </div>
1964
+
1607
1965
  <script>
1608
1966
  // STATE_INJECTION_POINT
1609
1967
 
@@ -1709,8 +2067,9 @@
1709
2067
  // Check if file is already open
1710
2068
  const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
1711
2069
  if (existingTab) {
1712
- // Just switch to the existing tab
2070
+ // Switch to the existing tab and refresh content
1713
2071
  selectTab(existingTab.id);
2072
+ refreshFileTab(existingTab.id); // Refresh content in case file changed
1714
2073
  showToast(`Switched to ${getFileName(filePath)}`, 'success');
1715
2074
  // TODO: scroll to line if lineNumber provided
1716
2075
  return;
@@ -1760,9 +2119,25 @@
1760
2119
  let filesTreeError = null;
1761
2120
  let filesTreeLoaded = false;
1762
2121
 
2122
+ // File search state (Spec 0058)
2123
+ let filesTreeFlat = []; // Flattened array of {name, path} objects for searching
2124
+ let filesSearchQuery = '';
2125
+ let filesSearchResults = [];
2126
+ let filesSearchIndex = 0;
2127
+ let filesSearchDebounceTimer = null;
2128
+
2129
+ // Cmd+P palette state (Spec 0058)
2130
+ let paletteOpen = false;
2131
+ let paletteQuery = '';
2132
+ let paletteResults = [];
2133
+ let paletteIndex = 0;
2134
+ let paletteDebounceTimer = null;
2135
+
1763
2136
  // Build tabs from initial state
1764
2137
  function buildTabsFromState() {
1765
2138
  const previousTabIds = new Set(tabs.map(t => t.id));
2139
+ // Preserve client-side-only tabs (like activity)
2140
+ const clientSideTabs = tabs.filter(t => t.type === 'activity');
1766
2141
  tabs = [];
1767
2142
 
1768
2143
  // Dashboard tab is ALWAYS first and uncloseable (Spec 0045, 0057)
@@ -1808,6 +2183,11 @@
1808
2183
  });
1809
2184
  }
1810
2185
 
2186
+ // Re-add preserved client-side tabs
2187
+ for (const tab of clientSideTabs) {
2188
+ tabs.push(tab);
2189
+ }
2190
+
1811
2191
  // Detect new tabs and auto-switch to them (skip projects tab)
1812
2192
  for (const tab of tabs) {
1813
2193
  if (tab.id !== 'dashboard' && tab.id !== 'files' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
@@ -1826,10 +2206,13 @@
1826
2206
  }
1827
2207
  }
1828
2208
 
1829
- // Get filename from path
2209
+ // Get filename from path (includes parent dir for context)
1830
2210
  function getFileName(path) {
1831
- const parts = path.split('/');
1832
- return parts[parts.length - 1];
2211
+ const parts = path.split('/').filter(p => p);
2212
+ if (parts.length >= 2) {
2213
+ return parts.slice(-2).join('/');
2214
+ }
2215
+ return parts[parts.length - 1] || path;
1833
2216
  }
1834
2217
 
1835
2218
  // Track current architect port to avoid re-rendering iframe unnecessarily
@@ -2013,6 +2396,16 @@
2013
2396
  return;
2014
2397
  }
2015
2398
 
2399
+ // Handle activity tab specially (no iframe, inline content)
2400
+ if (tab.type === 'activity') {
2401
+ if (currentTabType !== 'activity') {
2402
+ currentTabType = 'activity';
2403
+ currentTabPort = null;
2404
+ renderActivityTab();
2405
+ }
2406
+ return;
2407
+ }
2408
+
2016
2409
  // For other tabs, only update iframe if port changed (avoid flashing on poll)
2017
2410
  if (currentTabPort !== tab.port || currentTabType !== tab.type) {
2018
2411
  currentTabPort = tab.port;
@@ -2021,6 +2414,22 @@
2021
2414
  }
2022
2415
  }
2023
2416
 
2417
+ // Force refresh the iframe for a file tab (reloads content from server)
2418
+ function refreshFileTab(tabId) {
2419
+ const tab = tabs.find(t => t.id === tabId);
2420
+ if (!tab || tab.type !== 'file' || !tab.port) return;
2421
+
2422
+ // If this tab is currently active, force iframe reload
2423
+ if (activeTabId === tabId) {
2424
+ const content = document.getElementById('tab-content');
2425
+ const iframe = content.querySelector('iframe');
2426
+ if (iframe) {
2427
+ // Add cache-busting query param to force reload
2428
+ iframe.src = `http://localhost:${tab.port}?t=${Date.now()}`;
2429
+ }
2430
+ }
2431
+ }
2432
+
2024
2433
  // Update status bar
2025
2434
  function updateStatusBar() {
2026
2435
  // Architect status
@@ -2299,6 +2708,13 @@
2299
2708
  menu.style.top = event.clientY + 'px';
2300
2709
  menu.classList.remove('hidden');
2301
2710
 
2711
+ // Show/hide reload option based on tab type
2712
+ const tab = tabs.find(t => t.id === tabId);
2713
+ const reloadItem = document.getElementById('context-reload');
2714
+ if (reloadItem) {
2715
+ reloadItem.style.display = (tab && tab.type === 'file') ? 'block' : 'none';
2716
+ }
2717
+
2302
2718
  // Focus first item for keyboard navigation
2303
2719
  const firstItem = menu.querySelector('.context-menu-item');
2304
2720
  if (firstItem) firstItem.focus();
@@ -2309,6 +2725,15 @@
2309
2725
  }, 0);
2310
2726
  }
2311
2727
 
2728
+ // Reload file tab content
2729
+ function reloadContextTab() {
2730
+ if (contextMenuTabId) {
2731
+ refreshFileTab(contextMenuTabId);
2732
+ showToast('Reloaded', 'success');
2733
+ }
2734
+ hideContextMenu();
2735
+ }
2736
+
2312
2737
  function hideContextMenu() {
2313
2738
  document.getElementById('context-menu').classList.add('hidden');
2314
2739
  contextMenuTabId = null;
@@ -2573,6 +2998,11 @@
2573
2998
  hideCloseDialog();
2574
2999
  hideContextMenu();
2575
3000
  hideOverflowMenu();
3001
+ // Activity modal (Spec 0059)
3002
+ const activityModal = document.getElementById('activity-modal');
3003
+ if (activityModal && !activityModal.classList.contains('hidden')) {
3004
+ closeActivityModal();
3005
+ }
2576
3006
  }
2577
3007
 
2578
3008
  // Enter in dialogs
@@ -3211,11 +3641,61 @@
3211
3641
  filesTreeData = await response.json();
3212
3642
  filesTreeError = null;
3213
3643
  filesTreeLoaded = true;
3644
+ // Flatten tree for search (Spec 0058)
3645
+ filesTreeFlat = flattenFilesTree(filesTreeData);
3214
3646
  } catch (err) {
3215
3647
  console.error('Failed to load files tree:', err);
3216
3648
  filesTreeError = 'Could not load file tree: ' + err.message;
3217
3649
  filesTreeData = [];
3650
+ filesTreeFlat = [];
3651
+ }
3652
+ }
3653
+
3654
+ // Flatten the file tree into a searchable array (Spec 0058)
3655
+ function flattenFilesTree(nodes, result = []) {
3656
+ for (const node of nodes) {
3657
+ if (node.type === 'file') {
3658
+ result.push({ name: node.name, path: node.path });
3659
+ } else if (node.children) {
3660
+ flattenFilesTree(node.children, result);
3661
+ }
3218
3662
  }
3663
+ return result;
3664
+ }
3665
+
3666
+ // Search files with relevance sorting (Spec 0058)
3667
+ function searchFiles(query) {
3668
+ if (!query) return [];
3669
+ const q = query.toLowerCase();
3670
+
3671
+ const matches = filesTreeFlat.filter(f =>
3672
+ f.path.toLowerCase().includes(q)
3673
+ );
3674
+
3675
+ // Sort by relevance: exact filename > filename prefix > filename contains > path
3676
+ matches.sort((a, b) => {
3677
+ const aName = a.name.toLowerCase();
3678
+ const bName = b.name.toLowerCase();
3679
+ const aPath = a.path.toLowerCase();
3680
+ const bPath = b.path.toLowerCase();
3681
+
3682
+ // Exact filename match first
3683
+ if (aName === q && bName !== q) return -1;
3684
+ if (bName === q && aName !== q) return 1;
3685
+
3686
+ // Filename starts with query
3687
+ if (aName.startsWith(q) && !bName.startsWith(q)) return -1;
3688
+ if (bName.startsWith(q) && !aName.startsWith(q)) return 1;
3689
+
3690
+ // Filename contains query
3691
+ if (aName.includes(q) && !bName.includes(q)) return -1;
3692
+ if (bName.includes(q) && !aName.includes(q)) return 1;
3693
+
3694
+ // Alphabetical by path
3695
+ return aPath.localeCompare(bPath);
3696
+ });
3697
+
3698
+ return matches.slice(0, 15);
3219
3699
  }
3220
3700
 
3221
3701
  // Escape a string for use inside a JavaScript string literal in onclick handlers
@@ -3303,14 +3783,273 @@
3303
3783
  // Re-render file browser in current context (dashboard or files tab)
3304
3784
  function rerenderFilesBrowser() {
3305
3785
  if (activeTabId === 'dashboard') {
3306
- // Re-render just the files column in dashboard
3307
- const filesListEl = document.getElementById('dashboard-files-list');
3308
- if (filesListEl) {
3309
- filesListEl.innerHTML = renderDashboardFilesBrowser();
3786
+ // Re-render just the files content in dashboard
3787
+ const filesContentEl = document.getElementById('dashboard-files-content');
3788
+ if (filesContentEl) {
3789
+ filesContentEl.innerHTML = filesSearchQuery
3790
+ ? renderFilesSearchResults()
3791
+ : renderDashboardFilesBrowserWithWrapper();
3792
+ }
3793
+ }
3794
+ }
3795
+
3796
+ // Wrapper for file browser that includes the list element ID (Spec 0058)
3797
+ function renderDashboardFilesBrowserWithWrapper() {
3798
+ return `<div class="dashboard-files-list" id="dashboard-files-list">${renderDashboardFilesBrowser()}</div>`;
3799
+ }
3800
+
3801
+ // ========================================
3802
+ // File Search Functions (Spec 0058)
3803
+ // ========================================
3804
+
3805
+ // Debounced search input handler for Files column
3806
+ function onFilesSearchInput(value) {
3807
+ clearTimeout(filesSearchDebounceTimer);
3808
+ filesSearchDebounceTimer = setTimeout(() => {
3809
+ filesSearchQuery = value;
3810
+ filesSearchResults = searchFiles(value);
3811
+ filesSearchIndex = 0;
3812
+ rerenderFilesSearch();
3813
+ }, 100);
3814
+ }
3815
+
3816
+ // Clear files search and restore tree view
3817
+ function clearFilesSearch() {
3818
+ filesSearchQuery = '';
3819
+ filesSearchResults = [];
3820
+ filesSearchIndex = 0;
3821
+ const input = document.getElementById('files-search-input');
3822
+ if (input) {
3823
+ input.value = '';
3824
+ }
3825
+ rerenderFilesSearch();
3826
+ }
3827
+
3828
+ // Re-render the files search area (results or tree)
3829
+ function rerenderFilesSearch() {
3830
+ const filesContentEl = document.getElementById('dashboard-files-content');
3831
+ if (filesContentEl) {
3832
+ filesContentEl.innerHTML = filesSearchQuery
3833
+ ? renderFilesSearchResults()
3834
+ : renderDashboardFilesBrowserWithWrapper();
3835
+ }
3836
+ // Update clear button visibility
3837
+ const clearBtn = document.querySelector('.files-search-clear');
3838
+ if (clearBtn) {
3839
+ clearBtn.classList.toggle('hidden', !filesSearchQuery);
3840
+ }
3841
+ }
3842
+
3843
+ // Render search results for Files column
3844
+ function renderFilesSearchResults() {
3845
+ if (!filesSearchResults.length) {
3846
+ return '<div class="dashboard-empty-state">No files found</div>';
3847
+ }
3848
+
3849
+ return `<div class="files-search-results">${filesSearchResults.map((file, index) =>
3850
+ renderSearchResult(file, index, index === filesSearchIndex, filesSearchQuery, 'files')
3851
+ ).join('')}</div>`;
3852
+ }
3853
+
3854
+ // Highlight matching text in search results
3855
+ function highlightMatch(text, query) {
3856
+ if (!query) return escapeHtml(text);
3857
+ const q = query.toLowerCase();
3858
+ const t = text.toLowerCase();
3859
+ const idx = t.indexOf(q);
3860
+ if (idx === -1) return escapeHtml(text);
3861
+
3862
+ return escapeHtml(text.substring(0, idx)) +
3863
+ '<span class="files-search-highlight">' + escapeHtml(text.substring(idx, idx + query.length)) + '</span>' +
3864
+ escapeHtml(text.substring(idx + query.length));
3865
+ }
3866
+
3867
+ // Render a single search result (shared by Files column and palette)
3868
+ function renderSearchResult(file, index, isSelected, query, context) {
3869
+ const classPrefix = context === 'palette' ? 'file-palette' : 'files-search';
3870
+ const jsPath = escapeJsString(file.path);
3871
+
3872
+ return `
3873
+ <div class="${classPrefix}-result ${isSelected ? 'selected' : ''}"
3874
+ data-index="${index}"
3875
+ onclick="openFileFromSearch('${jsPath}', '${context}')">
3876
+ <div class="${classPrefix}-result-name">${highlightMatch(file.name, query)}</div>
3877
+ <div class="${classPrefix}-result-path">${highlightMatch(file.path, query)}</div>
3878
+ </div>
3879
+ `;
3880
+ }
3881
+
3882
+ // Keyboard handler for Files search input
3883
+ function onFilesSearchKeydown(event) {
3884
+ if (!filesSearchResults.length) {
3885
+ if (event.key === 'Escape') {
3886
+ clearFilesSearch();
3887
+ event.target.blur();
3888
+ }
3889
+ return;
3890
+ }
3891
+
3892
+ if (event.key === 'ArrowDown') {
3893
+ event.preventDefault();
3894
+ filesSearchIndex = Math.min(filesSearchIndex + 1, filesSearchResults.length - 1);
3895
+ rerenderFilesSearch();
3896
+ scrollSelectedIntoView('files');
3897
+ } else if (event.key === 'ArrowUp') {
3898
+ event.preventDefault();
3899
+ filesSearchIndex = Math.max(filesSearchIndex - 1, 0);
3900
+ rerenderFilesSearch();
3901
+ scrollSelectedIntoView('files');
3902
+ } else if (event.key === 'Enter') {
3903
+ event.preventDefault();
3904
+ if (filesSearchResults[filesSearchIndex]) {
3905
+ openFileFromSearch(filesSearchResults[filesSearchIndex].path, 'files');
3906
+ }
3907
+ } else if (event.key === 'Escape') {
3908
+ clearFilesSearch();
3909
+ event.target.blur();
3910
+ }
3911
+ }
3912
+
3913
+ // Scroll selected result into view
3914
+ function scrollSelectedIntoView(context) {
3915
+ const selector = context === 'palette'
3916
+ ? '.file-palette-result.selected'
3917
+ : '.files-search-result.selected';
3918
+ const selected = document.querySelector(selector);
3919
+ if (selected) {
3920
+ selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3921
+ }
3922
+ }
3923
+
3924
+ // Open file from search result (shared by Files column and palette)
3925
+ function openFileFromSearch(filePath, context) {
3926
+ // Check if file is already open
3927
+ const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
3928
+ if (existingTab) {
3929
+ selectTab(existingTab.id);
3930
+ refreshFileTab(existingTab.id); // Refresh content in case file changed
3931
+ } else {
3932
+ openFileFromTree(filePath);
3933
+ }
3934
+
3935
+ // Clear search / close palette
3936
+ if (context === 'palette') {
3937
+ closePalette();
3938
+ } else {
3939
+ clearFilesSearch();
3940
+ }
3941
+ }
3942
+
3943
+ // ========================================
3944
+ // Cmd+P Palette Functions (Spec 0058)
3945
+ // ========================================
3946
+
3947
+ // Open the file search palette
3948
+ function openPalette() {
3949
+ paletteOpen = true;
3950
+ paletteQuery = '';
3951
+ paletteResults = [];
3952
+ paletteIndex = 0;
3953
+ document.getElementById('file-palette').classList.remove('hidden');
3954
+ const input = document.getElementById('palette-input');
3955
+ input.value = '';
3956
+ input.focus();
3957
+ rerenderPaletteResults();
3958
+ }
3959
+
3960
+ // Close the file search palette
3961
+ function closePalette() {
3962
+ paletteOpen = false;
3963
+ paletteQuery = '';
3964
+ paletteResults = [];
3965
+ paletteIndex = 0;
3966
+ document.getElementById('file-palette').classList.add('hidden');
3967
+ }
3968
+
3969
+ // Debounced palette input handler
3970
+ function onPaletteInput(value) {
3971
+ clearTimeout(paletteDebounceTimer);
3972
+ paletteDebounceTimer = setTimeout(() => {
3973
+ paletteQuery = value;
3974
+ paletteResults = searchFiles(value);
3975
+ paletteIndex = 0;
3976
+ rerenderPaletteResults();
3977
+ }, 100);
3978
+ }
3979
+
3980
+ // Re-render palette results
3981
+ function rerenderPaletteResults() {
3982
+ const resultsEl = document.getElementById('palette-results');
3983
+ if (!resultsEl) return;
3984
+
3985
+ if (!paletteQuery) {
3986
+ resultsEl.innerHTML = '<div class="file-palette-empty">Type to search files...</div>';
3987
+ return;
3988
+ }
3989
+
3990
+ if (!paletteResults.length) {
3991
+ resultsEl.innerHTML = '<div class="file-palette-empty">No files found</div>';
3992
+ return;
3993
+ }
3994
+
3995
+ resultsEl.innerHTML = paletteResults.map((file, index) =>
3996
+ renderSearchResult(file, index, index === paletteIndex, paletteQuery, 'palette')
3997
+ ).join('');
3998
+ }
3999
+
4000
+ // Keyboard handler for palette input
4001
+ function onPaletteKeydown(event) {
4002
+ if (event.key === 'Escape') {
4003
+ closePalette();
4004
+ return;
4005
+ }
4006
+
4007
+ if (!paletteResults.length) return;
4008
+
4009
+ if (event.key === 'ArrowDown') {
4010
+ event.preventDefault();
4011
+ paletteIndex = Math.min(paletteIndex + 1, paletteResults.length - 1);
4012
+ rerenderPaletteResults();
4013
+ scrollSelectedIntoView('palette');
4014
+ } else if (event.key === 'ArrowUp') {
4015
+ event.preventDefault();
4016
+ paletteIndex = Math.max(paletteIndex - 1, 0);
4017
+ rerenderPaletteResults();
4018
+ scrollSelectedIntoView('palette');
4019
+ } else if (event.key === 'Enter') {
4020
+ event.preventDefault();
4021
+ if (paletteResults[paletteIndex]) {
4022
+ openFileFromSearch(paletteResults[paletteIndex].path, 'palette');
3310
4023
  }
3311
4024
  }
3312
4025
  }
3313
4026
 
4027
+ // Global keyboard handler for Cmd+P / Ctrl+P and Escape
4028
+ document.addEventListener('keydown', (e) => {
4029
+ // Global Escape handler for palette (works even if input loses focus)
4030
+ if (e.key === 'Escape' && paletteOpen) {
4031
+ closePalette();
4032
+ return;
4033
+ }
4034
+
4035
+ // Cmd+P (macOS) or Ctrl+P (Windows/Linux)
4036
+ if ((e.metaKey || e.ctrlKey) && e.key === 'p') {
4037
+ // Skip if user is typing in an input/textarea (except our search inputs)
4038
+ const active = document.activeElement;
4039
+ const isOurInput = active?.id === 'palette-input' || active?.id === 'files-search-input';
4040
+ const isEditable = active?.tagName === 'INPUT' || active?.tagName === 'TEXTAREA' || active?.isContentEditable;
4041
+
4042
+ if (!isOurInput && isEditable) return; // Let native behavior happen
4043
+
4044
+ e.preventDefault(); // Prevent browser Print dialog
4045
+ if (paletteOpen) {
4046
+ closePalette();
4047
+ } else {
4048
+ openPalette();
4049
+ }
4050
+ }
4051
+ });
4052
+
3314
4053
  // Collapse all folders
3315
4054
  function collapseAllFolders() {
3316
4055
  filesTreeExpanded.clear();
@@ -3347,6 +4086,7 @@
3347
4086
  const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
3348
4087
  if (existingTab) {
3349
4088
  selectTab(existingTab.id);
4089
+ refreshFileTab(existingTab.id); // Refresh content in case file changed
3350
4090
  return;
3351
4091
  }
3352
4092
 
@@ -3403,6 +4143,10 @@
3403
4143
  <div class="dashboard-section section-tabs ${sectionState.tabs ? '' : 'collapsed'}">
3404
4144
  <div class="dashboard-section-header" onclick="toggleSection('tabs')">
3405
4145
  <h3><span class="collapse-icon">▼</span> Tabs</h3>
4146
+ <div class="header-actions" onclick="event.stopPropagation()">
4147
+ <button onclick="spawnBuilder()" title="New Worktree">+ Worktree</button>
4148
+ <button onclick="spawnShell()" title="New Shell">+ Shell</button>
4149
+ </div>
3406
4150
  </div>
3407
4151
  <div class="dashboard-section-content">
3408
4152
  <div class="dashboard-tabs-list" id="dashboard-tabs-list">
@@ -3415,13 +4159,26 @@
3415
4159
  <div class="dashboard-section-header" onclick="toggleSection('files')">
3416
4160
  <h3><span class="collapse-icon">▼</span> Files</h3>
3417
4161
  <div class="header-actions" onclick="event.stopPropagation()">
4162
+ <button onclick="refreshFilesTree()" title="Refresh">↻</button>
3418
4163
  <button onclick="collapseAllFolders()" title="Collapse All">⊟</button>
3419
4164
  <button onclick="expandAllFolders()" title="Expand All">⊞</button>
3420
4165
  </div>
3421
4166
  </div>
3422
4167
  <div class="dashboard-section-content">
3423
- <div class="dashboard-files-list" id="dashboard-files-list">
3424
- ${renderDashboardFilesBrowser()}
4168
+ <div class="files-search-container" onclick="event.stopPropagation()">
4169
+ <input type="text"
4170
+ id="files-search-input"
4171
+ class="files-search-input"
4172
+ placeholder="Search files by name..."
4173
+ oninput="onFilesSearchInput(this.value)"
4174
+ onkeydown="onFilesSearchKeydown(event)"
4175
+ value="${escapeHtml(filesSearchQuery)}" />
4176
+ <button class="files-search-clear ${filesSearchQuery ? '' : 'hidden'}"
4177
+ onclick="clearFilesSearch()"
4178
+ title="Clear search">×</button>
4179
+ </div>
4180
+ <div id="dashboard-files-content">
4181
+ ${filesSearchQuery ? renderFilesSearchResults() : renderDashboardFilesBrowserWithWrapper()}
3425
4182
  </div>
3426
4183
  </div>
3427
4184
  </div>
@@ -3714,6 +4471,269 @@
3714
4471
  // Start projectlist polling (separate from main state polling)
3715
4472
  setInterval(pollProjectlist, 5000);
3716
4473
 
4474
+ // ========================================
4475
+ // Activity Summary (Spec 0059)
4476
+ // ========================================
4477
+
4478
+ let activityData = null;
4479
+
4480
+ // Show activity summary modal
4481
+ async function showActivitySummary() {
4482
+ // Check if activity tab already exists
4483
+ let activityTab = tabs.find(t => t.type === 'activity');
4484
+
4485
+ if (!activityTab) {
4486
+ // Create new activity tab
4487
+ activityTab = {
4488
+ id: 'activity-today',
4489
+ type: 'activity',
4490
+ name: 'Today'
4491
+ };
4492
+ tabs.push(activityTab);
4493
+ }
4494
+
4495
+ // Switch to activity tab
4496
+ activeTabId = activityTab.id;
4497
+ currentTabType = null; // Force re-render
4498
+ renderTabs();
4499
+ renderTabContent();
4500
+ }
4501
+
4502
+ // Render the activity tab content
4503
+ async function renderActivityTab() {
4504
+ const content = document.getElementById('tab-content');
4505
+
4506
+ // Show loading state
4507
+ content.innerHTML = `
4508
+ <div class="activity-tab-container">
4509
+ <div class="activity-loading">
4510
+ <span class="activity-spinner"></span>
4511
+ Loading activity...
4512
+ </div>
4513
+ </div>
4514
+ `;
4515
+
4516
+ try {
4517
+ const response = await fetch('/api/activity-summary');
4518
+ if (!response.ok) {
4519
+ throw new Error(await response.text());
4520
+ }
4521
+ activityData = await response.json();
4522
+ renderActivityTabContent(activityData);
4523
+ } catch (err) {
4524
+ content.innerHTML = `
4525
+ <div class="activity-tab-container">
4526
+ <div class="activity-error">
4527
+ Failed to load activity: ${escapeHtml(err.message)}
4528
+ </div>
4529
+ </div>
4530
+ `;
4531
+ }
4532
+ }
4533
+
4534
+ // Render activity tab content (similar to modal but in tab)
4535
+ function renderActivityTabContent(data) {
4536
+ const content = document.getElementById('tab-content');
4537
+
4538
+ // Check for zero activity
4539
+ if (data.commits.length === 0 && data.prs.length === 0 && data.builders.length === 0) {
4540
+ content.innerHTML = `
4541
+ <div class="activity-tab-container">
4542
+ <div class="activity-empty">
4543
+ <p>No activity recorded today</p>
4544
+ <p style="font-size: 12px; margin-top: 8px;">Make some commits or create PRs to see your daily summary!</p>
4545
+ </div>
4546
+ </div>
4547
+ `;
4548
+ return;
4549
+ }
4550
+
4551
+ const hours = Math.floor(data.timeTracking.activeMinutes / 60);
4552
+ const mins = data.timeTracking.activeMinutes % 60;
4553
+ const uniqueBranches = new Set(data.commits.map(c => c.branch)).size;
4554
+ const mergedPrs = data.prs.filter(p => p.state === 'MERGED').length;
4555
+
4556
+ // Format time strings
4557
+ const formatTime = (isoString) => {
4558
+ if (!isoString) return '--';
4559
+ const date = new Date(isoString);
4560
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
4561
+ };
4562
+
4563
+ let html = '<div class="activity-tab-container"><div class="activity-summary">';
4564
+
4565
+ // AI Summary (if available)
4566
+ if (data.aiSummary) {
4567
+ html += `<div class="activity-ai-summary">${escapeHtml(data.aiSummary)}</div>`;
4568
+ }
4569
+
4570
+ // Activity section
4571
+ html += `
4572
+ <div class="activity-section">
4573
+ <h4>Activity</h4>
4574
+ <ul>
4575
+ <li>${data.commits.length} commits across ${uniqueBranches} branch${uniqueBranches !== 1 ? 'es' : ''}</li>
4576
+ <li>${data.files.length} files modified</li>
4577
+ <li>${data.prs.length} PR${data.prs.length !== 1 ? 's' : ''} created${mergedPrs > 0 ? `, ${mergedPrs} merged` : ''}</li>
4578
+ </ul>
4579
+ </div>
4580
+ `;
4581
+
4582
+ // Projects section (if any status changes)
4583
+ if (data.projectChanges && data.projectChanges.length > 0) {
4584
+ html += `
4585
+ <div class="activity-section">
4586
+ <h4>Projects Touched</h4>
4587
+ <ul>
4588
+ ${data.projectChanges.map(p => `<li>${escapeHtml(p.id)}: ${escapeHtml(p.title)} (${escapeHtml(p.oldStatus)} → ${escapeHtml(p.newStatus)})</li>`).join('')}
4589
+ </ul>
4590
+ </div>
4591
+ `;
4592
+ }
4593
+
4594
+ // Time section
4595
+ html += `
4596
+ <div class="activity-section">
4597
+ <h4>Time</h4>
4598
+ <p><span class="activity-time-value">~${hours}h ${mins}m</span> active time</p>
4599
+ <p>First activity: ${formatTime(data.timeTracking.firstActivity)}</p>
4600
+ <p>Last activity: ${formatTime(data.timeTracking.lastActivity)}</p>
4601
+ </div>
4602
+ `;
4603
+
4604
+ // Copy button
4605
+ html += `
4606
+ <div class="activity-actions">
4607
+ <button class="btn" onclick="copyActivityToClipboard()">Copy to Clipboard</button>
4608
+ </div>
4609
+ `;
4610
+
4611
+ html += '</div></div>';
4612
+ content.innerHTML = html;
4613
+ }
4614
+
4615
+ // Render activity summary content
4616
+ function renderActivitySummary(data) {
4617
+ const content = document.getElementById('activity-content');
4618
+
4619
+ // Check for zero activity
4620
+ if (data.commits.length === 0 && data.prs.length === 0 && data.builders.length === 0) {
4621
+ content.innerHTML = `
4622
+ <div class="activity-empty">
4623
+ <p>No activity recorded today</p>
4624
+ <p style="font-size: 12px; margin-top: 8px;">Make some commits or create PRs to see your daily summary!</p>
4625
+ </div>
4626
+ `;
4627
+ return;
4628
+ }
4629
+
4630
+ const hours = Math.floor(data.timeTracking.activeMinutes / 60);
4631
+ const mins = data.timeTracking.activeMinutes % 60;
4632
+ const uniqueBranches = new Set(data.commits.map(c => c.branch)).size;
4633
+ const mergedPrs = data.prs.filter(p => p.state === 'MERGED').length;
4634
+
4635
+ // Format time strings
4636
+ const formatTime = (isoString) => {
4637
+ if (!isoString) return '--';
4638
+ const date = new Date(isoString);
4639
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
4640
+ };
4641
+
4642
+ let html = '<div class="activity-summary">';
4643
+
4644
+ // AI Summary (if available)
4645
+ if (data.aiSummary) {
4646
+ html += `<div class="activity-ai-summary">${escapeHtml(data.aiSummary)}</div>`;
4647
+ }
4648
+
4649
+ // Activity section
4650
+ html += `
4651
+ <div class="activity-section">
4652
+ <h4>Activity</h4>
4653
+ <ul>
4654
+ <li>${data.commits.length} commits across ${uniqueBranches} branch${uniqueBranches !== 1 ? 'es' : ''}</li>
4655
+ <li>${data.files.length} files modified</li>
4656
+ <li>${data.prs.length} PR${data.prs.length !== 1 ? 's' : ''} created${mergedPrs > 0 ? `, ${mergedPrs} merged` : ''}</li>
4657
+ </ul>
4658
+ </div>
4659
+ `;
4660
+
4661
+ // Projects section (if any status changes)
4662
+ if (data.projectChanges && data.projectChanges.length > 0) {
4663
+ html += `
4664
+ <div class="activity-section">
4665
+ <h4>Projects Touched</h4>
4666
+ <ul>
4667
+ ${data.projectChanges.map(p => `<li>${escapeHtml(p.id)}: ${escapeHtml(p.title)} (${escapeHtml(p.oldStatus)} → ${escapeHtml(p.newStatus)})</li>`).join('')}
4668
+ </ul>
4669
+ </div>
4670
+ `;
4671
+ }
4672
+
4673
+ // Time section
4674
+ html += `
4675
+ <div class="activity-section">
4676
+ <h4>Time</h4>
4677
+ <p><span class="activity-time-value">~${hours}h ${mins}m</span> active time</p>
4678
+ <p>First activity: ${formatTime(data.timeTracking.firstActivity)}</p>
4679
+ <p>Last activity: ${formatTime(data.timeTracking.lastActivity)}</p>
4680
+ </div>
4681
+ `;
4682
+
4683
+ html += '</div>';
4684
+ content.innerHTML = html;
4685
+ }
4686
+
4687
+ // Close activity modal
4688
+ function closeActivityModal() {
4689
+ document.getElementById('activity-modal').classList.add('hidden');
4690
+ }
4691
+
4692
+ // Copy activity summary to clipboard
4693
+ function copyActivitySummary() {
4694
+ if (!activityData) return;
4695
+
4696
+ const hours = Math.floor(activityData.timeTracking.activeMinutes / 60);
4697
+ const mins = activityData.timeTracking.activeMinutes % 60;
4698
+ const uniqueBranches = new Set(activityData.commits.map(c => c.branch)).size;
4699
+ const mergedPrs = activityData.prs.filter(p => p.state === 'MERGED').length;
4700
+
4701
+ let markdown = `## Today's Summary\n\n`;
4702
+
4703
+ if (activityData.aiSummary) {
4704
+ markdown += `${activityData.aiSummary}\n\n`;
4705
+ }
4706
+
4707
+ markdown += `### Activity\n`;
4708
+ markdown += `- ${activityData.commits.length} commits across ${uniqueBranches} branches\n`;
4709
+ markdown += `- ${activityData.files.length} files modified\n`;
4710
+ markdown += `- ${activityData.prs.length} PRs${mergedPrs > 0 ? ` (${mergedPrs} merged)` : ''}\n\n`;
4711
+
4712
+ if (activityData.projectChanges && activityData.projectChanges.length > 0) {
4713
+ markdown += `### Projects Touched\n`;
4714
+ activityData.projectChanges.forEach(p => {
4715
+ markdown += `- ${p.id}: ${p.title} (${p.oldStatus} → ${p.newStatus})\n`;
4716
+ });
4717
+ markdown += '\n';
4718
+ }
4719
+
4720
+ markdown += `### Time\n`;
4721
+ markdown += `Active time: ~${hours}h ${mins}m\n`;
4722
+
4723
+ navigator.clipboard.writeText(markdown).then(() => {
4724
+ showToast('Copied to clipboard', 'success');
4725
+ }).catch(() => {
4726
+ showToast('Failed to copy', 'error');
4727
+ });
4728
+ }
4729
+
4730
+ // Close activity modal when clicking backdrop
4731
+ document.getElementById('activity-modal').addEventListener('click', (e) => {
4732
+ if (e.target.id === 'activity-modal') {
4733
+ closeActivityModal();
4734
+ }
4735
+ });
4736
+
3717
4737
  // Initialize on load
3718
4738
  init();
3719
4739
  </script>