Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ Always run `make check` locally before pushing.
internal/
├── board/ # YAML kanban boards, task CRUD, history, discovery
├── runner/ # Agent spawning, ralph loop, worktree isolation, tracker
├── mcp/ # MCP server (13+ tools: sessions, boards, costs, inventory)
├── mcp/ # MCP server (14+ tools: sessions, boards, costs, inventory, search_memory)
├── memory/ # FTS5 cross-tool transcript index (search_memory + /api/memory/search)
├── web/ # HTTP server, REST API, scan cache, SPA handler
├── provider/ # Session scanners (claude, codex, copilot, gemini, opencode, cursor)
├── provider/ # Session scanners (claude, claude-desktop, codex, copilot, gemini, opencode, cursor, antigravity)
├── costs/ # Token pricing, cost estimation, aggregation
├── inventory/ # Tool/extension/MCP/config scanning
├── audit/ # MCP tool call audit log
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ That's it. VibeCockpit auto-detects which tools you have installed and shows all
## Features

- **Dashboard** — overview of all tools with session counts, active indicators, and recent activity
- **Cross-tool memory** — full-text search across the *content* of every transcript across every tool (FTS5-backed), exposed via the web UI, the `vibecockpit memory search` CLI, and the `search_memory` MCP tool. Any agent connected via MCP can recall what you (or a past agent) said in any prior conversation — Claude Code → Cursor → Codex all share the same memory substrate. Index file (`~/.config/vibecockpit/cache/memory.db`) can be `export`ed and `import`ed to ship your memory to another machine.
- **Cost tracking** — see what your AI coding tools cost you across all providers. Heatmap, trend charts, per-project breakdown, and plan recommendations that tell you which subscription tier actually fits your usage
- **Fuzzy search** — type `hermes` or `model:opus branch:main active` to find anything
- **Filter chips** — click to filter by tool, model, or active status
Expand Down Expand Up @@ -266,6 +267,13 @@ vibecockpit [flags]
--autostart Start on login (systemd/launchd)
--remove-autostart Remove autostart service
--yes Skip confirmation prompts

