feat(app): add Settings view, configurable claude dir, and recap docs refactor

Introduce a Settings view for configuring the Claude data directory
  (with WSL auto-detection on Windows), sidebar project grouping module,
  and an empty-state onboarding screen for SessionList. Refactor
  recap-patterns.md into per-card reference files under references/recap/
  with separate retrieval and writing guides. Remove the legacy panel.html.
  On the data layer: incremental indexing via changedPaths, per-session
  live-update IPC (obelisk:session-updated), and session dirty-tracking
  in the renderer.
This commit is contained in:
tommy0103
2026-06-15 01:44:32 +08:00
parent 9fc7f202f0
commit b7506ee765
48 changed files with 1950 additions and 1406 deletions
+118 -2
View File
@@ -1,5 +1,5 @@
<script setup>
import { computed } from 'vue';
import { computed, ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { state } from '../store.js';
import { highlightPlain, escapeHTML, formatProjectLabel, fmtListTime, fmtRelative } from '../utils.js';
@@ -7,6 +7,17 @@ import { highlightPlain, escapeHTML, formatProjectLabel, fmtListTime, fmtRelativ
defineOptions({ name: 'SessionList' });
const router = useRouter();
const debugEmpty = ref(false);
function onKeydown(e) {
if (e.key === 'm' && !e.metaKey && !e.ctrlKey && e.target.tagName !== 'INPUT') {
debugEmpty.value = !debugEmpty.value;
}
}
onMounted(() => window.addEventListener('keydown', onKeydown));
onUnmounted(() => window.removeEventListener('keydown', onKeydown));
const homePath = (typeof process !== 'undefined' && process.env?.HOME) || '~';
const visibleSessions = computed(() => {
const q = state.query.trim().toLowerCase();
@@ -81,10 +92,47 @@ function obeliskStyle(session) {
<template>
<div class="session-list-wrap">
<div v-if="!visibleSessions.length" class="empty">
<!-- Empty state: no data source / debug toggle -->
<div v-if="debugEmpty || (!visibleSessions.length && !state.query)" class="empty-content">
<div class="empty-eyebrow">
<span class="diamond"></span>
<span>No data source connected</span>
</div>
<div class="empty-title">Obelisk reads your Claude Code session history.</div>
<div class="empty-body">
We didn't find <code>~/.claude</code> on this machine. If you've already used
Claude Code, point Obelisk at where its data lives in
<button class="inline-link" @click="router.push('/settings')">Settings</button>. If you haven't,
<strong>install Claude Code first</strong> Obelisk has nothing to read until
sessions exist.
</div>
<div class="empty-actions">
<button class="toolbar-action primary" @click="router.push('/settings')">
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round">
<path d="M2.5 3.5h3.5l1.2 1.2h4.3a1 1 0 0 1 1 1V11a1 1 0 0 1-1 1H2.5a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1z"/>
</svg>
Choose folder
</button>
</div>
<div class="empty-divider"></div>
<div class="empty-help">
<div class="help-row">
<span class="label">expected</span>
<code>~/.claude</code>
</div>
<div class="help-row">
<span class="label">searched</span>
<code>{{ homePath }}</code>
</div>
</div>
</div>
<!-- Empty state: search returned nothing -->
<div v-else-if="!visibleSessions.length" class="empty">
No sessions here.
<span class="hint">{{ state.query ? 'Try a different search term.' : 'Press / to search.' }}</span>
</div>
<div v-else class="session-list">
<div
v-for="s in visibleSessions"
@@ -116,6 +164,8 @@ function obeliskStyle(session) {
flex: 1;
overflow-y: auto;
min-height: 0;
display: flex;
flex-direction: column;
}
.srow {
@@ -217,4 +267,70 @@ function obeliskStyle(session) {
font-size: 11px;
color: var(--muted-2);
}
/* Onboarding empty state */
.empty-content {
flex: 1;
display: flex; flex-direction: column; gap: 16px;
max-width: 520px;
margin: 0 auto;
justify-content: center;
padding: 40px;
}
.empty-eyebrow {
display: flex; align-items: center; gap: 8px;
font-family: var(--font-mono); font-size: 11px;
color: var(--muted); letter-spacing: 0.04em;
}
.empty-eyebrow .diamond {
width: 6px; height: 6px;
background: var(--accent, #a78bfa); transform: rotate(45deg);
box-shadow: 0 0 6px rgba(167,139,250,0.4); flex-shrink: 0;
}
.empty-title {
font-family: var(--font-serif, Georgia); font-size: 22px;
font-weight: 500; color: var(--fg);
letter-spacing: -0.015em; line-height: 1.2;
}
.empty-body {
font-family: var(--font-serif, Georgia); font-style: italic;
font-size: 14px; color: var(--fg-3); line-height: 1.6; max-width: 460px;
}
.empty-body code {
font-family: var(--font-mono); font-style: normal; font-size: 12.5px;
color: var(--accent-2, #c4b5fd); background: rgba(167,139,250,0.12);
padding: 1px 6px; border-radius: 3px;
}
.empty-body strong { color: var(--fg); font-weight: 600; font-style: normal; }
.empty-body .inline-link {
color: var(--accent-2, #c4b5fd); background: none;
border: none; border-bottom: 1px solid rgba(167,139,250,0.4);
padding: 0 0 1px; font: inherit; cursor: pointer; transition: all 0.12s;
}
.empty-body .inline-link:hover { color: var(--accent, #a78bfa); border-bottom-color: var(--accent); }
.empty-actions { display: flex; gap: 8px; margin-top: 6px; }
.empty-actions .toolbar-action {
display: inline-flex; align-items: center; gap: 6px;
height: 32px; padding: 0 14px; border-radius: 5px;
font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.12s;
}
.empty-actions .toolbar-action.primary {
border: 1px solid rgba(167,139,250,0.35); background: rgba(167,139,250,0.12); color: #c4b5fd;
}
.empty-actions .toolbar-action.primary:hover {
background: rgba(167,139,250,0.18); border-color: #a78bfa; color: var(--fg);
box-shadow: 0 0 12px rgba(167,139,250,0.2);
}
.empty-actions .toolbar-action svg { width: 13px; height: 13px; }
.empty-divider { width: 100%; height: 1px; background: var(--hairline); margin: 6px 0; }
.empty-help {
display: flex; flex-direction: column; gap: 6px;
font-family: var(--font-mono); font-size: 11px; color: var(--muted);
}
.empty-help .help-row { display: flex; align-items: baseline; gap: 8px; }
.empty-help .help-row .label { color: var(--muted-2); letter-spacing: 0.04em; width: 76px; flex-shrink: 0; }
.empty-help code {
font-family: var(--font-mono); color: var(--fg-2);
background: rgba(0,0,0,0.3); padding: 1px 6px; border-radius: 3px;
}
</style>