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
21 changes: 21 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,27 @@ placing anything. Specifically:

A running log of durable decisions and feedback captured into the project config. Newest first.

- 2026-06-26 — **Build (guided-sessions expansion — fuller therapy/coaching + a Family group + catalog search;
on `feat/guided-sessions-expansion`, PR pending).** Additive content + one small UI feature (an extension of
spec 16/48, no new machinery, no schema/IPC). **New `family` group "Family & relationships"** (12
family-dynamics chat sessions — family role, a parent, siblings, boundaries, in-laws, inherited patterns,
repairing a rift, aging parents, your parenting, co-parenting, estrangement, gathering prep), and the
**Reflective** + **Coaching** groups grow to a fuller ~12 each (+worry time, thinking traps, three good
things, name the feeling, urge surfing; +habit builder, getting unstuck, time & priorities, strengths,
future self). Plumbing: `GuidedGroupId += 'family'`, `GUIDED_GROUPS`, `GUIDE_LIFE_AREAS.family`. Every entry
leads with the shared `frame()` not-therapy boundary; **family is never adult-gated** (the `adult ===
(group === 'intimacy')` catalog invariant holds — a unit asserts it). With ~56 browsable sessions now, a
**catalog search** filters across every group by name/framework/topic (non-matching groups collapse away, a
calm empty state when nothing matches, and the **18+ intimacy group is never surfaced via search before the
ack** — the no-leak guard). Gate green: typecheck (all), lint, **934 core + 851 desktop** unit (the new
family group + fuller-set assertions; 4 GuidedCatalog RTL incl. the no-leak guard), **E2E +1** (browse the
Family group + filter across groups + empty state + clear). Visual QA (real Electron screenshots): the
search sits cleanly above the catalog; filtering to "boundaries" shows only the matching Family cards —
sleek + intentional. Synced spec 16 §3.1/§3.2. **Lesson: `git checkout <file>` reverts UNCOMMITTED changes
too — injecting a throwaway screenshot into an uncommitted E2E then `git checkout`-ing to revert the
screenshot wipes the whole new test; commit the test first, then inject/revert screenshots over the committed
baseline.**

- 2026-06-26 — **Build (Memory redesign — sharing is context not display + relationship insights + test-sharing
default; SPEC 54 BUILT; on `feat/memory-redesign`, PR pending).** Two user-reported problems: Memory DISPLAYED a
partner's raw shared answers, and it was a wall of text. **The concept fix (the core):** a partner's shared data
Expand Down
31 changes: 31 additions & 0 deletions apps/desktop/e2e/launch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1927,6 +1927,37 @@ test('guided sessions: start a guided exercise → steered reply → complete &
}
});

test('guided sessions: the Family group is browsable and the search filters across groups (expansion)', async () => {
const { userData, vault } = await seedReadyVault({ 'ai.enabled': true });
await createNodeSecretStore(userData, passthrough).set('anthropic.apiKey', 'sk-ant-e2e');
const app = await launch(userData);
try {
const w = await app.firstWindow();
await w.getByRole('link', { name: 'Sessions' }).click();

// The new Family & relationships group + its cards browse alongside the existing groups.
await expect(w.getByText('Family & relationships')).toBeVisible();
await expect(w.getByText('Your Family Role')).toBeVisible();
await expect(w.getByText('Sibling Dynamics')).toBeVisible();

// Search filters across every group; non-matching groups disappear.
await w.getByLabel('Search guided sessions').fill('boundaries');
await expect(w.getByText('Boundaries with Family')).toBeVisible();
await expect(w.getByText('Your Family Role')).toHaveCount(0); // filtered out
await expect(w.getByText('Building a Habit')).toHaveCount(0); // a coaching entry, filtered out

// A no-match query shows the calm empty state; clearing it restores the catalog.
await w.getByLabel('Search guided sessions').fill('zzznotathing');
await expect(w.getByText(/No sessions match/)).toBeVisible();
await w.getByLabel('Search guided sessions').fill('');
await expect(w.getByText('Your Family Role')).toBeVisible();
} finally {
await app.close();
await rm(userData, { recursive: true, force: true });
await rm(vault, { recursive: true, force: true });
}
});

