feat(rituals): Phase 5 — morning triage, evening wind-down, autostart
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled

Three opt-in rituals, all default OFF. Research-anchored (Barkley's
point-of-performance, Sweller cognitive-load theory, Newport shutdown
ritual, Gollwitzer implementation intentions, Thaler/Sunstein nudge
with informed consent for the ADHD audience).

Morning triage: modal gated on ritualsMorning toggle, configurable
trigger time (default 08:00 to respect ADHD sleep inertia rather than
the spec's 06:00), "pick up to three for today" with a gentle swap
message on the fourth attempt. Skip sets last-shown-today so it never
re-prompts the same calendar day. last-shown persists via kon_storage.

Evening wind-down: dedicated page, user-triggered only (tray menu +
Settings button). Mechanical closure + physical reset + intentional
cue — the whole Newport template. Open loops are read-only reflection;
Tasks page owns transactions. Copy is additive throughout: "you
finished X today", never "you didn't finish Y".

Autostart: tauri-plugin-autostart registered (LaunchAgent on macOS,
.desktop on Linux, registry Run on Windows). No bespoke Rust commands
— frontend calls the plugin's invoke-handlers directly. Toggle in
Settings is one-way (click → OS call → state update) to avoid the UI
lying during the round-trip. First-run presents a forced-choice prompt
for all three options, with "skip all" escape hatches per step.

Copy audit against RSD literature: no "overdue", "failed", or
day-to-day comparison framing anywhere in ritual surfaces.

Post-v0.1 ideas captured in the roadmap: calendar integration
(read-only ICS as interim, cloud sync parked) and right-click-to-task
(in-app simple, system-wide a separate phase).
This commit is contained in:
2026-04-24 17:48:01 +01:00
parent 9f53702c7e
commit 3cf3e41899
21 changed files with 967 additions and 8 deletions

View File

@@ -674,6 +674,44 @@
}
}
// Phase 5 rituals. Autostart state mirrors the OS-level entry managed
// by tauri-plugin-autostart; reading via invoke keeps the toggle
// honest even if the user has edited their .desktop file manually.
let autostartSyncing = $state(false);
async function setLaunchAtLogin(nextOn: boolean) {
autostartSyncing = true;
try {
const plugin = await import("@tauri-apps/plugin-autostart");
if (nextOn) {
await plugin.enable();
} else {
await plugin.disable();
}
settings.launchAtLogin = nextOn;
} catch (err) {
toasts.warn("Could not update autostart", String(err));
// Re-read to correct the UI if we failed halfway.
try {
const plugin = await import("@tauri-apps/plugin-autostart");
settings.launchAtLogin = await plugin.isEnabled();
} catch { /* best-effort */ }
} finally {
autostartSyncing = false;
}
}
async function syncAutostartFromOs() {
try {
const plugin = await import("@tauri-apps/plugin-autostart");
settings.launchAtLogin = await plugin.isEnabled();
} catch { /* best-effort on browser / first load */ }
}
function openWindDown() {
page.current = "shutdown";
}
onMount(async () => {
try {
await refreshRuntimeCapabilities();
@@ -692,6 +730,10 @@
// the user opens the Audio section.
refreshAudioDevices();
// Phase 5: read the live OS-level autostart state so the toggle
// reflects reality rather than last-saved intent.
syncAutostartFromOs();
// Vocabulary is loaded reactively via the $effect that tracks the
// active profile id (set once profilesStore.load() resolves in the
// root layout). No eager fetch needed here.
@@ -1396,6 +1438,100 @@
{/if}
</div>
<!-- Rituals (Phase 5 roadmap) -->
<div class="border-b border-border-subtle">
<button
class="flex items-center justify-between w-full py-4 px-5 text-left"
onclick={() => openSection = openSection === 'rituals' ? null : 'rituals'}
>
<h3 class="font-display text-[18px] italic text-text">Rituals</h3>
<span class="text-text-tertiary text-[16px] leading-none">{openSection === 'rituals' ? '' : '+'}</span>
</button>
{#if openSection === 'rituals'}
<div class="px-5 pb-5 animate-fade-in">
<p class="text-[11px] text-text-tertiary mb-4">
All off by default. Rituals only appear when you ask for them.
</p>
<Toggle
bind:checked={settings.ritualsMorning}
label="Morning triage"
description="On the first launch of the day after your set time, show a gentle pick-three modal drawn from open tasks. Skip any day without penalty."
/>
{#if settings.ritualsMorning}
<div class="pl-1 py-3 animate-fade-in">
<label for="triage-time" class="block text-[10px] font-medium text-text-tertiary uppercase tracking-wider mb-2">
Earliest triage time
</label>
<input
id="triage-time"
type="time"
class="bg-bg-input border border-border rounded-lg px-3 py-1.5 text-[12px] text-text focus:border-accent focus:outline-none"
bind:value={settings.ritualsMorningTime}
/>
<p class="text-[11px] text-text-tertiary mt-2">
ADHD sleep inertia is intense for the first 3045 minutes after waking. Pick a time when you're genuinely ready to decide.
</p>
</div>
{/if}
<Toggle
bind:checked={settings.ritualsEvening}
label="Evening wind-down"
description="A reflective page you can open when you want to close the day. Not scheduled, never nagging."
/>
{#if settings.ritualsEvening}
<div class="pl-1 py-3 animate-fade-in">
<button
type="button"
class="px-3 py-2 rounded-lg bg-bg-elevated border border-border text-[12px] text-text hover:border-accent"
onclick={openWindDown}
>
Open wind-down now
</button>
</div>
{/if}
<div class="mt-4 pt-4 border-t border-border-subtle">
<!-- One-way flow: click → OS call → state update. Can't use
the standard Toggle because its bind:checked would
race the autostart invoke and let the UI lie during
the round-trip. -->
<div class="flex items-start gap-3 py-2.5">
<button
class="relative mt-0.5 w-[38px] min-w-[38px] h-[22px] rounded-full flex-shrink-0
{settings.launchAtLogin ? 'bg-accent shadow-[0_0_8px_rgba(232,168,124,0.25)]' : 'bg-bg-elevated'}
active:scale-95 disabled:opacity-60"
style="transition-duration: var(--duration-ui)"
onclick={() => setLaunchAtLogin(!settings.launchAtLogin)}
disabled={autostartSyncing}
role="switch"
aria-checked={settings.launchAtLogin}
aria-label="Launch Corbie at login"
>
<span
class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white shadow-sm
{settings.launchAtLogin ? 'translate-x-[16px]' : 'translate-x-0'}"
style="transition: transform var(--duration-ui) cubic-bezier(0.34, 1.56, 0.64, 1)"
></span>
</button>
<div class="flex-1 min-w-0">
<p class="text-[13px] text-text leading-tight">Launch Corbie at login</p>
<p class="text-[11px] text-text-tertiary mt-0.5 leading-snug">
So Corbie is already there at the start of the day. Uses your OS's standard autostart — no background tricks.
</p>
{#if autostartSyncing}
<p class="text-[11px] text-text-tertiary mt-1">Updating…</p>
{/if}
</div>
</div>
</div>
</div>
{/if}
</div>
<!-- Read aloud (Phase 4 roadmap) -->
<div class="border-b border-border-subtle">
<button