Files
hikari-desktop/src/lib/components/TodoPanel.svelte
T
hikari 3194a3cca5 feat: add visual todo list panel
- Add TodoPanel component to display TodoWrite tool calls
- Create todos Svelte store to track todo state
- Emit todo-update Tauri event when TodoWrite is called
- Add todo button to status bar (next to session history)
- Display todos with status icons, progress bar, and completion count
- Real-time updates as I work through tasks

Closes #132
2026-02-07 14:47:20 -08:00

183 lines
6.2 KiB
Svelte

<script lang="ts">
import { todos, type TodoItem } from "$lib/stores/todos";
import { CheckCircle, Circle, Loader } from "lucide-svelte";
interface Props {
onClose: () => void;
}
const { onClose }: Props = $props();
const currentTodos = $derived($todos);
const hasTodos = $derived(currentTodos.length > 0);
const completedCount = $derived(currentTodos.filter((t) => t.status === "completed").length);
const totalCount = $derived(currentTodos.length);
</script>
<div
class="fixed top-0 right-0 h-full w-96 bg-gray-900 border-l border-pink-500/30 shadow-2xl flex flex-col z-50"
>
<!-- Header -->
<div class="flex items-center justify-between p-4 border-b border-pink-500/30">
<div class="flex items-center gap-3">
<div class="text-pink-400">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
/>
</svg>
</div>
<div>
<h2 class="text-lg font-semibold text-white">Hikari's Todo List</h2>
{#if hasTodos}
<p class="text-xs text-gray-400">
{completedCount} of {totalCount} completed
</p>
{/if}
</div>
</div>
<button
onclick={onClose}
class="text-gray-400 hover:text-white transition-colors p-1 rounded-lg hover:bg-gray-800"
aria-label="Close todo panel"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
<!-- Content -->
<div class="flex-1 overflow-y-auto p-4">
{#if !hasTodos}
<div class="flex flex-col items-center justify-center h-full text-gray-500">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-16 w-16 mb-4 opacity-50"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<p class="text-center">No active todos</p>
<p class="text-sm text-center mt-2">I'll update this when I start working on tasks!</p>
</div>
{:else}
<div class="space-y-2">
{#each currentTodos as todo (todo.content)}
<div
class="group bg-gray-800/50 rounded-lg p-3 border border-gray-700 hover:border-pink-500/50 transition-all"
class:opacity-60={todo.status === "completed"}
>
<div class="flex items-start gap-3">
<!-- Status Icon -->
<div class="mt-0.5 flex-shrink-0">
{#if todo.status === "completed"}
<CheckCircle class="w-5 h-5 text-green-400" />
{:else if todo.status === "in_progress"}
<Loader class="w-5 h-5 text-pink-400 animate-spin" />
{:else}
<Circle class="w-5 h-5 text-gray-500" />
{/if}
</div>
<!-- Content -->
<div class="flex-1 min-w-0">
<p
class="text-sm font-medium"
class:text-gray-400={todo.status === "completed"}
class:line-through={todo.status === "completed"}
class:text-white={todo.status !== "completed"}
>
{todo.status === "in_progress" ? todo.activeForm : todo.content}
</p>
<!-- Status Badge -->
<div class="mt-1">
{#if todo.status === "completed"}
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-900/30 text-green-400 border border-green-500/30"
>
✓ Completed
</span>
{:else if todo.status === "in_progress"}
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-pink-900/30 text-pink-400 border border-pink-500/30 animate-pulse"
>
⚡ In Progress
</span>
{:else}
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-700/30 text-gray-400 border border-gray-600/30"
>
○ Pending
</span>
{/if}
</div>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
<!-- Footer with Progress Bar -->
{#if hasTodos}
<div class="border-t border-pink-500/30 p-4 bg-gray-800/50">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-medium text-gray-400">Progress</span>
<span class="text-xs font-medium text-pink-400">
{Math.round((completedCount / totalCount) * 100)}%
</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2 overflow-hidden">
<div
class="bg-gradient-to-r from-pink-500 to-purple-500 h-2 rounded-full transition-all duration-500 ease-out"
style="width: {(completedCount / totalCount) * 100}%"
></div>
</div>
</div>
{/if}
</div>
<style>
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
</style>