test('challenges (52): co-create a challenge → propose-then-agree captures a tracked Challenge → check-in feeds memory; decline path; 360px clean', async () => {
const { userData, vault } = await seedReadyVault({ 'ai.enabled': true });
await createNodeSecretStore(userData, passthrough).set('anthropic.apiKey', 'sk-ant-e2e');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { GuidedCatalog } from './GuidedCatalog';

function setup(adultAcknowledged = false): void {
render(
<GuidedCatalog
onPick={vi.fn()}
adultAcknowledged={adultAcknowledged}
onAcknowledgeAdult={vi.fn()}
/>,
);
}

describe('GuidedCatalog — expanded groups + search', () => {
it('shows the Family group and the fuller therapy/coaching sets', () => {
setup();
expect(screen.getByText('Family & relationships')).toBeInTheDocument();
expect(screen.getByText('Your Family Role')).toBeInTheDocument();
expect(screen.getByText('Urge Surfing')).toBeInTheDocument(); // a new reflective entry
expect(screen.getByText('Building a Habit')).toBeInTheDocument(); // a new coaching entry
});

it('filters across groups by name/framework/topic; non-matching groups disappear', async () => {
setup();
await userEvent.type(screen.getByLabelText('Search guided sessions'), 'sibling');
expect(screen.getByText('Sibling Dynamics')).toBeInTheDocument();
// A non-matching group's cards are gone.
expect(screen.queryByText('Building a Habit')).not.toBeInTheDocument();
expect(screen.queryByText('Your Family Role')).not.toBeInTheDocument();
});

it('NEVER reveals gated intimacy content via search before the 18+ ack', async () => {
setup(false);
await userEvent.type(screen.getByLabelText('Search guided sessions'), 'sensate');
// The intimacy match's CARD is withheld — no leak (the empty state echoes the query, so assert the
// actual card title, not the raw word).
expect(screen.queryByText('Sensate Focus')).not.toBeInTheDocument();
expect(screen.getByText(/No sessions match/)).toBeInTheDocument();
});

it('shows a calm empty state when nothing matches', async () => {
setup();
await userEvent.type(screen.getByLabelText('Search guided sessions'), 'zzznotathing');
expect(screen.getByText(/No sessions match/)).toBeInTheDocument();
});
});
43 changes: 32 additions & 11 deletions apps/desktop/src/renderer/src/app/routes/sessions/GuidedCatalog.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useState } from 'react';
import { GUIDED_GROUPS, listExercises } from '@selfos/core/conversations';
import { Button, Card, Stack, Text } from '../../../design-system/components';
import { Button, Card, Stack, Text, TextInput } from '../../../design-system/components';
import { GuidedExerciseCard } from './GuidedExerciseCard';
import styles from './Launcher.module.css';

/**
* The grouped, built-in catalog (16 §3.2). Non-clinical group titles; the framework lives in each card's
* tag. Groups are collapsible (native <details>). The Intimacy & connection group is gated behind a
* one-time 18+ acknowledgement (§8.3).
* tag. Groups are collapsible (native <details>). A search filters across every group by name, framework, or
* topic — while searching, only groups with matches open. The Intimacy & connection group is gated behind a
* one-time 18+ acknowledgement (§8.3), and search never reveals it before the ack.
*/
export function GuidedCatalog({
onPick,
Expand All @@ -18,17 +20,36 @@ export function GuidedCatalog({
onAcknowledgeAdult: () => void;
}): JSX.Element {
const all = listExercises();
const [query, setQuery] = useState('');
const q = query.trim().toLowerCase();
const matches = (e: { title: string; framework: string; blurb: string }): boolean =>
!q || `${e.title} ${e.framework} ${e.blurb}`.toLowerCase().includes(q);

const groups = GUIDED_GROUPS.map((group) => {
const isIntimacy = group.id === 'intimacy';
const gated = isIntimacy && !adultAcknowledged;
const items = all.filter((e) => e.group === group.id && matches(e));
return { group, isIntimacy, gated, items };
// While searching, show a group only if it has matches — and never surface a gated intimacy match.
}).filter(({ gated, items }) => (q ? items.length > 0 && !gated : true));

return (
<Stack gap={3}>
{GUIDED_GROUPS.map((group) => {
const items = all.filter((e) => e.group === group.id);
const isIntimacy = group.id === 'intimacy';
const gated = isIntimacy && !adultAcknowledged;
return (
<TextInput
type="search"
value={query}
aria-label="Search guided sessions"
placeholder="Search sessions by name, framework, or topic…"
onChange={(event) => setQuery(event.target.value)}
/>
{groups.length === 0 ? (
<Text tone="secondary">No sessions match “{query}”. Try a different word.</Text>
) : (
groups.map(({ group, isIntimacy, gated, items }) => (
// A native <details> group; the title is a styled span (not a heading) to avoid nesting a
// heading inside the summary's disclosure button (16 §9). The <section> labels the region.
<section key={group.id} aria-label={group.title}>
<details className={styles.group} open={!isIntimacy}>
<details className={styles.group} open={q ? true : !isIntimacy}>
<summary className={styles.groupSummary}>
<span className={styles.groupTitle}>{group.title}</span>
{isIntimacy ? <span className={styles.adultTag}>18+</span> : null}
Expand Down Expand Up @@ -60,8 +81,8 @@ export function GuidedCatalog({
)}
</details>
</section>
);
})}
))
)}
</Stack>
);
}
27 changes: 19 additions & 8 deletions docs/specs/16-guided-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ launcher (the conversation **list** stays in the left pane as today):
- **Suggested for you** (§3.4): a row/cards of 2–4 AI-recommended exercises (or a calm "turn on AI" / "add
more about yourself" state). Each card → starts that guided session.
- **The grouped catalog** (§3.2): collapsible groups — **Reflective & therapy-informed**, **Coaching**,
**Intimacy & connection** — each a grid of exercise cards (title + framework tag + one-line blurb). The
Intimacy & connection group is gated (§8.3). (Group titles are deliberately non-clinical; the recognisable
framework lives in each card's tag — §8.1.)
**Family & relationships**, **Intimacy & connection** — each a grid of exercise cards (title + framework
tag + one-line blurb), above a **search** that filters every group by name, framework, or topic (a
non-matching group collapses away; the gated Intimacy group is never surfaced via search before the ack).
The Intimacy & connection group is gated (§8.3). (Group titles are deliberately non-clinical; the
recognisable framework lives in each card's tag — §8.1.)

Picking an exercise (or free-start) opens a normal session thread; the launcher returns whenever there's no
active session.
Expand All @@ -92,12 +94,21 @@ Each exercise is a built-in definition (§4.1) with a title, group, framework ta
applies. **Group titles are non-clinical; the framework is a per-card tag** (§8.1). The **initial catalog**
(groups shown with their internal id → display title; **structured** exercises marked ⚙):

- **`therapy` → "Reflective & therapy-informed"** — Reflective Session (Integrative) · Thought Record ⚙ (CBT) ·
Worry Decatastrophizing (CBT) · Behavioral Activation Plan (Behavioral Activation) · Values Clarification
(ACT) · Self-Compassion Break (Self-Compassion) · Grief & Loss Check-in (Grief work).
- **`coaching` → "Coaching"** — Life Coaching Session (Integrative) · GROW Goal-Setting ⚙ (GROW) · Weekly
- **`therapy` → "Reflective & therapy-informed"** (~12) — Reflective Session (Integrative) · Thought Record ⚙
(CBT) · Worry Decatastrophizing (CBT) · Behavioral Activation Plan (Behavioral Activation) · Values
Clarification (ACT) · Self-Compassion Break (Self-Compassion) · Grief & Loss Check-in (Grief work) · Worry
Time (CBT) · Spotting Thinking Traps (CBT) · Three Good Things (Positive psychology) · Name the Feeling
(Affect labeling) · Urge Surfing (Mindfulness).
- **`coaching` → "Coaching"** (~12) — Life Coaching Session (Integrative) · GROW Goal-Setting ⚙ (GROW) · Weekly
Review & Reset ⚙ (Reflective practice) · Decision Clarifier ⚙ (Values-based) · Hard Conversation Prep
(DEAR MAN) · Boundary Setting (Assertiveness) · Burnout & Energy Audit.
(DEAR MAN) · Boundary Setting (Assertiveness) · Burnout & Energy Audit · Building a Habit (Tiny Habits) ·
Getting Unstuck (Behavioral activation) · Time & Priorities (Prioritization) · Playing to Your Strengths
(Strengths-based) · Meet Your Future Self (Visioning).
- **`family` → "Family & relationships"** (12, never adult-gated) — Your Family Role (Family systems) ·
Reflecting on a Parent (Attachment-informed) · Sibling Dynamics · Boundaries with Family (Assertiveness) ·
In-Laws & Extended Family · Patterns You Inherited (Intergenerational) · Repairing a Family Rift (Repair) ·
Caring for an Aging Parent (Caregiver support) · Reflecting on Your Parenting (Reflective parenting) ·
Co-Parenting · Distance or Estrangement · Preparing for a Family Gathering.
- **`intimacy` → "Intimacy & connection"** (18+, §8.3) — Sensate Focus (Masters & Johnson) · Desire
Discrepancy · Talking About Sex. (Expanded to 20 entries — relational through explicit, plus the structured
Yes/No/Maybe builder — by [`48`](48-intimacy-guided-sessions.md).)
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/conversations/guidedCatalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
GUIDED_CATALOG,
GUIDED_GROUPS,
getExercise,
guideLifeAreas,
guidedGroupTitle,
listExercises,
} from './guidedCatalog';
Expand Down Expand Up @@ -116,9 +117,26 @@ describe('guidedCatalog integrity', () => {
it('guidedGroupTitle maps ids to the non-clinical display titles', () => {
expect(guidedGroupTitle('therapy')).toBe('Reflective & therapy-informed');
expect(guidedGroupTitle('coaching')).toBe('Coaching');
expect(guidedGroupTitle('family')).toBe('Family & relationships');
expect(guidedGroupTitle('intimacy')).toBe('Intimacy & connection');
});

it('offers a Family & relationships group — family-dynamics chats, never adult-gated (expansion)', () => {
expect(GUIDED_GROUPS.map((g) => g.id)).toContain('family');
const family = listExercises().filter((e) => e.group === 'family');
expect(family.length).toBeGreaterThanOrEqual(10);
// Family sessions are NOT in the 18+ intimacy group → never adult-gated (the catalog invariant holds).
expect(family.every((e) => !e.adult && e.kind === 'chat')).toBe(true);
// They foreground the Family life-area for portrait-fact selection.
expect(guideLifeAreas('family')).toContain('Family');
});

it('the reflective and coaching groups each offer a fuller set (~12) (expansion)', () => {
const all = listExercises();
expect(all.filter((e) => e.group === 'therapy').length).toBeGreaterThanOrEqual(12);
expect(all.filter((e) => e.group === 'coaching').length).toBeGreaterThanOrEqual(12);
});

it('listExercises returns the full catalog', () => {
expect(listExercises().length).toBe(GUIDED_CATALOG.length);
});
Expand Down
Loading