From b0390f462c33be1ebbc2c6ff514f2dd92e096a7a Mon Sep 17 00:00:00 2001 From: Ben Marshall Date: Fri, 26 Jun 2026 14:39:51 -0500 Subject: [PATCH 1/3] =?UTF-8?q?feat(sessions):=20expand=20guided=20catalog?= =?UTF-8?q?=20=E2=80=94=20fuller=20therapy/coaching=20+=20a=20Family=20gro?= =?UTF-8?q?up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new "Family & relationships" group (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 brings the Reflective and Coaching groups 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). Every entry leads with the shared not-therapy frame() and is a free chat session (no new machinery). Family is never adult-gated (the intimacy-only `adult` invariant holds). 14 catalog tests (the new group + the fuller sets + the unchanged invariants); 934 core. Co-Authored-By: Claude Opus 4.8 --- .../src/conversations/guidedCatalog.test.ts | 18 + .../core/src/conversations/guidedCatalog.ts | 330 +++++++++++++++++- 2 files changed, 347 insertions(+), 1 deletion(-) diff --git a/packages/core/src/conversations/guidedCatalog.test.ts b/packages/core/src/conversations/guidedCatalog.test.ts index e1f261a..7c3e24f 100644 --- a/packages/core/src/conversations/guidedCatalog.test.ts +++ b/packages/core/src/conversations/guidedCatalog.test.ts @@ -3,6 +3,7 @@ import { GUIDED_CATALOG, GUIDED_GROUPS, getExercise, + guideLifeAreas, guidedGroupTitle, listExercises, } from './guidedCatalog'; @@ -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); }); diff --git a/packages/core/src/conversations/guidedCatalog.ts b/packages/core/src/conversations/guidedCatalog.ts index 4d00456..a44b834 100644 --- a/packages/core/src/conversations/guidedCatalog.ts +++ b/packages/core/src/conversations/guidedCatalog.ts @@ -14,7 +14,7 @@ // challenge", not browsed). Its guides live in `challengeCoach.ts` and are absent from `GUIDED_CATALOG`. import { CHALLENGE_GUIDES } from './challengeCoach'; -export type GuidedGroupId = 'therapy' | 'coaching' | 'intimacy' | 'challenge'; +export type GuidedGroupId = 'therapy' | 'coaching' | 'family' | 'intimacy' | 'challenge'; export interface GuidedExercise { /** Stable id, e.g. 'cbt-thought-record'. */ @@ -41,6 +41,7 @@ export interface GuidedExercise { export const GUIDED_GROUPS: ReadonlyArray<{ id: GuidedGroupId; title: string }> = [ { id: 'therapy', title: 'Reflective & therapy-informed' }, { id: 'coaching', title: 'Coaching' }, + { id: 'family', title: 'Family & relationships' }, { id: 'intimacy', title: 'Intimacy & connection' }, ]; @@ -56,6 +57,7 @@ export function guidedGroupTitle(group: GuidedGroupId): string { const GUIDE_LIFE_AREAS: Record = { therapy: ['Emotions & patterns', 'Family', 'Relationships'], coaching: ['Goals & growth', 'Work & purpose', 'Money'], + family: ['Family', 'Relationships', 'Emotions & patterns'], intimacy: ['Intimacy', 'Relationships'], // A challenge spans any domain — foreground the most challengeable areas; the always-on CORE adds the rest. challenge: [ @@ -183,6 +185,81 @@ not a personal failing, (3) self-kindness — offering themselves the warmth the grief. Normalize that grief is non-linear and has no timeline. Be especially attentive to distress — if it \ points toward crisis, follow the safety guidance and encourage professional support.`, }, + { + id: 'worry-time', + group: 'therapy', + title: 'Worry Time', + framework: 'CBT', + blurb: 'Contain spiralling worry by giving it a time and a place.', + kind: 'chat', + openingMessage: + "Let's try a self-help exercise inspired by CBT worry postponement — not therapy. The idea is to give " + + "your worries one contained slot instead of all day. What's been looping in your mind lately?", + systemPromptAddendum: `${frame('the CBT technique of worry postponement ("worry time")')} Help them name \ +the worry, decide whether it's a solvable problem or an unsolvable hypothetical, and either plan one small \ +next step (solvable) or practise setting it down until a chosen worry slot (unsolvable). Keep it light and \ +practical, not a deep-dive into every fear.`, + }, + { + id: 'cognitive-distortions', + group: 'therapy', + title: 'Spotting Thinking Traps', + framework: 'CBT', + blurb: 'Catch the common distortions bending a stuck thought.', + kind: 'chat', + openingMessage: + "Let's look at a thought that's been weighing on you — a self-help exercise inspired by CBT, not " + + 'therapy. Our minds fall into predictable "thinking traps"; naming one loosens its grip. What thought ' + + 'has been hard to shake?', + systemPromptAddendum: `${frame('CBT cognitive-distortion work')} Gently help them notice which common \ +thinking traps may be at play (all-or-nothing, catastrophizing, mind-reading, overgeneralizing, \ +should-statements, emotional reasoning), name it without judgment, and try a more balanced alternative \ +thought in their own words. Ask, don't lecture.`, + }, + { + id: 'three-good-things', + group: 'therapy', + title: 'Three Good Things', + framework: 'Positive psychology', + blurb: 'Notice what went well today, and why it mattered.', + kind: 'chat', + openingMessage: + "Let's end on what went right — a self-help exercise from positive psychology, not therapy. Even on a " + + 'hard day, small good things are usually there. What are one to three things that went well today?', + systemPromptAddendum: `${frame('the positive-psychology "Three Good Things" practice')} Invite up to three \ +things that went well, and for each gently explore why it happened and what part they played in it — \ +savoring, not toxic positivity. If the day felt bleak, validate that first and look for the small.`, + }, + { + id: 'name-the-feeling', + group: 'therapy', + title: 'Name the Feeling', + framework: 'Affect labeling', + blurb: 'Put precise words to what you’re feeling to ease its intensity.', + kind: 'chat', + openingMessage: + "Let's put words to what you're feeling — a self-help exercise in affect labeling, not therapy. Naming " + + 'an emotion precisely tends to turn its volume down. How are you feeling right now, even roughly?', + systemPromptAddendum: `${frame('affect labeling ("name it to tame it")')} Help them move from a vague \ +"bad/stressed" toward a more specific emotion word, notice where it sits in the body, and acknowledge it \ +without needing to fix it. Offer a small vocabulary of feelings if they're stuck. Stay with the feeling, \ +gently.`, + }, + { + id: 'urge-surfing', + group: 'therapy', + title: 'Urge Surfing', + framework: 'Mindfulness', + blurb: 'Ride out a craving or urge without acting on it.', + kind: 'chat', + openingMessage: + "Let's ride out an urge together — a self-help mindfulness exercise, not therapy or addiction " + + 'treatment. Urges rise, crest, and fall like waves if we let them. What urge would you like to surf?', + systemPromptAddendum: `${frame('the mindfulness practice of urge surfing')} Guide them to observe the \ +urge with curiosity — where they feel it, how intense it is (0–10), how it shifts breath by breath — rather \ +than fighting or feeding it, noticing that it peaks and passes. If the urge involves self-harm or a \ +substance crisis, follow the safety guidance and point to professional support.`, + }, // ── Coaching ───────────────────────────────────────────────────────────────────────────────────── { @@ -289,6 +366,257 @@ Normalize that boundaries are an act of care, not aggression.`, restorers across work, relationships, body, and mind, then find one realistic shift toward balance. This is \ wellness reflection — if they describe symptoms that need medical attention, encourage professional care.`, }, + { + id: 'habit-builder', + group: 'coaching', + title: 'Building a Habit', + framework: 'Tiny Habits', + blurb: 'Design a small habit that actually sticks.', + kind: 'chat', + openingMessage: + "Let's build a habit that lasts — a self-help exercise inspired by Tiny Habits and habit science, not " + + 'therapy. Big resolutions tend to fade; tiny anchored ones stick. What habit would you like to grow?', + systemPromptAddendum: `${frame("BJ Fogg's Tiny Habits and habit-formation science")} Help them shrink the \ +habit to something almost too small to fail, anchor it to an existing routine ("after I ___, I will ___"), \ +and plan a tiny celebration. Troubleshoot friction and focus on consistency over intensity.`, + }, + { + id: 'procrastination-unblock', + group: 'coaching', + title: 'Getting Unstuck', + framework: 'Behavioral activation', + blurb: 'Find the real block behind a task you keep avoiding.', + kind: 'chat', + openingMessage: + "Let's get you unstuck on something you've been putting off — a practical self-help exercise, not " + + "therapy. Procrastination is usually protecting us from something. What's the task you keep avoiding?", + systemPromptAddendum: `${frame('procrastination coaching and behavioral activation')} Help them surface \ +what's really in the way (fear, ambiguity, perfectionism, overwhelm, low energy), break the task into a \ +two-minute first step, and lower the bar to "good enough to start." Be encouraging, never shaming.`, + }, + { + id: 'time-and-priorities', + group: 'coaching', + title: 'Time & Priorities', + framework: 'Prioritization', + blurb: 'Sort the urgent from the important and reclaim your week.', + kind: 'chat', + openingMessage: + "Let's sort out where your time is going — a self-help prioritization exercise, not therapy. Often the " + + "urgent crowds out the important. What's filling your days, and what keeps getting pushed aside?", + systemPromptAddendum: `${frame('prioritization frameworks like the Eisenhower matrix')} Help them sort \ +commitments by urgent vs. important, notice where their time and their values have drifted apart, and choose \ +one thing to protect, delegate, or drop. Keep it concrete and kind.`, + }, + { + id: 'strengths-spotlight', + group: 'coaching', + title: 'Playing to Your Strengths', + framework: 'Strengths-based', + blurb: 'Name your strengths and use them more deliberately.', + kind: 'chat', + openingMessage: + "Let's spotlight what you're already good at — a strengths-based self-help exercise, not therapy. We " + + 'grow fastest by leaning into strengths, not just fixing weaknesses. When do you feel most like yourself?', + systemPromptAddendum: `${frame('strengths-based coaching')} Help them name a few signature strengths \ +(drawing on moments of energy, flow, and pride), then find one current challenge they could approach by \ +using a strength more deliberately. Affirm without flattering.`, + }, + { + id: 'future-self', + group: 'coaching', + title: 'Meet Your Future Self', + framework: 'Visioning', + blurb: 'Picture the you a year on, and what they’d ask of you now.', + kind: 'chat', + openingMessage: + "Let's imagine your future self — a self-help visioning exercise, not therapy. Picture yourself a year " + + 'from now, living a little more like you want to. What does that version of you look like?', + systemPromptAddendum: `${frame('future-self visioning and values-based goal-setting')} Help them vividly \ +picture themselves a year ahead — how they spend their time, feel, and relate — then work backward to one \ +small thing their present self could start. Keep it hopeful and grounded, not a fantasy.`, + }, + + // ── Family & relationships (54-memory-redesign follow-up: family-dynamics guided sessions) ─────────── + { + id: 'family-role', + group: 'family', + title: 'Your Family Role', + framework: 'Family systems', + blurb: 'Notice the role you play in your family, and whether it still fits.', + kind: 'chat', + openingMessage: + "Let's look at the role you tend to play in your family — a self-help exercise inspired by family-" + + 'systems thinking, not therapy. Many of us slip into a familiar part (the fixer, the peacekeeper, the ' + + 'responsible one). Which feels most like yours?', + systemPromptAddendum: `${frame('family-systems thinking')} Help them notice the role they tend to occupy \ +in their family, where it came from, what it costs and protects, and whether they'd like to hold it more \ +lightly. Stay curious and non-blaming about the family as a whole.`, + }, + { + id: 'reflecting-on-a-parent', + group: 'family', + title: 'Reflecting on a Parent', + framework: 'Attachment-informed', + blurb: 'Make sense of your relationship with a parent, past or present.', + kind: 'chat', + openingMessage: + "Let's reflect on your relationship with a parent — a gentle self-help exercise, not therapy. These " + + 'bonds shape a lot in us, for better and worse. Which parent would you like to think about, and how are ' + + 'things between you?', + systemPromptAddendum: `${frame('attachment-informed reflection on family relationships')} Help them explore \ +the relationship with honesty and compassion — what they received, what they missed, what they carry — \ +without pushing toward either idealizing or condemning. Hold complexity; if grief or trauma surfaces, slow \ +down, validate, and point to professional support.`, + }, + { + id: 'sibling-dynamics', + group: 'family', + title: 'Sibling Dynamics', + framework: 'Family systems', + blurb: 'Untangle an old or current dynamic with a sibling.', + kind: 'chat', + openingMessage: + "Let's look at a sibling relationship — a self-help exercise, not therapy. Sibling bonds carry a lot of " + + 'history: rivalry, loyalty, comparison, love. Which sibling is on your mind, and what’s the dynamic like?', + systemPromptAddendum: `${frame('family-systems thinking about sibling relationships')} Help them explore \ +the patterns between them — roles assigned in childhood, comparison, fairness, closeness or distance now — \ +and what they'd like the relationship to be. Avoid taking sides; stay curious about both perspectives.`, + }, + { + id: 'boundaries-with-family', + group: 'family', + title: 'Boundaries with Family', + framework: 'Assertiveness', + blurb: 'Set a caring boundary with a family member who oversteps.', + kind: 'chat', + openingMessage: + "Let's work on a boundary with someone in your family — a self-help exercise in assertiveness, not " + + 'therapy. Family boundaries can feel especially loaded. Where do you feel overstepped, guilted, or ' + + 'stretched thin?', + systemPromptAddendum: `${frame('assertiveness and boundary-setting within families')} Help them locate \ +where a boundary is needed, separate the relationship from the behavior, and craft a clear, kind, \ +non-apologetic way to express it — while anticipating guilt-trips or pushback. Normalize that boundaries \ +can coexist with love.`, + }, + { + id: 'inlaws-extended', + group: 'family', + title: 'In-Laws & Extended Family', + framework: 'Boundaries', + blurb: 'Navigate in-laws or extended family with less friction.', + kind: 'chat', + openingMessage: + "Let's navigate the extended family — in-laws, relatives, the wider web — a self-help exercise, not " + + 'therapy. These relationships come with their own loyalties and expectations. What’s feeling tricky?', + systemPromptAddendum: `${frame('boundary and expectation work with extended family and in-laws')} Help \ +them clarify their own and (if relevant) their partner's needs, find a united approach where a partner is \ +involved, and choose how much to engage. Be even-handed about competing family cultures and loyalties.`, + }, + { + id: 'generational-patterns', + group: 'family', + title: 'Patterns You Inherited', + framework: 'Intergenerational', + blurb: 'Spot a pattern passed down your family, and choose what to keep.', + kind: 'chat', + openingMessage: + "Let's look at a pattern that runs in your family — a self-help exercise, not therapy. We inherit ways " + + 'of handling money, conflict, love, and feelings. Which pattern do you notice repeating across ' + + 'generations?', + systemPromptAddendum: `${frame('intergenerational pattern awareness')} Help them name a pattern handed \ +down (around conflict, emotion, money, parenting, secrecy), understand it with compassion for those who \ +passed it on, and decide consciously what to keep and what to change with them. Avoid clinical framing.`, + }, + { + id: 'family-conflict-repair', + group: 'family', + title: 'Repairing a Family Rift', + framework: 'Repair', + blurb: 'Prepare to reconnect after a falling-out with family.', + kind: 'chat', + openingMessage: + "Let's think about repairing a rift with family — a self-help exercise, not therapy or mediation. " + + 'Reaching back across a break takes courage. Who is the rift with, and what happened?', + systemPromptAddendum: `${frame('relationship-repair principles')} Help them weigh whether and how to \ +re-approach — what they'd want to say, take responsibility for, and ask for — while respecting that repair \ +is not always safe or wanted. Never pressure reconciliation; honor their pace and safety, and validate \ +ambivalence.`, + }, + { + id: 'aging-parents', + group: 'family', + title: 'Caring for an Aging Parent', + framework: 'Caregiver support', + blurb: 'Tend to the strain and feelings of caring for an aging parent.', + kind: 'chat', + openingMessage: + "Let's make space for what it's like to care for an aging parent — a self-help exercise, not therapy or " + + 'medical advice. It can hold love, exhaustion, grief, and guilt all at once. How are you holding up?', + systemPromptAddendum: `${frame('caregiver support and reflection')} Help them name the emotional load \ +(role reversal, grief, guilt, resentment, logistics), tend to their own needs and limits, and consider \ +support or sharing the load. This is emotional support — direct medical, legal, or care decisions to the \ +right professionals.`, + }, + { + id: 'your-parenting', + group: 'family', + title: 'Reflecting on Your Parenting', + framework: 'Reflective parenting', + blurb: 'Reflect on the parent you are, without the guilt spiral.', + kind: 'chat', + openingMessage: + "Let's reflect on your own parenting — a warm self-help exercise, not therapy or parenting instruction. " + + 'No parent gets it all right, and reflection is itself a sign of care. What’s on your mind as a parent?', + systemPromptAddendum: `${frame('reflective-parenting practice')} Help them reflect on a moment or pattern \ +with their child with curiosity rather than guilt — what they value, where they want to repair or adjust, \ +and what they're already doing well. Normalize rupture-and-repair; counter all-or-nothing self-judgment.`, + }, + { + id: 'coparenting', + group: 'family', + title: 'Co-Parenting', + framework: 'Co-parenting', + blurb: 'Work through a co-parenting challenge after separation.', + kind: 'chat', + openingMessage: + "Let's work through a co-parenting situation — a self-help exercise, not therapy or legal advice. " + + 'Raising kids across two homes is genuinely hard. What’s coming up with your co-parent right now?', + systemPromptAddendum: `${frame('co-parenting communication strategies')} Help them keep the focus on the \ +child's wellbeing, separate the parenting relationship from the past romantic one, and find businesslike, \ +low-conflict ways to communicate. Stay neutral about the co-parent; never give legal or custody advice.`, + }, + { + id: 'estrangement', + group: 'family', + title: 'Distance or Estrangement', + framework: 'Estrangement support', + blurb: 'Sit with the complicated feelings of family distance or no-contact.', + kind: 'chat', + openingMessage: + "Let's make room for the feelings around family distance or estrangement — a gentle self-help exercise, " + + 'not therapy. Whether the distance is your choice or not, it can be heavy and lonely. Would you like to ' + + 'tell me about it?', + systemPromptAddendum: `${frame('support around family estrangement and distance')} Hold space without \ +pushing toward reconciliation OR cut-off — both can be valid. Validate grief, relief, guilt, and \ +ambivalence equally, and respect the boundaries they've drawn for their own safety. If distress points \ +toward crisis, follow the safety guidance and encourage professional support.`, + }, + { + id: 'family-gathering-prep', + group: 'family', + title: 'Preparing for a Family Gathering', + framework: 'Coping planning', + blurb: 'Go into a holiday or gathering with a plan to protect your peace.', + kind: 'chat', + openingMessage: + "Let's prepare for a family gathering — a practical self-help exercise, not therapy. Holidays and " + + 'reunions can stir up old roles and tensions fast. What gathering is coming up, and what do you brace ' + + 'for?', + systemPromptAddendum: `${frame('coping and boundary planning for family gatherings')} Help them anticipate \ +the likely flashpoints, decide ahead which topics and dynamics they'll engage or sidestep, plan exits and \ +recovery breaks, and choose one intention for how they want to show up. Practical and reassuring.`, + }, // ── Intimacy & connection (18+, §8.3) ────────────────────────────────────────────────────────────── { From 8f5234e871587101380a3421a87e71267daeb499 Mon Sep 17 00:00:00 2001 From: Ben Marshall Date: Fri, 26 Jun 2026 14:46:04 -0500 Subject: [PATCH 2/3] feat(sessions): add a search to the guided catalog (expansion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalog now spans ~56 browsable sessions, so a search filters across every group by name, framework, or topic — non-matching groups collapse away, a calm empty state shows when nothing matches, and the 18+ intimacy group is never revealed via search before the ack. The new Family group renders alongside the existing ones. 4 RTL (filter, no-leak, empty state) + an E2E (browse Family + filter + empty + clear). Co-Authored-By: Claude Opus 4.8 --- apps/desktop/e2e/launch.spec.ts | 31 ++++++++++++ .../routes/sessions/GuidedCatalog.test.tsx | 48 +++++++++++++++++++ .../src/app/routes/sessions/GuidedCatalog.tsx | 43 ++++++++++++----- 3 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 apps/desktop/src/renderer/src/app/routes/sessions/GuidedCatalog.test.tsx diff --git a/apps/desktop/e2e/launch.spec.ts b/apps/desktop/e2e/launch.spec.ts index d63e3a3..e276a84 100644 --- a/apps/desktop/e2e/launch.spec.ts +++ b/apps/desktop/e2e/launch.spec.ts @@ -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'); diff --git a/apps/desktop/src/renderer/src/app/routes/sessions/GuidedCatalog.test.tsx b/apps/desktop/src/renderer/src/app/routes/sessions/GuidedCatalog.test.tsx new file mode 100644 index 0000000..44c193c --- /dev/null +++ b/apps/desktop/src/renderer/src/app/routes/sessions/GuidedCatalog.test.tsx @@ -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( + , + ); +} + +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(); + }); +}); diff --git a/apps/desktop/src/renderer/src/app/routes/sessions/GuidedCatalog.tsx b/apps/desktop/src/renderer/src/app/routes/sessions/GuidedCatalog.tsx index f94d6b3..f3a0fdb 100644 --- a/apps/desktop/src/renderer/src/app/routes/sessions/GuidedCatalog.tsx +++ b/apps/desktop/src/renderer/src/app/routes/sessions/GuidedCatalog.tsx @@ -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
). The Intimacy & connection group is gated behind a - * one-time 18+ acknowledgement (§8.3). + * tag. Groups are collapsible (native
). 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, @@ -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 ( - {GUIDED_GROUPS.map((group) => { - const items = all.filter((e) => e.group === group.id); - const isIntimacy = group.id === 'intimacy'; - const gated = isIntimacy && !adultAcknowledged; - return ( + setQuery(event.target.value)} + /> + {groups.length === 0 ? ( + No sessions match “{query}”. Try a different word. + ) : ( + groups.map(({ group, isIntimacy, gated, items }) => ( // A native
group; the title is a styled span (not a heading) to avoid nesting a // heading inside the summary's disclosure button (16 §9). The
labels the region.
-
+
{group.title} {isIntimacy ? 18+ : null} @@ -60,8 +81,8 @@ export function GuidedCatalog({ )}
- ); - })} + )) + )} ); } From 7b37160b7334f7d2eae37c3d650eb33a038e812d Mon Sep 17 00:00:00 2001 From: Ben Marshall Date: Fri, 26 Jun 2026 14:49:21 -0500 Subject: [PATCH 3/3] docs(sessions): sync spec 16 + changelog for the guided expansion Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 21 +++++++++++++++++++++ docs/specs/16-guided-sessions.md | 27 +++++++++++++++++++-------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cc40c80..bbb74fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ` 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 diff --git a/docs/specs/16-guided-sessions.md b/docs/specs/16-guided-sessions.md index 1c37e71..c8587dc 100644 --- a/docs/specs/16-guided-sessions.md +++ b/docs/specs/16-guided-sessions.md @@ -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. @@ -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).)