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,508 +1,9 @@
|
||||
# Obelisk Recap Patterns
|
||||
# Obelisk Recap Retrieval Patterns
|
||||
|
||||
Use this when Obelisk Intent Routing sends the `recap` intent. In practice,
|
||||
that means the first word after `/obelisk` is `recap`; everything after it is
|
||||
the recap target.
|
||||
Compatibility pointer.
|
||||
|
||||
Do not self-trigger this reference from broad weekly/monthly summaries, charts,
|
||||
rankings, shareable-card language, or playlist-style metaphors. Those are target
|
||||
details only after the explicit `recap` intent has already selected this file.
|
||||
|
||||
This is an optional app handoff pattern. Obelisk stays agent-first: the agent
|
||||
queries sessions and memories, judges what matters, then fills card content.
|
||||
The app owns rendering, layout, export, and animation.
|
||||
|
||||
## Common Period Targets
|
||||
|
||||
Treat the target after `recap` as normal user language. Common app-generated
|
||||
targets:
|
||||
|
||||
- `/obelisk recap this week` -- the current calendar week in the user's timezone.
|
||||
- `/obelisk recap last week` -- the previous calendar week.
|
||||
- `/obelisk recap this month` -- the current calendar month.
|
||||
- `/obelisk recap last month` -- the previous calendar month.
|
||||
|
||||
Use the current date and timezone from the runtime/session context when
|
||||
available. If the target contains extra style language, keep the period meaning
|
||||
and treat the rest as presentation guidance.
|
||||
|
||||
## Contract
|
||||
|
||||
- Produce card content, not HTML, CSS, SVG, or layout instructions.
|
||||
- Keep the first result share-safe. Avoid secrets, tokens, private URLs, long
|
||||
absolute paths, raw tool outputs, and embarrassing private text unless the
|
||||
user asked for private analysis.
|
||||
- Concrete numbers, exact times, quotes, and verdicts need evidence. If you
|
||||
cannot support a detail, omit it or soften it.
|
||||
- Memory is prior notes, not ground truth. If a card uses a memory conclusion,
|
||||
say in prose that it came from a prior memory when answering in chat, and add
|
||||
`memory_id` evidence when producing JSON.
|
||||
- Do not propose a durable memory just because you generated a recap. Propose
|
||||
memory only if the retrieval reveals a reusable cross-session conclusion not
|
||||
already covered by `memories()`.
|
||||
|
||||
## Retrieval Shape
|
||||
|
||||
Start with orientation and bounded evidence. Use English memory queries even
|
||||
when the recap text will be in another language.
|
||||
|
||||
```js
|
||||
const period = {
|
||||
label: 'Week 24',
|
||||
after: '2026-06-08T00:00:00+08:00',
|
||||
before: '2026-06-15T00:00:00+08:00',
|
||||
timezone: 'Asia/Shanghai',
|
||||
};
|
||||
|
||||
const map = overview({ limit: 8 });
|
||||
const project = map.current.project?.project;
|
||||
const scoped = project ? { project } : {};
|
||||
|
||||
return {
|
||||
current: map.current,
|
||||
current_project: map.current_project,
|
||||
prior_memories: memories({
|
||||
...scoped,
|
||||
query: 'weekly recap project decisions workflows debugging shipping',
|
||||
limit: 8,
|
||||
}).map(m => ({
|
||||
id: m.id,
|
||||
path: m.path,
|
||||
anchors: m.anchors,
|
||||
session_id: m.session_id,
|
||||
created_at: m.created_at,
|
||||
summary: m.summary?.slice(0, 280),
|
||||
})),
|
||||
sessions: sessions({
|
||||
...scoped,
|
||||
after: period.after,
|
||||
before: period.before,
|
||||
limit: 40,
|
||||
}).map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
project: s.project,
|
||||
branch: s.git_branch,
|
||||
started_at: s.started_at,
|
||||
ended_at: s.ended_at,
|
||||
message_count: s.message_count,
|
||||
})),
|
||||
summaries: summaries({
|
||||
...scoped,
|
||||
after: period.after,
|
||||
before: period.before,
|
||||
limit: 30,
|
||||
}).map(s => ({
|
||||
id: s.id,
|
||||
session_id: s.session_id,
|
||||
session_title: s.session_title,
|
||||
project: s.project,
|
||||
timestamp: s.timestamp,
|
||||
snippet: s.content?.slice(0, 260),
|
||||
})),
|
||||
workflows: workflows({
|
||||
...scoped,
|
||||
after: period.after,
|
||||
before: period.before,
|
||||
limit: 20,
|
||||
}).map(w => ({
|
||||
run_id: w.run_id,
|
||||
session_id: w.session_id,
|
||||
name: w.workflow_name,
|
||||
status: w.status,
|
||||
agent_count: w.agent_count,
|
||||
tokens: w.total_tokens,
|
||||
timestamp: w.timestamp,
|
||||
})),
|
||||
failures: failures({
|
||||
...scoped,
|
||||
after: period.after,
|
||||
before: period.before,
|
||||
limit: 12,
|
||||
}).map(f => ({
|
||||
session_id: f.session_id,
|
||||
session_title: f.session_title,
|
||||
tool: f.tool_name,
|
||||
timestamp: f.timestamp,
|
||||
snippet: f.content?.slice(0, 180),
|
||||
})),
|
||||
};
|
||||
```
|
||||
|
||||
Use SQL for exact aggregate numbers after the helper-first pass has established
|
||||
the scope. Keep meta rows out of ordinary user-visible counts.
|
||||
|
||||
```js
|
||||
const project = '%quiet-zero%';
|
||||
const after = '2026-06-08T00:00:00+08:00';
|
||||
const before = '2026-06-15T00:00:00+08:00';
|
||||
|
||||
const metrics = sql(`
|
||||
SELECT
|
||||
COUNT(DISTINCT s.id) AS sessions,
|
||||
COUNT(m.uuid) AS messages,
|
||||
COALESCE(SUM(COALESCE(m.input_tokens, 0) + COALESCE(m.output_tokens, 0)), 0) AS tokens,
|
||||
COUNT(DISTINCT substr(m.timestamp, 1, 10)) AS active_days
|
||||
FROM sessions s
|
||||
LEFT JOIN messages m ON m.session_id = s.id
|
||||
WHERE s.project LIKE ?
|
||||
AND COALESCE(m.is_meta, 0) = 0
|
||||
AND m.timestamp >= ?
|
||||
AND m.timestamp < ?
|
||||
`, project, after, before)[0];
|
||||
|
||||
const user_messages = sql(`
|
||||
SELECT
|
||||
m.uuid,
|
||||
m.session_id,
|
||||
s.title AS session_title,
|
||||
m.timestamp,
|
||||
substr(m.text, 1, 500) AS text
|
||||
FROM messages m
|
||||
JOIN sessions s ON s.id = m.session_id
|
||||
WHERE s.project LIKE ?
|
||||
AND m.type = 'user'
|
||||
AND m.content_type = 'text'
|
||||
AND COALESCE(m.is_meta, 0) = 0
|
||||
AND m.timestamp >= ?
|
||||
AND m.timestamp < ?
|
||||
ORDER BY m.timestamp
|
||||
LIMIT 300
|
||||
`, project, after, before);
|
||||
|
||||
return { metrics, user_messages };
|
||||
```
|
||||
|
||||
## Archetypes
|
||||
|
||||
Choose one dominant archetype from the evidence. Do not force all cards to match
|
||||
it; the archetype is the cover persona and tone baseline.
|
||||
|
||||
| archetype | essential action | tone baseline |
|
||||
|---|---|---|
|
||||
| `architect` | establishes structure, boundaries, and systems from above | matter-of-fact, structural pride |
|
||||
| `debugger` | loops through symptoms until root cause becomes visible | wry, weary, occasional dark humor |
|
||||
| `shipper` | keeps pushing forward with dense cadence | energetic, slightly breathless |
|
||||
| `curator` | collects, organizes, refines, and distills | reflective, low-key clarity |
|
||||
| `director` | coordinates many threads from the center outward | observant, slight remove |
|
||||
| `cartographer` | reorganizes known structure, redraws maps, moves boundaries | patient, surveyor-like |
|
||||
| `wanderer` | crosses projects without one obvious center, leaving traces | gentle, accepting, no apology |
|
||||
|
||||
Selection hints:
|
||||
|
||||
- Use `architect` when the week is dominated by system design, APIs, schemas,
|
||||
boundaries, or core concepts.
|
||||
- Use `debugger` when repeated failures, false positives, regressions, or root
|
||||
cause hunts dominate.
|
||||
- Use `shipper` when the evidence shows sustained implementation velocity,
|
||||
builds, releases, or many completed edits.
|
||||
- Use `curator` when the work is mostly cleanup, documentation, memory,
|
||||
organization, or taste/refinement.
|
||||
- Use `director` when workflows, subagents, review loops, or multi-agent
|
||||
coordination are the main story.
|
||||
- Use `cartographer` when the user moves modules, redraws information
|
||||
architecture, re-scopes boundaries, or turns "this lives here" into "this
|
||||
belongs there".
|
||||
- Use `wanderer` when the period spans many unrelated projects and the honest
|
||||
story is exploratory rather than centered.
|
||||
|
||||
## JSON Shape
|
||||
|
||||
The schema is deliberately card-content oriented. Keep keys stable, but let
|
||||
strings carry the style. Card text can use the user's language; field names stay
|
||||
English.
|
||||
|
||||
For weekly recaps, `metrics.active_days` and cover `activity` should contain 7
|
||||
numbers ordered Monday through Sunday, where `0` means no visible activity and
|
||||
`1` means active. For monthly recaps, use one value per calendar day in the
|
||||
period, or omit the field if the evidence is too thin.
|
||||
|
||||
```ts
|
||||
type Recap = {
|
||||
schema_version: "obelisk.recap.v1";
|
||||
kind: "weekly" | "monthly";
|
||||
generated_at: string;
|
||||
|
||||
period: {
|
||||
label: string;
|
||||
start: string;
|
||||
end: string;
|
||||
timezone: string;
|
||||
};
|
||||
|
||||
source: {
|
||||
project?: string;
|
||||
session_ids: string[];
|
||||
memory_ids?: string[];
|
||||
};
|
||||
|
||||
metrics: {
|
||||
sessions?: number;
|
||||
messages?: number;
|
||||
tokens?: number;
|
||||
active_days?: number[];
|
||||
streak_days?: number;
|
||||
workflows?: number;
|
||||
workflow_agents?: number;
|
||||
corrections?: number;
|
||||
};
|
||||
|
||||
persona: {
|
||||
archetype:
|
||||
| "architect"
|
||||
| "debugger"
|
||||
| "shipper"
|
||||
| "curator"
|
||||
| "director"
|
||||
| "cartographer"
|
||||
| "wanderer";
|
||||
title: string;
|
||||
subtitle: string;
|
||||
tone: string;
|
||||
};
|
||||
|
||||
cards: [
|
||||
CoverCard,
|
||||
ThinkingPathCard,
|
||||
VibeCard,
|
||||
WorkflowOrToolsCard,
|
||||
ClosingCard
|
||||
];
|
||||
|
||||
evidence?: Evidence[];
|
||||
};
|
||||
```
|
||||
|
||||
### Card 1: Cover
|
||||
|
||||
```ts
|
||||
type CoverCard = {
|
||||
type: "cover";
|
||||
badge: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
activity: number[];
|
||||
footer: string;
|
||||
evidence_refs?: string[];
|
||||
};
|
||||
```
|
||||
|
||||
Use the cover to name the period and the persona. The title can be an archetype
|
||||
label such as "The Architect"; the subtitle should summarize the period's real
|
||||
dominant work in one line.
|
||||
|
||||
### Card 2: Thinking Path
|
||||
|
||||
```ts
|
||||
type ThinkingPathCard = {
|
||||
type: "thinking_path";
|
||||
title: string;
|
||||
items: Array<{
|
||||
day: string;
|
||||
prompt: string;
|
||||
outcome: string;
|
||||
evidence_refs?: string[];
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
Pick 3-6 turning points. A `prompt` is the question, friction, or task that
|
||||
started the path. An `outcome` is the decision, fix, or learned framing.
|
||||
|
||||
### Card 3: Vibe
|
||||
|
||||
```ts
|
||||
type VibeCard = {
|
||||
type: "vibe";
|
||||
title: string;
|
||||
observations: Array<{
|
||||
label: string;
|
||||
text: string;
|
||||
count?: number;
|
||||
time?: string;
|
||||
evidence_refs?: string[];
|
||||
}>;
|
||||
meter?: {
|
||||
label: string;
|
||||
value: number;
|
||||
caption: string;
|
||||
};
|
||||
quote?: {
|
||||
text: string;
|
||||
caption?: string;
|
||||
evidence_refs?: string[];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
This card can be playful, but it must stay grounded. Repeated phrases should be
|
||||
counted from visible user messages, not meta messages or tool results.
|
||||
|
||||
### Card 4: Workflows Or Tools
|
||||
|
||||
```ts
|
||||
type WorkflowOrToolsCard = {
|
||||
type: "workflow" | "tool_habits" | "debugging" | "shipping";
|
||||
title: string;
|
||||
summary?: string;
|
||||
stats?: string;
|
||||
items: Array<{
|
||||
name: string;
|
||||
outcome: string;
|
||||
evidence_refs?: string[];
|
||||
}>;
|
||||
verdict: string;
|
||||
};
|
||||
```
|
||||
|
||||
Choose the card type that best fits the period. Use `workflow` when workflow
|
||||
runs or subagents are the story; `debugging` when failures and fixes dominate;
|
||||
`shipping` when completed implementation dominates; `tool_habits` when the
|
||||
period is mostly about how the user worked.
|
||||
|
||||
### Card 5: Closing
|
||||
|
||||
```ts
|
||||
type ClosingCard = {
|
||||
type: "closing";
|
||||
headline: string;
|
||||
stats: string[];
|
||||
most_said_phrase?: string;
|
||||
signoff: string;
|
||||
evidence_refs?: string[];
|
||||
};
|
||||
```
|
||||
|
||||
Close with one high-signal stat and a short signoff. Avoid turning the closing
|
||||
card into a second summary.
|
||||
|
||||
### Evidence
|
||||
|
||||
```ts
|
||||
type Evidence = {
|
||||
id: string;
|
||||
session_id?: string;
|
||||
message_uuid?: string;
|
||||
memory_id?: string;
|
||||
summary?: string;
|
||||
};
|
||||
```
|
||||
|
||||
Evidence is for local traceability and app inspection. It does not have to be
|
||||
shown on exported cards. Prefer short summaries over raw snippets.
|
||||
|
||||
## Output Rules
|
||||
|
||||
- If the user asks for app handoff, return one JSON object and no surrounding
|
||||
prose.
|
||||
- If the user asks conversationally, answer with the card content naturally and
|
||||
include JSON only if useful.
|
||||
- Keep `cards.length === 5` in the order above.
|
||||
- Do not include unsupported cards just to fill space. It is better to make a
|
||||
quieter card than to invent drama.
|
||||
- Do not include raw SQL, query scripts, or private evidence in the shareable
|
||||
card text.
|
||||
|
||||
## Minimal Example
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "obelisk.recap.v1",
|
||||
"kind": "weekly",
|
||||
"generated_at": "2026-06-13T03:40:00+08:00",
|
||||
"period": {
|
||||
"label": "Week 24",
|
||||
"start": "2026-06-08",
|
||||
"end": "2026-06-14",
|
||||
"timezone": "Asia/Shanghai"
|
||||
},
|
||||
"source": {
|
||||
"project": "quiet-zero",
|
||||
"session_ids": ["sid-a", "sid-b"],
|
||||
"memory_ids": ["mem-a"]
|
||||
},
|
||||
"metrics": {
|
||||
"sessions": 12,
|
||||
"messages": 847,
|
||||
"tokens": 2400000,
|
||||
"active_days": [1, 1, 1, 0, 1, 1, 1],
|
||||
"workflows": 3,
|
||||
"workflow_agents": 42
|
||||
},
|
||||
"persona": {
|
||||
"archetype": "architect",
|
||||
"title": "The Architect",
|
||||
"subtitle": "Designed a memory system from raw sessions to durable notes.",
|
||||
"tone": "matter-of-fact, structural pride"
|
||||
},
|
||||
"cards": [
|
||||
{
|
||||
"type": "cover",
|
||||
"badge": "Week 24",
|
||||
"title": "The Architect",
|
||||
"subtitle": "Designed a full memory layer without turning sessions into a wiki.",
|
||||
"activity": [1, 1, 1, 0, 1, 1, 1],
|
||||
"footer": "12 sessions - 2.4M tokens",
|
||||
"evidence_refs": ["e1", "e2"]
|
||||
},
|
||||
{
|
||||
"type": "thinking_path",
|
||||
"title": "Your thinking path",
|
||||
"items": [
|
||||
{
|
||||
"day": "Mon",
|
||||
"prompt": "Why compile sessions into a wiki?",
|
||||
"outcome": "Kept raw SQLite as the evidence layer.",
|
||||
"evidence_refs": ["e1"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "vibe",
|
||||
"title": "Your vibe this week",
|
||||
"observations": [
|
||||
{
|
||||
"label": "Catchphrase",
|
||||
"text": "This is too ugly.",
|
||||
"count": 4,
|
||||
"evidence_refs": ["e3"]
|
||||
}
|
||||
],
|
||||
"meter": {
|
||||
"label": "Patience",
|
||||
"value": 0.8,
|
||||
"caption": "saint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "workflow",
|
||||
"title": "Are you a Workflow Enjoyer?",
|
||||
"stats": "3 workflows - 42 agents",
|
||||
"items": [
|
||||
{
|
||||
"name": "hono-plugin-review",
|
||||
"outcome": "perfect",
|
||||
"evidence_refs": ["e4"]
|
||||
}
|
||||
],
|
||||
"verdict": "Mostly tolerated"
|
||||
},
|
||||
{
|
||||
"type": "closing",
|
||||
"headline": "19 day streak",
|
||||
"stats": ["847 messages exchanged"],
|
||||
"most_said_phrase": "Okay, start doing it.",
|
||||
"signoff": "See you next week.",
|
||||
"evidence_refs": ["e5"]
|
||||
}
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"id": "e1",
|
||||
"session_id": "sid-a",
|
||||
"message_uuid": "msg-a",
|
||||
"summary": "The user chose raw SQLite as the evidence layer."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
The `/obelisk recap` flow now starts at `references/recap/overview.md`.
|
||||
Do not use this as an all-in-one retrieval document. The current flow is
|
||||
card-by-card: read the overview, then for each card read its `patternN-*.md`,
|
||||
retrieve that card's evidence, read its `writingN-*.md`, and update the JSON
|
||||
before moving on.
|
||||
|
||||
Reference in New Issue
Block a user