From 7daf5677c9f582972fdc5f54ba4c3cc34ff7271b Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 26 Apr 2026 19:22:02 +0100 Subject: [PATCH] =?UTF-8?q?feat(ux):=20B3.5=20+=20B3.14=20=E2=80=94=20spar?= =?UTF-8?q?kline=20range=20picker=20+=20shutdown=20copy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B3.5: settings.sparklineRangeDays (added schema-only in B2b) now drives the recentCompletions fetch via a reactive effect; new segmented control in Settings → Tasks lets the user pick 7 / 28 / 90 days. Default 7. B3.14: /shutdown "Open loops" header softened to "Still here", body copy updated to remove pressure ("Just naming them helps — nothing to do here"), truncation tail drops the count. The whole "Still here" section is now gated by a fresh-start derived flag — when B3.1 lands and writes reentryFreshStartUntil, this surface auto-suppresses. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/pages/SettingsPage.svelte | 50 +++++++++++++++++- src/lib/pages/ShutdownRitualPage.svelte | 67 +++++++++++++++--------- src/lib/stores/completionStats.svelte.ts | 29 ++++++++-- 3 files changed, 118 insertions(+), 28 deletions(-) diff --git a/src/lib/pages/SettingsPage.svelte b/src/lib/pages/SettingsPage.svelte index b94df0c..43ed3d1 100644 --- a/src/lib/pages/SettingsPage.svelte +++ b/src/lib/pages/SettingsPage.svelte @@ -814,6 +814,33 @@ } }); + // B3.5 — sparkline range picker. SegmentedButton works on string + // options; settings.sparklineRangeDays is the integer 7 / 28 / 90. + // Mirror in both directions so changes from either side propagate. + let sparklineRangeLabel = $state( + settings.sparklineRangeDays === 90 + ? "90 days" + : settings.sparklineRangeDays === 28 + ? "28 days" + : "7 days", + ); + $effect(() => { + const next = sparklineRangeLabel === "90 days" ? 90 : sparklineRangeLabel === "28 days" ? 28 : 7; + if (settings.sparklineRangeDays !== next) { + settings.sparklineRangeDays = next; + } + }); + $effect(() => { + const label = settings.sparklineRangeDays === 90 + ? "90 days" + : settings.sparklineRangeDays === 28 + ? "28 days" + : "7 days"; + if (sparklineRangeLabel !== label) { + sparklineRangeLabel = label; + } + }); + async function downloadModel(size) { downloadingModel = size; downloadProgress = 0; @@ -1575,8 +1602,29 @@ + + + {#if settings.showMomentumSparkline} +
+

Sparkline range

+ +

+ How far back the sparkline looks. Wider windows show longer-arc trends; the narrower window stays close to right-now. +

+
+ {/if} {/if} diff --git a/src/lib/pages/ShutdownRitualPage.svelte b/src/lib/pages/ShutdownRitualPage.svelte index 3a5b190..0a787ee 100644 --- a/src/lib/pages/ShutdownRitualPage.svelte +++ b/src/lib/pages/ShutdownRitualPage.svelte @@ -17,9 +17,21 @@ import { onMount } from 'svelte'; import { invoke } from '@tauri-apps/api/core'; import { Moon, ArrowLeft } from 'lucide-svelte'; - import { page } from '$lib/stores/page.svelte.js'; + import { page, settings } from '$lib/stores/page.svelte.js'; import { hasTauriRuntime } from '$lib/utils/runtime.js'; + // B3.14 — fresh-start gate. When B3.1 lands and stamps + // settings.reentryFreshStartUntil with a future timestamp (the user + // has been away long enough to warrant a "yesterday is sealed" + // window), the "Still here" reflection is suppressed so the shutdown + // surface doesn't drag forward what the fresh-start window is + // explicitly inviting the user to set down. The `!!` guards against + // the legacy null and any malformed string. + const inFreshStartWindow = $derived( + !!settings.reentryFreshStartUntil && + new Date(settings.reentryFreshStartUntil).getTime() > Date.now(), + ); + interface TaskRow { id: string; text: string; @@ -110,29 +122,36 @@ {/if} - -
-

Open loops

- {#if openLoops.length === 0} -

Nothing in the list.

- {:else} -

- These are still here. Naming them silences the loop — you don't have to act now. -

-
    - {#each openLoops.slice(0, 12) as task (task.id)} -
  • - {task.text} -
  • - {/each} - {#if openLoops.length > 12} -
  • - …and {openLoops.length - 12} more. -
  • - {/if} -
- {/if} -
+ + {#if !inFreshStartWindow} +
+

Still here

+ {#if openLoops.length === 0} +

Nothing in the list.

+ {:else} +

+ Still on the list. Just naming them helps — nothing to do here. +

+
    + {#each openLoops.slice(0, 12) as task (task.id)} +
  • + {task.text} +
  • + {/each} + {#if openLoops.length > 12} +
  • + …the rest is on the Tasks page when you want it. +
  • + {/if} +
+ {/if} +
+ {/if}