vibecockpit memory <subcommand>
reindex Walk every provider, write transcript content into the FTS5 index
search <query> FTS5 search (porter stemmer; supports prefix, phrase, AND/OR/NOT)
stats Document count + index size on disk
export <path> Snapshot the index to <path> (e.g. for moving to another machine)
import <path> Merge another machine's memory.db; local entries win on conflict
```

## License
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { saveConfig, launchSession, deleteSession, createProject as apiCreateProject, testSSH, startSecretScan, getScanStatus } from "./lib/api.js";
import { shortModel, relativeTime, providerColors, providerLabels, dateGroup } from "./lib/utils.js";
import Dashboard from "./components/Dashboard.svelte";
import Memory from "./components/Memory.svelte";
import CostsDashboard from "./components/CostsDashboard.svelte";
import ToolInventory from "./components/ToolInventory.svelte";
import BoardView from "./components/BoardView.svelte";
Expand Down Expand Up @@ -762,6 +763,10 @@
<span class="sidebar-label">Sessions</span>
{#if activeSessionCount > 0}<span class="sidebar-count sidebar-count-active">{activeSessionCount}</span>{/if}
</button>
<button class="sidebar-btn" class:active={page === "memory"} onclick={() => navigateTo("memory")} title="Full-text search across every transcript">
<svg class="sidebar-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/><path d="M11 8v3l2 2" stroke-linecap="round"/></svg>
<span class="sidebar-label">Memory</span>
</button>
<button class="sidebar-btn" class:active={page === "costs"} onclick={() => navigateTo("costs")}>
<svg class="sidebar-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>
<span class="sidebar-label">Costs</span>
Expand Down Expand Up @@ -1143,6 +1148,8 @@
<main class="chat-main-container">
<ChatView />
</main>
{:else if page === "memory"}
<Memory onlaunch={(id, prov) => launch(id, prov)} />
{:else if page === "costs"}
<div class="page-bar">
<h1 class="page-bar-title">Costs</h1>
Expand Down
66 changes: 66 additions & 0 deletions frontend/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,72 @@ code{
.version-update{color:#fff;background:var(--primary);cursor:pointer}
.version-update:hover{filter:brightness(1.1)}

/* ─── Memory page ─── */
.memory-page{padding:1.2rem 2rem 3rem;display:flex;flex-direction:column;gap:1rem;max-width:1100px}
.memory-stats{font-size:.78rem;color:var(--text-secondary)}
.memory-warning{background:var(--danger-dim);border:1px solid var(--danger);color:var(--danger);padding:.7rem 1rem;border-radius:var(--radius-sm);font-size:.85rem}
.memory-search{display:flex;flex-direction:column;gap:.6rem;padding:1rem;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow)}
.memory-search-row{display:flex;gap:.5rem;align-items:center}
.memory-input{flex:1;padding:.6rem .85rem;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:.95rem;outline:none;font-family:inherit}
.memory-input:focus{border-color:var(--primary)}
.memory-input-sm{flex:0 1 220px;font-size:.85rem;padding:.45rem .7rem}
.memory-filters{flex-wrap:wrap}
.memory-tips{font-size:.72rem;color:var(--text-secondary);padding:.2rem .1rem 0}
.memory-tips code{font-size:.7rem;padding:.1rem .35rem}
.memory-summary{font-size:.85rem;color:var(--text-secondary)}
.memory-summary strong{color:var(--text)}
.memory-results{display:flex;flex-direction:column;gap:.6rem}
.memory-result{display:flex;flex-direction:column;gap:.35rem;text-align:left;padding:.85rem 1rem;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;color:inherit;font-family:inherit;transition:border-color var(--transition),background var(--transition)}
.memory-result:hover{border-color:var(--primary-dim);background:var(--surface-hover)}
.memory-result-head{display:flex;align-items:center;gap:.6rem;font-size:.78rem;color:var(--text-secondary)}
.memory-pill{display:inline-flex;align-items:center;gap:.3rem;padding:.15rem .55rem;border-radius:999px;background:color-mix(in srgb,var(--chip-color) 12%,transparent);color:var(--chip-color);font-weight:600;font-size:.7rem;letter-spacing:.02em}
.memory-pill-dot{width:.45rem;height:.45rem;border-radius:50%;background:var(--chip-color)}
.memory-project{color:var(--text);font-weight:600;font-size:.82rem}
.memory-branch{font-family:"SF Mono",Menlo,monospace;font-size:.72rem;background:var(--surface-active);padding:.05rem .35rem;border-radius:4px}
.memory-spacer{flex:1}
.memory-time{font-size:.72rem;color:var(--text-secondary)}
.memory-summary-line{font-weight:500;color:var(--text);font-size:.92rem;line-height:1.35}
.memory-snippet{font-size:.85rem;color:var(--text-secondary);line-height:1.45;white-space:pre-wrap;word-break:break-word}
.memory-snippet mark{background:color-mix(in srgb,var(--primary) 30%,transparent);color:var(--text);border-radius:2px;padding:0 .15rem}
.memory-meta{display:flex;gap:.7rem;align-items:center;font-size:.7rem;color:var(--text-secondary);margin-top:.15rem}
.memory-id{font-family:"SF Mono",Menlo,monospace;opacity:.6}
.memory-msgcount{font-size:.72rem;color:var(--text-secondary);font-variant-numeric:tabular-nums}
.memory-host{display:inline-flex;align-items:center;gap:.25rem;font-size:.7rem;color:var(--text-secondary);background:var(--surface-active);padding:.05rem .4rem;border-radius:4px;font-family:"SF Mono",Menlo,monospace}
.memory-host svg{opacity:.7}
.memory-delete{display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;border-radius:4px;border:1px solid transparent;background:transparent;color:var(--text-secondary);font-size:1rem;line-height:1;cursor:pointer;padding:0;transition:background var(--transition),color var(--transition),border-color var(--transition)}
.memory-delete:hover{background:color-mix(in srgb,var(--danger,#ef4444) 14%,transparent);color:var(--danger,#ef4444);border-color:color-mix(in srgb,var(--danger,#ef4444) 30%,transparent)}
.memory-toast{padding:.55rem .85rem;background:color-mix(in srgb,var(--primary) 10%,var(--surface));border:1px solid color-mix(in srgb,var(--primary) 30%,var(--border));border-radius:var(--radius-sm);font-size:.85rem;color:var(--text);margin-bottom:.6rem}
.memory-view-toggle{display:inline-flex;gap:.25rem}
.memory-excluded-intro{font-size:.85rem;color:var(--text-secondary);background:var(--surface);border:1px dashed var(--border);border-radius:var(--radius-sm);padding:.6rem .85rem;margin-bottom:.6rem;line-height:1.4}
.memory-result-excluded{opacity:.85}
.memory-result-excluded:hover{border-color:var(--border);background:var(--surface)}

/* ─── Memory drawer ─── */
.memory-drawer-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);backdrop-filter:blur(2px);z-index:200;animation:fade .15s ease;border:0;padding:0;margin:0;cursor:pointer}
.memory-drawer{position:fixed;top:0;right:0;bottom:0;width:min(640px,90vw);background:var(--surface);border-left:1px solid var(--border);box-shadow:-12px 0 36px rgba(0,0,0,.18);z-index:201;display:flex;flex-direction:column;animation:slide-in .2s ease}
@keyframes slide-in{from{transform:translateX(100%)}to{transform:translateX(0)}}
@keyframes fade{from{opacity:0}to{opacity:1}}
.memory-drawer-head{display:flex;align-items:center;gap:.75rem;padding:.95rem 1rem;border-bottom:1px solid var(--border)}
.memory-drawer-title{display:flex;align-items:center;gap:.6rem;flex:1;font-size:.95rem}
.memory-drawer-summary{padding:.6rem 1rem;font-size:.85rem;color:var(--text);background:var(--surface-hover);border-bottom:1px solid var(--border)}
.memory-drawer-actions{display:flex;align-items:center;gap:.6rem;padding:.6rem 1rem;border-bottom:1px solid var(--border);font-size:.78rem;color:var(--text-secondary)}
.memory-drawer-meta{font-variant-numeric:tabular-nums}
.memory-drawer-spacer{flex:1}
.memory-drawer-window{display:flex;align-items:center;gap:.3rem}
.memory-drawer-window span{font-size:.75rem;color:var(--text-secondary);margin-right:.25rem}
.memory-drawer-body{flex:1;overflow-y:auto;padding:.5rem 1rem;display:flex;flex-direction:column;gap:.65rem}
.memory-drawer-empty{text-align:center;color:var(--text-secondary);padding:2rem 1rem;font-size:.9rem}
.memory-msg{padding:.65rem .8rem;border-radius:var(--radius-sm);background:var(--bg);border:1px solid var(--border);font-size:.88rem;line-height:1.45}
.memory-msg-user{border-left:3px solid var(--text-secondary)}
.memory-msg-assistant{border-left:3px solid var(--primary)}
.memory-msg-center{background:color-mix(in srgb,var(--primary) 8%,var(--bg));border-color:var(--primary-dim)}
.memory-msg-head{display:flex;align-items:center;gap:.5rem;font-size:.7rem;color:var(--text-secondary);margin-bottom:.3rem;text-transform:uppercase;letter-spacing:.04em}
.memory-msg-role{font-weight:700}
.memory-msg-idx{font-variant-numeric:tabular-nums}
.memory-msg-time{font-style:italic;text-transform:none;letter-spacing:0}
.memory-msg-content{white-space:pre-wrap;word-break:break-word;color:var(--text)}
.memory-drawer-foot{display:flex;align-items:center;gap:.5rem;padding:.7rem 1rem;border-top:1px solid var(--border)}

/* ─── App layout ─── */
.app-content{margin-left:200px;min-height:100vh}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ChatView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
let knownProjects = $state([]);
let modelsByProvider = $state({});
let allModels = $state([]);
let messagesEnd;
let messagesEnd = $state();

async function loadSessionData() {
try {
Expand Down
Loading
Loading