claude-home 1.4.8 → 1.5.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-home",
3
- "version": "1.4.8",
3
+ "version": "1.5.9",
4
4
  "description": "Web dashboard for Claude Code — browse sessions, manage skills, hooks, commands, and agents",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -740,6 +740,90 @@
740
740
  font-variant-numeric: tabular-nums;
741
741
  }
742
742
 
743
+ /* ── Projects ─────────────────────────────────────────── */
744
+ .project-grid {
745
+ display: grid;
746
+ grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
747
+ gap: 12px;
748
+ padding: 20px;
749
+ align-content: start;
750
+ }
751
+ .project-card {
752
+ background: var(--white);
753
+ border: 1px solid var(--rule);
754
+ border-radius: 6px;
755
+ padding: 16px 18px;
756
+ cursor: pointer;
757
+ transition: border-color 0.12s, box-shadow 0.12s;
758
+ }
759
+ .project-card:hover {
760
+ border-color: var(--rule-2);
761
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
762
+ }
763
+ .project-card-name {
764
+ font-size: 14px;
765
+ font-weight: 600;
766
+ overflow: hidden;
767
+ text-overflow: ellipsis;
768
+ white-space: nowrap;
769
+ margin-bottom: 2px;
770
+ }
771
+ .project-card-path {
772
+ font-size: 10.5px;
773
+ color: var(--ink-3);
774
+ overflow: hidden;
775
+ text-overflow: ellipsis;
776
+ white-space: nowrap;
777
+ margin-bottom: 12px;
778
+ font-family: 'SF Mono','Fira Code',monospace;
779
+ }
780
+ .project-card-stats {
781
+ display: flex;
782
+ gap: 16px;
783
+ flex-wrap: wrap;
784
+ }
785
+ .project-stat { display: flex; flex-direction: column; gap: 1px; }
786
+ .project-stat-val { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.1; }
787
+ .project-stat-label { font-size: 10px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; }
788
+ .project-card-footer {
789
+ display: flex;
790
+ align-items: center;
791
+ gap: 8px;
792
+ margin-top: 12px;
793
+ padding-top: 10px;
794
+ border-top: 1px solid var(--rule);
795
+ flex-wrap: wrap;
796
+ }
797
+ .proj-badge {
798
+ font-size: 10px;
799
+ padding: 2px 7px;
800
+ border-radius: 3px;
801
+ background: var(--canvas);
802
+ color: var(--ink-3);
803
+ font-weight: 500;
804
+ }
805
+ .proj-badge.missing { background: #3a1a1a; color: var(--red); }
806
+ .proj-badge.memory { background: var(--canvas); color: var(--ink-2); }
807
+ .project-detail-header {
808
+ display: flex;
809
+ align-items: center;
810
+ gap: 12px;
811
+ padding: 14px 20px;
812
+ border-bottom: 1px solid var(--rule);
813
+ min-height: 52px;
814
+ }
815
+ .project-detail-body {
816
+ flex: 1;
817
+ display: flex;
818
+ flex-direction: column;
819
+ overflow: hidden;
820
+ }
821
+ .proj-md-content {
822
+ flex: 1;
823
+ overflow-y: auto;
824
+ padding: 20px;
825
+ }
826
+
743
827
  /* ── Memory ───────────────────────────────────────────── */
744
828
  .memory-layout {
745
829
  display: flex;
@@ -1573,35 +1657,6 @@
1573
1657
  <span style="color:var(--ink-3)">CO₂</span>
1574
1658
  <span x-text="fmtCarbon(insights.totalCarbon) || '—'"></span>
1575
1659
  </div>
1576
- <template x-if="insights.totalCarbon > 100">
1577
- <div style="margin-top:6px;display:flex;flex-direction:column;gap:3px">
1578
- <!-- Car -->
1579
- <div style="font-size:10px;color:var(--ink-3);display:flex;align-items:center;gap:5px">
1580
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M5 17H3v-5l2-5h14l2 5v5h-2"/><circle cx="7.5" cy="17.5" r="1.5"/><circle cx="16.5" cy="17.5" r="1.5"/><path d="M5 12h14"/></svg>
1581
- <span x-text="carbonEquivs(insights.totalCarbon)?.car"></span>
1582
- </div>
1583
- <!-- Plane -->
1584
- <div style="font-size:10px;color:var(--ink-3);display:flex;align-items:center;gap:5px">
1585
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5z"/></svg>
1586
- <span x-text="carbonEquivs(insights.totalCarbon)?.flights"></span>
1587
- </div>
1588
- <!-- Tree -->
1589
- <div style="font-size:10px;color:var(--ink-3);display:flex;align-items:center;gap:5px">
1590
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M12 22v-7"/><path d="M9 15l3-3 3 3"/><path d="M7 12l5-5 5 5"/><path d="M5 9l7-7 7 7"/></svg>
1591
- <span x-text="carbonEquivs(insights.totalCarbon)?.trees"></span>
1592
- </div>
1593
- <!-- Burger / Food -->
1594
- <div style="font-size:10px;color:var(--ink-3);display:flex;align-items:center;gap:5px">
1595
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M4 8h16"/><path d="M4 12h16"/><path d="M4 16h16"/><path d="M6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/></svg>
1596
- <span x-text="carbonEquivs(insights.totalCarbon)?.burgers"></span>
1597
- </div>
1598
- <!-- Home / Electricity -->
1599
- <div style="font-size:10px;color:var(--ink-3);display:flex;align-items:center;gap:5px">
1600
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/><path d="M9 21V12h6v9"/></svg>
1601
- <span x-text="carbonEquivs(insights.totalCarbon)?.electricity"></span>
1602
- </div>
1603
- </div>
1604
- </template>
1605
1660
  </div>
1606
1661
  </template>
1607
1662
  <!-- Version + last session -->
@@ -1614,12 +1669,174 @@
1614
1669
  Last: <span x-text="formatDate(sessions[0]?.modified)"></span>
1615
1670
  </div>
1616
1671
  </template>
1672
+ <div class="sidebar-footer-row" style="color:var(--ink-3)">
1673
+ Created by <a href="https://zenekezene.com/" target="_blank" style="color:var(--ink-2);text-decoration:none;" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">Zenekezene</a> with wine
1674
+ </div>
1617
1675
  </div>
1618
1676
  </aside>
1619
1677
 
1620
1678
  <!-- ── Main ── -->
1621
1679
  <div class="main">
1622
1680
 
1681
+ <!-- Project detail -->
1682
+ <template x-if="view === 'projects' && selectedProject && !selectedSession">
1683
+ <div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
1684
+ <!-- Header -->
1685
+ <div class="project-detail-header">
1686
+ <button class="btn btn-outline btn-sm" @click="selectedProject=null;view='dashboard'" style="flex-shrink:0">← Dashboard</button>
1687
+ <div style="flex:1;min-width:0">
1688
+ <div style="font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" x-text="shortProjectName(selectedProject.projectPath) || selectedProject.dirName"></div>
1689
+ <div style="font-size:10.5px;color:var(--ink-3);font-family:'SF Mono','Fira Code',monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" x-text="selectedProject.projectPath"></div>
1690
+ </div>
1691
+ <!-- Stats chips -->
1692
+ <div style="display:flex;gap:10px;align-items:center;flex-shrink:0">
1693
+ <span style="font-size:11.5px;color:var(--ink-3)"><b x-text="selectedProject.sessionCount"></b> sessions</span>
1694
+ <template x-if="selectedProject.memoryCount > 0">
1695
+ <span style="font-size:11.5px;color:var(--ink-3)"><b x-text="selectedProject.memoryCount"></b> memories</span>
1696
+ </template>
1697
+ <template x-if="projectCost(selectedProject.dirName) !== null">
1698
+ <span style="font-size:11.5px;color:var(--ink-3)"><b x-text="fmtCost(projectCost(selectedProject.dirName))"></b> cost</span>
1699
+ </template>
1700
+ <template x-if="!selectedProject.diskExists">
1701
+ <span class="proj-badge missing">directory missing</span>
1702
+ </template>
1703
+ </div>
1704
+ </div>
1705
+
1706
+ <!-- Tabs -->
1707
+ <div class="tools-tabs" style="border-bottom:1px solid var(--rule)">
1708
+ <div class="tools-tab" :class="{active: projectTab==='sessions'}" @click="projectTab='sessions'">Sessions</div>
1709
+ <div class="tools-tab" :class="{active: projectTab==='memory'}" @click="projectTab='memory'">Memory <template x-if="projectMemory.length>0"><span class="nav-count" x-text="projectMemory.length"></span></template></div>
1710
+ <div class="tools-tab" :class="{active: projectTab==='claudemd'}" @click="projectTab='claudemd'">CLAUDE.md</div>
1711
+ </div>
1712
+
1713
+ <!-- Tab: Sessions -->
1714
+ <template x-if="projectTab === 'sessions'">
1715
+ <div style="flex:1;overflow-y:auto;padding-top:6px;">
1716
+ <template x-if="projectSessionsLoading"><div class="loading"><div class="spinner"></div> Loading…</div></template>
1717
+ <template x-if="!projectSessionsLoading && projectSessions.length === 0">
1718
+ <div class="empty"><div class="empty-mark"></div>No sessions</div>
1719
+ </template>
1720
+ <template x-if="!projectSessionsLoading && projectSessions.length > 0">
1721
+ <div class="sessions-list" style="padding:0 20px">
1722
+ <template x-for="s in projectSessions" :key="s.sessionId">
1723
+ <div class="session-row" @click="view='sessions';openSession(s)">
1724
+ <div class="session-body">
1725
+ <div class="session-prompt" x-text="s.firstPrompt || '(no prompt)'"></div>
1726
+ <div class="session-lower">
1727
+ <template x-if="s.gitBranch">
1728
+ <div class="session-branch">
1729
+ <div class="branch-square" :style="'background:' + branchColor(s.gitBranch)"></div>
1730
+ <span x-text="s.gitBranch"></span>
1731
+ </div>
1732
+ </template>
1733
+ <span class="session-msgs" x-text="s.messageCount + ' msgs'"></span>
1734
+ <span class="session-msgs" x-text="formatDate(s.modified)"></span>
1735
+ <template x-if="sessionCosts[s.sessionId]">
1736
+ <span class="session-msgs" style="padding-left:8px;border-left:1px solid var(--rule);font-family:'SF Mono','Fira Code',monospace" x-text="fmtCost(sessionCosts[s.sessionId])"></span>
1737
+ </template>
1738
+ </div>
1739
+ </div>
1740
+ <div class="session-right">
1741
+ <button class="session-resume-btn" :disabled="!s.resumable" :title="!s.resumable ? 'Session file deleted' : 'Copy resume command'" @click.stop="s.resumable && resumeSession(s.sessionId, 'proj-resume-'+s.sessionId, s.projectPath)" :id="'proj-resume-'+s.sessionId">Resume →</button>
1742
+ </div>
1743
+ </div>
1744
+ </template>
1745
+ </div>
1746
+ </template>
1747
+ </div>
1748
+ </template>
1749
+
1750
+ <!-- Tab: Memory -->
1751
+ <template x-if="projectTab === 'memory'">
1752
+ <div style="flex:1;overflow:hidden;display:flex;">
1753
+ <template x-if="projectMemoryLoading"><div class="loading"><div class="spinner"></div> Loading…</div></template>
1754
+ <template x-if="!projectMemoryLoading && projectMemory.length === 0">
1755
+ <div class="empty"><div class="empty-mark"></div>No memory files for this project</div>
1756
+ </template>
1757
+ <template x-if="!projectMemoryLoading && projectMemory.length > 0">
1758
+ <div style="display:flex;width:100%;overflow:hidden;">
1759
+ <div class="memory-sidebar" :style="'width:'+sidebarW+'px'">
1760
+ <template x-for="f in projectMemory" :key="f.filename">
1761
+ <div class="memory-row" :class="{active: projectMemorySelected?.filename===f.filename}"
1762
+ @click="projectMemorySelected=f">
1763
+ <div class="memory-row-name" x-text="f.name || f.filename"></div>
1764
+ <div class="memory-row-desc" x-text="f.description || f.filename"></div>
1765
+ </div>
1766
+ </template>
1767
+ </div>
1768
+ <div class="resize-handle" @mousedown.prevent="startResize($event)"></div>
1769
+ <div class="memory-detail">
1770
+ <template x-if="!projectMemorySelected">
1771
+ <div class="memory-empty"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>Select a memory file</div>
1772
+ </template>
1773
+ <template x-if="projectMemorySelected">
1774
+ <div>
1775
+ <div class="memory-detail-title" x-text="projectMemorySelected.name || projectMemorySelected.filename"></div>
1776
+ <div class="memory-detail-meta">
1777
+ <span :class="'type-pill type-'+(projectMemorySelected.type||'unknown')" x-text="projectMemorySelected.type||'unknown'"></span>
1778
+ <span class="memory-filename" x-text="projectMemorySelected.filename"></span>
1779
+ </div>
1780
+ <template x-if="projectMemorySelected.description"><div class="memory-desc-intro" x-text="projectMemorySelected.description"></div></template>
1781
+ <div class="memory-content md-content" x-html="renderMd(projectMemorySelected.content)"></div>
1782
+ </div>
1783
+ </template>
1784
+ </div>
1785
+ </div>
1786
+ </template>
1787
+ </div>
1788
+ </template>
1789
+
1790
+ <!-- Tab: CLAUDE.md -->
1791
+ <template x-if="projectTab === 'claudemd'">
1792
+ <div style="flex:1;overflow:hidden;display:flex;flex-direction:column;">
1793
+ <template x-if="projectClaudeMdLoading"><div class="loading"><div class="spinner"></div> Loading…</div></template>
1794
+ <template x-if="!projectClaudeMdLoading && projectClaudeMd">
1795
+ <div style="flex:1;overflow:hidden;display:flex;flex-direction:column;">
1796
+ <!-- File selector + actions -->
1797
+ <div style="display:flex;align-items:center;gap:8px;padding:8px 20px;border-bottom:1px solid var(--rule);flex-wrap:wrap">
1798
+ <template x-if="projectClaudeMd.files.length === 0">
1799
+ <span style="font-size:12px;color:var(--ink-3)">No CLAUDE.md found in this project</span>
1800
+ </template>
1801
+ <template x-if="projectClaudeMd.files.length > 0">
1802
+ <select class="filter-select" x-model.number="projectClaudeMdEdit.fileIdx" @change="projectClaudeMdEdit.active=false;projectClaudeMdEdit.msg=''">
1803
+ <template x-for="(f, i) in projectClaudeMd.files" :key="f.filePath">
1804
+ <option :value="i" x-text="f.label"></option>
1805
+ </template>
1806
+ </select>
1807
+ </template>
1808
+ <div style="margin-left:auto;display:flex;gap:8px;align-items:center">
1809
+ <template x-if="projectClaudeMdEdit.msg">
1810
+ <span :class="projectClaudeMdEdit.msg==='Saved'?'save-msg':'err-msg'" x-text="projectClaudeMdEdit.msg"></span>
1811
+ </template>
1812
+ <template x-if="projectClaudeMd.files.length > 0 && !projectClaudeMdEdit.active">
1813
+ <button class="btn btn-outline btn-sm" @click="projectClaudeMdEdit.active=true;projectClaudeMdEdit.draft=projectClaudeMd.files[projectClaudeMdEdit.fileIdx]?.content||'';projectClaudeMdEdit.msg=''">Edit</button>
1814
+ </template>
1815
+ <template x-if="projectClaudeMdEdit.active">
1816
+ <button class="btn btn-outline btn-sm" @click="projectClaudeMdEdit.active=false">Cancel</button>
1817
+ </template>
1818
+ <template x-if="projectClaudeMdEdit.active">
1819
+ <button class="btn btn-primary btn-sm" @click="saveProjectClaudeMd()" :disabled="projectClaudeMdEdit.saving" x-text="projectClaudeMdEdit.saving?'Saving…':'Save'"></button>
1820
+ </template>
1821
+ </div>
1822
+ </div>
1823
+ <!-- Content -->
1824
+ <div style="flex:1;overflow:hidden;">
1825
+ <template x-if="projectClaudeMd.files.length > 0 && !projectClaudeMdEdit.active">
1826
+ <div class="proj-md-content md-content" x-html="renderMd(projectClaudeMd.files[projectClaudeMdEdit.fileIdx]?.content||'')"></div>
1827
+ </template>
1828
+ <template x-if="projectClaudeMdEdit.active">
1829
+ <textarea class="edit-textarea" style="width:100%;height:100%;resize:none;border:none;outline:none;padding:20px;font-family:'SF Mono','Fira Code',monospace;font-size:12.5px;line-height:1.7;background:var(--white);box-sizing:border-box" x-model="projectClaudeMdEdit.draft"></textarea>
1830
+ </template>
1831
+ </div>
1832
+ </div>
1833
+ </template>
1834
+ </div>
1835
+ </template>
1836
+
1837
+ </div>
1838
+ </template>
1839
+
1623
1840
  <!-- Dashboard (home) — disabled -->
1624
1841
  <template x-if="view === 'dashboard' && !selectedSession">
1625
1842
  <div style="display:flex;flex-direction:column;height:100%;overflow:hidden;">
@@ -1628,7 +1845,10 @@
1628
1845
  <span style="font-size:11px;color:var(--ink-3)" x-text="formatDate(new Date().toISOString())"></span>
1629
1846
  </div>
1630
1847
  <div class="content">
1631
- <div style="max-width:1100px">
1848
+ <div style="max-width:1200px;display:flex;gap:20px;align-items:flex-start">
1849
+
1850
+ <!-- Left column: all dashboard content -->
1851
+ <div style="flex:1;min-width:0">
1632
1852
 
1633
1853
  <!-- Row 1: Stats -->
1634
1854
  <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px">
@@ -1638,12 +1858,42 @@
1638
1858
  <div class="db-card-sub" x-text="insights ? fmtTokensM(insights.totalTokens.output) + ' output · ' + fmtTokensM(insights.totalTokens.input) + ' input' : ''"></div>
1639
1859
  </div>
1640
1860
  <div class="db-card">
1641
- <div class="db-card-label">Total cost</div>
1861
+ <div class="db-card-label" style="display:flex;align-items:center;gap:5px">
1862
+ Total cost
1863
+ <span class="tooltip-wrap" @click.stop>
1864
+ <i class="tooltip-icon">i</i>
1865
+ <span class="tooltip-box">
1866
+ <strong>Cost calculation</strong><br><br>
1867
+ Based on Anthropic's published API pricing (USD per million tokens):<br><br>
1868
+ <table style="border-collapse:collapse;width:100%;font-size:11px">
1869
+ <tr style="color:var(--ink-3)"><td></td><td style="text-align:right;padding-right:8px">Input</td><td style="text-align:right;padding-right:8px">Output</td><td style="text-align:right;padding-right:8px">Cache write</td><td style="text-align:right">Cache read</td></tr>
1870
+ <tr><td style="padding-right:8px">Opus</td><td style="text-align:right;padding-right:8px">$15</td><td style="text-align:right;padding-right:8px">$75</td><td style="text-align:right;padding-right:8px">$18.75</td><td style="text-align:right">$1.50</td></tr>
1871
+ <tr><td style="padding-right:8px">Sonnet</td><td style="text-align:right;padding-right:8px">$3</td><td style="text-align:right;padding-right:8px">$15</td><td style="text-align:right;padding-right:8px">$3.75</td><td style="text-align:right">$0.30</td></tr>
1872
+ <tr><td style="padding-right:8px">Haiku</td><td style="text-align:right;padding-right:8px">$0.80</td><td style="text-align:right;padding-right:8px">$4</td><td style="text-align:right;padding-right:8px">$1.00</td><td style="text-align:right">$0.08</td></tr>
1873
+ </table><br>
1874
+ Cost is summed across all sessions by reading token usage from each JSONL file. Sessions with no token data are excluded.
1875
+ </span>
1876
+ </span>
1877
+ </div>
1642
1878
  <div class="db-card-value" x-text="insights ? fmtCost(insights.totalCost) : '—'"></div>
1643
1879
  <div class="db-card-sub">all time</div>
1644
1880
  </div>
1645
1881
  <div class="db-card">
1646
- <div class="db-card-label">Cache savings</div>
1882
+ <div class="db-card-label" style="display:flex;align-items:center;gap:5px">
1883
+ Cache savings
1884
+ <span class="tooltip-wrap" @click.stop>
1885
+ <i class="tooltip-icon">i</i>
1886
+ <span class="tooltip-box">
1887
+ <strong>How cache savings are calculated</strong><br><br>
1888
+ When Claude reuses a prompt prefix it has seen before, those tokens are billed at the cheaper <em>cache read</em> rate instead of the full input rate.<br><br>
1889
+ <strong>Savings per token = input price − cache read price:</strong><br>
1890
+ Opus: $15 − $1.50 = <strong>$13.50</strong>/M tokens<br>
1891
+ Sonnet: $3 − $0.30 = <strong>$2.70</strong>/M tokens<br>
1892
+ Haiku: $0.80 − $0.08 = <strong>$0.72</strong>/M tokens<br><br>
1893
+ Savings shown here are the cumulative total across all sessions where cache hits were recorded.
1894
+ </span>
1895
+ </span>
1896
+ </div>
1647
1897
  <div class="db-card-value" style="color:var(--green)" x-text="insights ? fmtCost(insights.totalSavings) : '—'"></div>
1648
1898
  <div class="db-card-sub">money saved via prompt cache</div>
1649
1899
  </div>
@@ -1690,28 +1940,9 @@
1690
1940
  </div>
1691
1941
  </template>
1692
1942
  </div>
1693
- </div>
1943
+ </div><!-- /stats grid -->
1694
1944
 
1695
- <!-- Row 2: Latest session -->
1696
- <template x-if="sessions.length > 0">
1697
- <div class="db-latest" @click="openSession(sessions[0])">
1698
- <div class="db-latest-label">Latest session</div>
1699
- <div class="db-latest-prompt" x-text="sessions[0].firstPrompt || '(no prompt)'"></div>
1700
- <div class="db-latest-meta">
1701
- <template x-if="sessions[0].gitBranch">
1702
- <span style="display:flex;align-items:center;gap:5px">
1703
- <div class="branch-square" :style="'background:' + branchColor(sessions[0].gitBranch)"></div>
1704
- <span x-text="sessions[0].gitBranch"></span>
1705
- </span>
1706
- </template>
1707
- <span x-text="formatDate(sessions[0].modified)"></span>
1708
- <span x-text="sessions[0].messageCount + ' msgs'"></span>
1709
- <button class="btn btn-primary btn-sm" style="margin-left:auto" :id="'resume-db'" :disabled="!sessions[0].resumable" :title="!sessions[0].resumable ? 'Session file deleted — cannot resume' : 'Copy command to resume this session'" @click.stop="sessions[0].resumable && resumeSession(sessions[0].sessionId,'resume-db', sessions[0].projectPath)">Resume →</button>
1710
- </div>
1711
- </div>
1712
- </template>
1713
-
1714
- <!-- Row 3: Heatmap -->
1945
+ <!-- Row 2: Heatmap -->
1715
1946
  <template x-if="stats">
1716
1947
  <div class="heatmap-wrap">
1717
1948
  <div class="heatmap-section-title">Contribution heatmap</div>
@@ -1761,6 +1992,25 @@
1761
1992
  </template>
1762
1993
  </div>
1763
1994
 
1995
+ <!-- Latest session -->
1996
+ <template x-if="sessions.length > 0">
1997
+ <div class="db-latest" @click="openSession(sessions[0])" style="margin-bottom:16px">
1998
+ <div class="db-latest-label">Latest session</div>
1999
+ <div class="db-latest-prompt" x-text="sessions[0].firstPrompt || '(no prompt)'"></div>
2000
+ <div class="db-latest-meta">
2001
+ <template x-if="sessions[0].gitBranch">
2002
+ <span style="display:flex;align-items:center;gap:5px">
2003
+ <div class="branch-square" :style="'background:' + branchColor(sessions[0].gitBranch)"></div>
2004
+ <span x-text="sessions[0].gitBranch"></span>
2005
+ </span>
2006
+ </template>
2007
+ <span x-text="formatDate(sessions[0].modified)"></span>
2008
+ <span x-text="sessions[0].messageCount + ' msgs'"></span>
2009
+ <button class="btn btn-primary btn-sm" style="margin-left:auto" :id="'resume-db'" :disabled="!sessions[0].resumable" :title="!sessions[0].resumable ? 'Session file deleted — cannot resume' : 'Copy command to resume this session'" @click.stop="sessions[0].resumable && resumeSession(sessions[0].sessionId,'resume-db', sessions[0].projectPath)">Resume →</button>
2010
+ </div>
2011
+ </div>
2012
+ </template>
2013
+
1764
2014
  <!-- Top sessions -->
1765
2015
  <template x-if="insights.topSessions && insights.topSessions.length > 0">
1766
2016
  <div class="insight-section">
@@ -1777,7 +2027,34 @@
1777
2027
  </div>
1778
2028
  </template>
1779
2029
 
1780
- </div>
2030
+ </div><!-- /left column -->
2031
+
2032
+ <!-- Right column: Projects -->
2033
+ <div style="width:240px;min-width:240px;background:var(--white);border:1px solid var(--rule);border-radius:6px;overflow:hidden;position:sticky;top:0">
2034
+ <div style="padding:12px 14px 10px;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);border-bottom:1px solid var(--rule)">Projects</div>
2035
+ <template x-if="projects.length === 0">
2036
+ <div style="padding:16px 14px;font-size:12px;color:var(--ink-3)">No projects</div>
2037
+ </template>
2038
+ <template x-for="proj in projects" :key="proj.dirName">
2039
+ <div @click="openProject(proj)" style="padding:9px 14px;border-bottom:1px solid var(--rule);cursor:pointer;transition:background 0.1s" @mouseenter="$el.style.background='var(--canvas)'" @mouseleave="$el.style.background=''">
2040
+ <div style="display:flex;align-items:baseline;gap:5px;margin-bottom:2px">
2041
+ <span style="font-size:12.5px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1" x-text="shortProjectName(proj.projectPath) || proj.dirName"></span>
2042
+ <template x-if="!proj.diskExists">
2043
+ <span style="font-size:9px;color:var(--red);flex-shrink:0">missing</span>
2044
+ </template>
2045
+ </div>
2046
+ <div style="display:flex;align-items:center;gap:6px;font-size:11px;color:var(--ink-3)">
2047
+ <span x-text="proj.sessionCount + ' sessions'"></span>
2048
+ <template x-if="projectCost(proj.dirName) !== null">
2049
+ <span x-text="fmtCost(projectCost(proj.dirName))" style="font-family:'SF Mono','Fira Code',monospace"></span>
2050
+ </template>
2051
+ </div>
2052
+ <div style="font-size:10.5px;color:var(--ink-4);margin-top:1px" x-text="proj.lastActive ? formatDate(proj.lastActive) : ''"></div>
2053
+ </div>
2054
+ </template>
2055
+ </div><!-- /right column -->
2056
+
2057
+ </div><!-- /two-col layout -->
1781
2058
  </div>
1782
2059
  </div>
1783
2060
  </template>
@@ -3981,7 +4258,7 @@
3981
4258
  { cmd: '/insights', desc: 'Session analytics report' },
3982
4259
  { cmd: '/keybindings', desc: 'Open keybindings file' },
3983
4260
  { cmd: '/login', desc: 'Sign in to Anthropic' },
3984
- { cmd: '/logout', desc: 'Sign out de Anthropic' },
4261
+ { cmd: '/logout', desc: 'Sign out from Anthropic' },
3985
4262
  { cmd: '/mcp', desc: 'Manage MCP connections and OAuth' },
3986
4263
  { cmd: '/memory', desc: 'Edit CLAUDE.md and manage auto-memory' },
3987
4264
  { cmd: '/model [model]', desc: 'Select or change AI model' },
@@ -4005,6 +4282,16 @@
4005
4282
  { cmd: '/vim', desc: 'Toggle Vim and Normal modes' },
4006
4283
  { cmd: '/voice', desc: 'Enable voice dictation (push to talk)' },
4007
4284
  ],
4285
+ selectedProject: null,
4286
+ projectSessions: [],
4287
+ projectSessionsLoading: false,
4288
+ projectMemory: [],
4289
+ projectMemorySelected: null,
4290
+ projectMemoryLoading: false,
4291
+ projectClaudeMd: null,
4292
+ projectClaudeMdLoading: false,
4293
+ projectClaudeMdEdit: { active: false, fileIdx: 0, draft: '', saving: false, msg: '' },
4294
+ projectTab: 'sessions',
4008
4295
  sessionsTab: 'list',
4009
4296
  knowledgeTab: 'memory',
4010
4297
  setupTab: 'commands',
@@ -4042,6 +4329,7 @@
4042
4329
 
4043
4330
  initView(v) {
4044
4331
  if (v === 'dashboard') { this.loadStats(); this.loadInsights(); }
4332
+ if (v === 'projects') { this.loadProjects(); }
4045
4333
  if (v === 'memory') { this.loadMemory(); }
4046
4334
  if (v === 'plans') { this.loadPlans(); }
4047
4335
  if (v === 'commands' || v === 'skills') { this.loadTools(); }
@@ -4055,6 +4343,67 @@
4055
4343
  this.totalSessions = data.reduce((s, p) => s + p.sessionCount, 0);
4056
4344
  },
4057
4345
 
4346
+ async openProject(proj) {
4347
+ this.view = 'projects';
4348
+ this.selectedProject = proj;
4349
+ this.projectTab = 'sessions';
4350
+ this.projectMemorySelected = null;
4351
+ this.projectClaudeMd = null;
4352
+ this.projectClaudeMdEdit = { active: false, fileIdx: 0, draft: '', saving: false, msg: '' };
4353
+ this.loadProjectSessions();
4354
+ this.loadProjectMemory();
4355
+ this.loadProjectClaudeMd();
4356
+ },
4357
+
4358
+ async loadProjectSessions() {
4359
+ if (!this.selectedProject) return;
4360
+ this.projectSessionsLoading = true;
4361
+ this.projectSessions = await fetch(`/api/sessions?project=${this.selectedProject.dirName}`).then(r => r.json());
4362
+ this.projectSessionsLoading = false;
4363
+ },
4364
+
4365
+ async loadProjectMemory() {
4366
+ if (!this.selectedProject) return;
4367
+ this.projectMemoryLoading = true;
4368
+ this.projectMemory = await fetch(`/api/memory?project=${this.selectedProject.dirName}`).then(r => r.json());
4369
+ this.projectMemoryLoading = false;
4370
+ },
4371
+
4372
+ async loadProjectClaudeMd() {
4373
+ if (!this.selectedProject) return;
4374
+ this.projectClaudeMdLoading = true;
4375
+ this.projectClaudeMd = await fetch(`/api/projects/${this.selectedProject.dirName}/claude-md`).then(r => r.json());
4376
+ this.projectClaudeMdLoading = false;
4377
+ },
4378
+
4379
+ async saveProjectClaudeMd() {
4380
+ if (!this.selectedProject || !this.projectClaudeMd) return;
4381
+ const file = this.projectClaudeMd.files[this.projectClaudeMdEdit.fileIdx];
4382
+ if (!file) return;
4383
+ this.projectClaudeMdEdit.saving = true;
4384
+ this.projectClaudeMdEdit.msg = '';
4385
+ try {
4386
+ const res = await fetch(`/api/projects/${this.selectedProject.dirName}/claude-md`, {
4387
+ method: 'PUT',
4388
+ headers: { 'Content-Type': 'application/json' },
4389
+ body: JSON.stringify({ filePath: file.filePath, content: this.projectClaudeMdEdit.draft }),
4390
+ });
4391
+ if (!res.ok) throw new Error((await res.json()).error || 'Error');
4392
+ file.content = this.projectClaudeMdEdit.draft;
4393
+ this.projectClaudeMdEdit.active = false;
4394
+ this.projectClaudeMdEdit.msg = 'Saved';
4395
+ } catch (e) {
4396
+ this.projectClaudeMdEdit.msg = e.message;
4397
+ }
4398
+ this.projectClaudeMdEdit.saving = false;
4399
+ },
4400
+
4401
+ projectCost(dirName) {
4402
+ if (!this.insights) return null;
4403
+ const entry = this.insights.byProject?.find(p => p.dir === dirName);
4404
+ return entry ? entry.cost : null;
4405
+ },
4406
+
4058
4407
  async loadBranches() {
4059
4408
  const params = this.filterProject ? `?project=${this.filterProject}` : '';
4060
4409
  this.branches = await fetch(`/api/branches${params}`).then(r => r.json());
@@ -4104,7 +4453,7 @@
4104
4453
  setTimeout(() => { btn.textContent = orig; btn.classList.remove('btn-copied', 'copied'); }, 2000);
4105
4454
  }
4106
4455
  } catch {
4107
- prompt('Copia este comando:', cmd);
4456
+ prompt('Copy this command:', cmd);
4108
4457
  }
