fix(panel): column-reverse thread keeps the bottom, no island in the plugin

This commit is contained in:
hh
2026-09-02 04:58:48 +02:00
parent 2815956f5b
commit fe1c894b93
4 changed files with 11 additions and 90 deletions
+6 -9
View File
@@ -79,20 +79,17 @@
} }
} }
// The scroller is column-reverse: its origin is the bottom, scrollTop is
// zero there and goes negative upward, and the browser keeps the view at
// the bottom while content grows - no matter how late markdown lands.
function nearBottom(): boolean { function nearBottom(): boolean {
if (!scroller) { return !scroller || Math.abs(scroller.scrollTop) < NEAR_BOTTOM_PX;
return true;
}
const left =
scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
return left < NEAR_BOTTOM_PX;
} }
function toBottom() { function toBottom() {
if (scroller) { if (scroller) {
scroller.scrollTop = scroller.scrollHeight; scroller.scrollTop = 0;
} }
// Whatever actually scrolls - this box, or a host's pane around it.
end?.scrollIntoView({ block: "end" }); end?.scrollIntoView({ block: "end" });
} }
@@ -211,7 +208,7 @@
</script> </script>
<div <div
class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-3 py-3 @md:px-6" class="flex min-h-0 flex-1 flex-col-reverse overflow-y-auto overflow-x-hidden px-3 py-3 @md:px-6"
onscroll={() => { onscroll={() => {
if (Date.now() > settleUntil) { if (Date.now() > settleUntil) {
pinned = nearBottom(); pinned = nearBottom();
-2
View File
@@ -9,7 +9,6 @@
import KindMark from "$lib/shell/kind-mark.svelte"; import KindMark from "$lib/shell/kind-mark.svelte";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import type { ConversationIndex } from "./index.svelte"; import type { ConversationIndex } from "./index.svelte";
import PanelIsland from "./panel-island.svelte";
let { let {
client, client,
@@ -35,7 +34,6 @@
</script> </script>
<div class="flex items-center gap-1.5 border-b px-2 py-1.5"> <div class="flex items-center gap-1.5 border-b px-2 py-1.5">
<PanelIsland compact {index} {onOpen} />
<Popover.Root bind:open={switcherOpen}> <Popover.Root bind:open={switcherOpen}>
<Popover.Trigger> <Popover.Trigger>
{#snippet child({ props })} {#snippet child({ props })}
-70
View File
@@ -1,70 +0,0 @@
<script lang="ts">
import { cn } from "$lib/utils";
import type { ConversationIndex } from "./index.svelte";
let {
index,
onOpen,
compact = false,
class: className = "",
}: {
index: ConversationIndex;
onOpen: (id: string) => void;
compact?: boolean;
class?: string;
} = $props();
const waiting = $derived(index.open.filter((row) => row.pending_question));
const running = $derived(
index.open.filter((row) => row.running_turn && !row.pending_question)
);
const live = $derived(index.live.state);
const dot = $derived.by(() => {
if (live === "failed") {
return "bg-destructive";
}
if (live !== "open") {
return "bg-warn";
}
return running.length > 0 ? "bg-signal animate-pulse-dot" : "bg-ok";
});
const headline = $derived.by(() => {
if (live === "failed") {
return "offline";
}
if (live !== "open") {
return "connecting";
}
if (running.length === 0) {
return "idle";
}
return running.length === 1 ? "1 in motion" : `${running.length} in motion`;
});
const target = $derived(waiting[0] ?? running[0] ?? null);
const questions = $derived.by(() => {
if (compact) {
return String(waiting.length);
}
return waiting.length === 1 ? "1 question" : `${waiting.length} questions`;
});
</script>
<button
aria-label={target ? `Open ${target.title ?? target.kind}` : headline}
class={cn("island", compact && "gap-1.5 px-2.5", className)}
disabled={!target}
onclick={() => target && onOpen(target.id)}
title={index.live.detail ?? headline}
type="button"
>
<span class={cn("size-2 shrink-0 rounded-full", dot)}></span>
{#if !compact}
<span class="font-medium">{headline}</span>
{/if}
{#if waiting.length > 0}
{#if !compact}
<span class="island-sep"></span>
{/if}
<span class="font-medium text-attention-foreground">{questions}</span>
{/if}
</button>
+5 -9
View File
@@ -5,7 +5,6 @@
import ConversationView from "./conversation-view.svelte"; import ConversationView from "./conversation-view.svelte";
import type { ConversationIndex } from "./index.svelte"; import type { ConversationIndex } from "./index.svelte";
import PanelBar from "./panel-bar.svelte"; import PanelBar from "./panel-bar.svelte";
import PanelIsland from "./panel-island.svelte";
let { let {
client, client,
@@ -43,15 +42,15 @@
<!-- <!--
impeccable direction contract (seed 071a8c77, mode operate, strip board × live activity, in Obsidian's skin) impeccable direction contract (seed 071a8c77, mode operate, strip board × live activity, in Obsidian's skin)
THESIS: the agent's front inside the vault, not a website in a pane. The island carries what is THESIS: the agent's front inside the vault, not a website in a pane. The rail is the day's
alive, the rail is the day's strips, the thread owns its column. strips, the thread owns its column; what is alive shows on the strips themselves.
OWN-WORLD: Obsidian's own variables through .beaver-root; strips with a state edge, hairline OWN-WORLD: Obsidian's own variables through .beaver-root; strips with a state edge, hairline
rules, tabular numerals; amber only for a question waiting; nothing decorative. rules, tabular numerals; amber only for a question waiting; nothing decorative.
STORY: the operator opens the tab and reads the island, picks today's strip, answers a question, STORY: the operator opens the tab, picks today's strip, answers a question,
watches tools stream; beside a deep chat the sidedock shows the agent's activity, never a watches tools stream; beside a deep chat the sidedock shows the agent's activity, never a
second copy of the note. second copy of the note.
FIRST VIEWPORT: narrow: island + switcher over the thread and the composer pinned low. Wide: FIRST VIEWPORT: narrow: the switcher over the thread and the composer pinned low. Wide:
the rail with the island on top, the thread to the right. the rail on the left, the thread to the right.
FORM: strip board × live activity; user-steered; seed key 071a8c77. FORM: strip board × live activity; user-steered; seed key 071a8c77.
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review,
the verdict, and DESIGN.md the verdict, and DESIGN.md
@@ -63,9 +62,6 @@ the verdict, and DESIGN.md
<div class="flex min-h-0 flex-1"> <div class="flex min-h-0 flex-1">
{#if wide} {#if wide}
<aside class="flex w-80 shrink-0 flex-col border-r"> <aside class="flex w-80 shrink-0 flex-col border-r">
<div class="flex items-center gap-2 px-3 pt-3 pb-1">
<PanelIsland {index} onOpen={open} />
</div>
<Rail {client} {index} onOpen={open} {onOpenFile} {selected} /> <Rail {client} {index} onOpen={open} {onOpenFile} {selected} />
</aside> </aside>
{/if} {/if}