feat: productivity suite — task loop, workflow, theming, docs & more #197

Merged
naomi merged 16 commits from feat/productivity into main 2026-03-07 03:08:33 -08:00
Showing only changes of commit 80a59a0b22 - Show all commits
+32 -11
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { claudeStore, type TerminalLine } from "$lib/stores/claude";
import { afterUpdate, tick, onMount, onDestroy } from "svelte";
import { get } from "svelte/store";
import { invoke } from "@tauri-apps/api/core";
import ConversationTabs from "./ConversationTabs.svelte";
@@ -25,6 +26,7 @@
let isRestoringScroll = false;
let windowStart = 0;
let isLoadingMore = false;
let isSwitchingConversation = false;
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
searchQuery.subscribe((value) => {
@@ -59,24 +61,39 @@
currentConversationId = newId;
// Peek at the saved position to set windowStart before the first tick,
// preventing a stale windowStart from a previous conversation leaving
// visibleLines empty (windowStart >= lines.length).
// Guard the $: reactive auto-scroll block from firing with stale `lines`
// (the old conversation's data) during the switch. Without this, Svelte's
// reactive system can re-run the window-advance block before `terminalLines`
// has recomputed for the new conversation, overriding our correct windowStart.
isSwitchingConversation = true;
// Read the new conversation's lines directly from the store — the derived
// `terminalLines` store (and thus `lines`) may not have recomputed yet when
// this subscriber fires, so using `lines` here would give stale data.
const newConvLines = get(claudeStore.conversations).get(newId)?.terminalLines ?? [];
const savedPosition = claudeStore.getScrollPosition(newId);
if (savedPosition === -1) {
// Will auto-scroll: pin the window to the tail of the new conversation
shouldAutoScroll = true;
windowStart = Math.max(0, lines.length - WINDOW_SIZE);
windowStart = Math.max(0, newConvLines.length - WINDOW_SIZE);
} else {
// Will restore a specific position: always start from the top of history
shouldAutoScroll = false;
windowStart = 0;
}
// Restore scroll position for the new conversation after DOM updates
await tick();
if (terminalElement) {
// Block the scroll handler during the entire DOM transition — scroll events
// can fire mid-tick when the content changes, and handleScroll would see
// scrollTop not at the bottom yet and set shouldAutoScroll = false, breaking
// autoscroll for the new conversation permanently.
isRestoringScroll = true;
// Restore scroll position for the new conversation after DOM updates.
// Clear the switch guard first so the $: block can react to new lines
// arriving after the switch settles.
await tick();
isSwitchingConversation = false;
if (terminalElement) {
if (savedPosition === -1) {
terminalElement.scrollTop = terminalElement.scrollHeight;
} else {
@@ -94,8 +111,10 @@
const { scrollTop, scrollHeight, clientHeight } = terminalElement;
shouldAutoScroll = scrollHeight - scrollTop - clientHeight < 100;
// Load older lines when the user scrolls near the top
if (scrollTop < 300 && windowStart > 0 && !isLoadingMore) {
// Load older lines when the user scrolls near the top of the visible window.
// Use windowStart * AVG_LINE_HEIGHT (the spacer height) as the baseline so
// we trigger at the top of the rendered content, not the absolute container top.
if (scrollTop < windowStart * AVG_LINE_HEIGHT + 300 && windowStart > 0 && !isLoadingMore) {
isLoadingMore = true;
const prevScrollHeight = terminalElement.scrollHeight;
const prevScrollTop = terminalElement.scrollTop;
@@ -182,8 +201,10 @@
// Height of the invisible spacer above the visible window
$: topSpacerHeight = windowStart * AVG_LINE_HEIGHT;
// Advance the window forward when auto-scrolling and new lines overflow it
$: if (shouldAutoScroll && lines.length > windowStart + WINDOW_SIZE) {
// Advance the window forward when auto-scrolling and new lines overflow it.
// Skip during conversation switches — `lines` may still hold the previous
// conversation's data, which would push windowStart past the new conv's end.
$: if (shouldAutoScroll && !isSwitchingConversation && lines.length > windowStart + WINDOW_SIZE) {
windowStart = Math.max(0, lines.length - WINDOW_SIZE);
}