4109
4458
  },
4110
4459
 
package/server.js CHANGED
@@ -406,20 +406,95 @@ function loadMemoryFiles(dirName) {
406
406
 
407
407
  app.use(express.static(path.join(__dirname, 'public')));
408
408
 
409
+ // ─── Project helpers ─────────────────────────────────────────────────────────
410
+
411
+ async function getProjectPath(dirName) {
412
+ // 1. Try sessions-index.json originalPath (most reliable)
413
+ const indexPath = path.join(PROJECTS_DIR, dirName, 'sessions-index.json');
414
+ try {
415
+ const idx = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
416
+ if (idx.originalPath) return idx.originalPath;
417
+ } catch {}
418
+ // 2. Try projectPath from loaded entries
419
+ const entries = await loadSessionIndex(dirName);
420
+ if (entries[0]?.projectPath) return entries[0].projectPath;
421
+ // 3. Try cwd field from first few lines of any available JSONL
422
+ const dir = path.join(PROJECTS_DIR, dirName);
423
+ try {
424
+ const jsonlFiles = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
425
+ for (const f of jsonlFiles.slice(0, 3)) {
426
+ const rl = readline.createInterface({ input: fs.createReadStream(path.join(dir, f)), crlfDelay: Infinity });
427
+ let scanned = 0;
428
+ for await (const line of rl) {
429
+ if (!line.trim()) continue;
430
+ try {
431
+ const obj = JSON.parse(line);
432
+ if (obj.cwd) { rl.close(); return obj.cwd; }
433
+ } catch {}
434
+ if (++scanned >= 20) break; // scan up to 20 lines per file
435
+ }
436
+ rl.close();
437
+ }
438
+ } catch {}
439
+ return '';
440
+ }
441
+
409
442
  // GET /api/projects
410
443
  app.get('/api/projects', async (req, res) => {
411
444
  const dirs = getProjectDirs();
412
445
  const results = await Promise.all(dirs.map(async dirName => {
413
446
  const entries = await loadSessionIndex(dirName);
414
- const projectPath = entries[0]?.projectPath || dirName.replace(/-/g, '/').replace('/Users/', '/Users/');
447
+ const projectPath = await getProjectPath(dirName);
415
448
  const lastActive = entries.length
416
449
  ? entries.reduce((max, e) => e.modified > max ? e.modified : max, '')
417
450
  : null;
418
- return { dirName, projectPath, sessionCount: entries.length, lastActive };
451
+ const memDir = path.join(PROJECTS_DIR, dirName, 'memory');
452
+ const memoryCount = fs.existsSync(memDir)
453
+ ? fs.readdirSync(memDir).filter(f => f.endsWith('.md') && !f.endsWith('.bak')).length
454
+ : 0;
455
+ const diskExists = projectPath ? fs.existsSync(projectPath) : false;
456
+ const branches = [...new Set(entries.map(e => e.gitBranch).filter(Boolean))];
457
+ return { dirName, projectPath, sessionCount: entries.length, lastActive, memoryCount, diskExists, branches };
419
458
  }));
420
459
  res.json(results.filter(p => p.sessionCount > 0).sort((a, b) => (b.lastActive || '').localeCompare(a.lastActive || '')));
421
460
  });
422
461
 
462
+ // GET /api/projects/:dirName/claude-md
463
+ app.get('/api/projects/:dirName/claude-md', async (req, res) => {
464
+ const { dirName } = req.params;
465
+ const projectPath = await getProjectPath(dirName);
466
+ if (!projectPath) return res.json({ projectPath: '', files: [] });
467
+ const candidates = [
468
+ { label: 'CLAUDE.md', p: path.join(projectPath, 'CLAUDE.md') },
469
+ { label: '.claude/CLAUDE.md', p: path.join(projectPath, '.claude', 'CLAUDE.md') },
470
+ { label: '.claude/CLAUDE.local.md', p: path.join(projectPath, '.claude', 'CLAUDE.local.md') },
471
+ ];
472
+ const files = [];
473
+ for (const { label, p } of candidates) {
474
+ if (fs.existsSync(p)) files.push({ label, filePath: p, content: fs.readFileSync(p, 'utf8') });
475
+ }
476
+ res.json({ projectPath, files });
477
+ });
478
+
479
+ // PUT /api/projects/:dirName/claude-md
480
+ app.put('/api/projects/:dirName/claude-md', async (req, res) => {
481
+ const { dirName } = req.params;
482
+ const { filePath, content } = req.body;
483
+ if (!filePath || typeof content !== 'string') return res.status(400).json({ error: 'Missing filePath or content' });
484
+ const projectPath = await getProjectPath(dirName);
485
+ if (!projectPath) return res.status(404).json({ error: 'Project not found' });
486
+ // Security: filePath must be within projectPath
487
+ const resolved = path.resolve(filePath);
488
+ const base = path.resolve(projectPath);
489
+ if (!resolved.startsWith(base + path.sep) && resolved !== base) {
490
+ return res.status(403).json({ error: 'Path not allowed' });
491
+ }
492
+ try {
493
+ safeWrite(resolved, content);
494
+ res.json({ ok: true });
495
+ } catch (e) { res.status(500).json({ error: e.message }); }
496
+ });
497
+
423
498
  // GET /api/sessions
424
499
  app.get('/api/sessions', async (req, res) => {
425
500
  const { project, branch, search, from, to } = req.query;