fix(panel): column-reverse thread keeps the bottom, no island in the plugin
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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 })}
|
||||||
|
|||||||
@@ -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,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}
|
||||||
|
|||||||
Reference in New Issue
Block a user