feat(tasks): PR 1.2 — default to Today + WIP Now lane reads filtered set

Flips the Tasks landing bucket from All to Today so the cold-open answer to
"what should I do now?" is scoped to today's commitments rather than the
full backlog. Refactors WipTaskList to accept a tasks prop instead of
reading the global store, and wires it into TasksPage so the Now lane and
the bucket list below share one filtered set as their source of truth —
the lane consumes the first three top-level rows and the bucket list
excludes those IDs, so no task is ever rendered in both panels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 18:25:55 +01:00
parent 07925cf360
commit 6389fc2ce7
2 changed files with 46 additions and 6 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { tasks, addTask, completeTask, uncompleteTask, deleteTask, setTaskEnergy } from '$lib/stores/page.svelte.js';
import { addTask, completeTask, uncompleteTask, deleteTask, setTaskEnergy } from '$lib/stores/page.svelte.js';
import type { TaskEntry } from '$lib/types/app';
import MicroSteps from '$lib/components/MicroSteps.svelte';
import EnergyChip from '$lib/components/EnergyChip.svelte';
import { ChevronDown, ChevronRight, Timer } from 'lucide-svelte';
@@ -10,14 +11,26 @@
}));
}
let { wipLimit = 3 } = $props();
// PR 1.2: the Now lane no longer reads the global tasks store. The
// parent passes an already-filtered `tasks` array (respecting the
// active bucket, list, search, and energy sort) so the Now lane and
// the bucket list below share one filtered set as source of truth.
// Subtasks are still excluded here — that is a Now-lane invariant,
// not a filter the parent owns.
let {
tasks = [],
wipLimit = 3,
}: {
tasks?: TaskEntry[];
wipLimit?: number;
} = $props();
let newTaskText = $state('');
let showOverflow = $state(false);
let expandedTaskIds = $state(new Set<string>());
// Exclude subtasks from WIP count — only top-level tasks count
let activeTasks = $derived(tasks.filter(t => !t.done && !t.parentTaskId));
let activeTasks = $derived(tasks.filter((t) => !t.done && !t.parentTaskId));
let visibleTasks = $derived(activeTasks.slice(0, wipLimit));
let overflowTasks = $derived(activeTasks.slice(wipLimit));
let hasOverflow = $derived(overflowTasks.length > 0);

View File

@@ -19,7 +19,10 @@
import { formatTimestamp } from "$lib/utils/time.js";
import { BUCKET_COLORS, EFFORT_LABELS, EFFORT_ORDER } from "$lib/utils/constants.js";
let activeBucket = $state("all");
// PR 1.2: default landing bucket is Today rather than All. The cold-open
// surface area for "what should I do now?" is now scoped to today's
// commitments, with All still reachable as a one-click filter.
let activeBucket = $state("today");
let activeListId = $state("all");
let showCompleted = $state(false);
let quickInput = $state("");
@@ -94,6 +97,20 @@
return list;
});
// PR 1.2: the Now lane consumes the same filtered set as the bucket
// list so its rows respect the active bucket / list / search / sort.
// We compute the rendered slice here (top-level, capped at wipLimit)
// and exclude those IDs from the bucket list — no row appears twice.
// Computing in the parent (rather than via callback) avoids a
// first-paint flash where both panels briefly render the same row
// before an effect reconciles them.
const NOW_LANE_LIMIT = 3;
let nowLaneTasks = $derived(
filteredTasks.filter((t) => !t.parentTaskId).slice(0, NOW_LANE_LIMIT),
);
let nowLaneIds = $derived(new Set(nowLaneTasks.map((t) => t.id)));
let bucketListTasks = $derived(filteredTasks.filter((t) => !nowLaneIds.has(t.id)));
function cycleCurrentEnergy(next: EnergyLevel | null) {
settings.currentEnergy = next;
saveSettings();
@@ -580,6 +597,16 @@
<!-- Task list -->
<div class="flex-1 overflow-y-auto min-h-0">
<!-- PR 1.2: Now lane sits above the bucket list and shares its
filtered set. The bucket list excludes whatever IDs the lane
rendered, so a task is never shown in both. The lane is only
mounted when it has something to show — when the filter is
empty, the EmptyState below carries onboarding copy alone. -->
{#if nowLaneTasks.length > 0}
<div class="mb-3">
<WipTaskList tasks={nowLaneTasks} wipLimit={NOW_LANE_LIMIT} />
</div>
{/if}
{#if filteredTasks.length === 0}
<EmptyState
icon={SquareCheck}
@@ -587,9 +614,9 @@
? "No matching tasks"
: "Add tasks manually. Automatic extraction from your transcripts is coming."}
/>
{:else}
{:else if bucketListTasks.length > 0}
<div class="flex flex-col gap-1">
{#each filteredTasks as task (task.id)}
{#each bucketListTasks as task (task.id)}
<div class="group flex items-start gap-3 p-3 rounded-xl bg-bg-card border border-border-subtle hover:border-border"
style="transition-duration: var(--duration-ui)">
<!-- Checkbox -->