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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user