feat(tasks): PR 1.5 — return refreshed subtask + parent from completion

Change complete_subtask_and_check_parent to return
(TaskRow, Option<TaskRow>): the refreshed subtask, plus the parent when
the cascade auto-completed it. Both rows are read inside the same
transaction so the frontend never observes partial state.

Wrap the cmd in a CompleteSubtaskResult { updatedSubtask,
autoCompletedParent } DTO. MicroSteps now applies the refreshed subtask
locally and, when present, calls a new replaceTaskFromDto helper on the
Tasks store so the parent's done/doneAt/badge counts update without a
follow-up list_tasks_cmd round-trip.

Test: complete_subtask_returns_updated_subtask_and_optional_parent
covers both paths (sibling-pending → None, last-sibling → Some(parent)).
All 61 kon-storage tests pass; svelte-check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 18:17:30 +01:00
parent 0a8cb55447
commit 6e663a3625
4 changed files with 126 additions and 7 deletions

View File

@@ -2,6 +2,8 @@
import { invoke } from '@tauri-apps/api/core';
import { ListTree, Check, Timer, Loader2, ThumbsUp, ThumbsDown, Pencil } from 'lucide-svelte';
import { profilesStore } from '$lib/stores/profiles.svelte.ts';
import { replaceTaskFromDto } from '$lib/stores/page.svelte.js';
import type { TaskDto } from '$lib/types/app.ts';
import SpeakerButton from '$lib/components/SpeakerButton.svelte';
let { parentTaskId, parentTaskText = '', reduceMotion = false } = $props();
@@ -71,9 +73,24 @@
async function checkStep(subtaskId: string) {
try {
await invoke('complete_subtask_cmd', { subtaskId });
const result = await invoke<{
updatedSubtask: TaskDto;
autoCompletedParent: TaskDto | null;
}>('complete_subtask_cmd', { subtaskId });
const idx = subtasks.findIndex(s => s.id === subtaskId);
if (idx >= 0) subtasks[idx] = { ...subtasks[idx], done: true };
if (idx >= 0) {
subtasks[idx] = {
...subtasks[idx],
done: result.updatedSubtask.done,
text: result.updatedSubtask.text,
};
}
// PR 1.5: when this completion auto-completed the parent, swap the
// refreshed parent into the Tasks store so its done state, doneAt,
// and badge counts pick up immediately — no full refetch needed.
if (result.autoCompletedParent) {
replaceTaskFromDto(result.autoCompletedParent);
}
// Phase 6 nudge-bus signal. Any completed step clears the
// micro-step-idle timer for this parent task — the breakdown
// is demonstrably getting worked, no nudge needed.

View File

@@ -432,6 +432,21 @@ function applyLocalTaskUpdate(id: string, updates: Partial<TaskEntry>) {
}
}
/**
* PR 1.5: replace an in-store task with a fresh DTO from the backend
* (e.g. the auto-completed parent returned by `complete_subtask_cmd`).
* No-op if the task is not in the store — that's expected when a parent
* has already been pruned client-side. Broadcasts on success so other
* windows pick up the new state.
*/
export function replaceTaskFromDto(row: TaskDto) {
const idx = tasks.findIndex((task) => task.id === row.id);
if (idx >= 0) {
tasks[idx] = mapTaskRow(row);
broadcastTasks();
}
}
export async function updateTask(id: string, updates: TaskUpdate) {
if (!hasTauriRuntime()) {
applyLocalTaskUpdate(id, updates as Partial<TaskEntry>);