Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/web/src/components/Sidebar.motion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ export function createSidebarListMotion(parent: HTMLUListElement) {
return {
update(animate: boolean) {
if (disposed) return;
// A hidden list (the closed narrow sheet stays mounted) reports zero
// offsets, which would make the first pass after it opens animate every row.
if (parent.getClientRects().length === 0) {
suspend();
return;
}
const next = new Map(
Array.from(parent.children)
.filter((node): node is HTMLElement => node instanceof HTMLElement && !exiting.has(node))
Expand Down
27 changes: 19 additions & 8 deletions apps/web/src/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type SidebarContextProps = {
setOpen: (open: boolean) => void;
openMobile: boolean;
mobileSheetMounted: boolean;
setMobileSheetElement: (element: HTMLDivElement | null) => void;
setMobileSheetMounted: (mounted: boolean) => void;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
Expand Down Expand Up @@ -110,9 +110,13 @@ function SidebarProvider({
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
const [mobileSheetMounted, setMobileSheetMounted] = React.useState(false);
const setMobileSheetElement = (element: HTMLDivElement | null) => {
setMobileSheetMounted(element !== null);
};
if (openMobile && !mobileSheetMounted) {
setMobileSheetMounted(true);
}
// Widening mid-close unmounts the sheet before its close completes.
if (!isMobile && !openMobile && mobileSheetMounted) {
setMobileSheetMounted(false);
}

const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
Expand Down Expand Up @@ -143,7 +147,7 @@ function SidebarProvider({
open,
openMobile,
mobileSheetMounted,
setMobileSheetElement,
setMobileSheetMounted,
setOpen,
setOpenMobile,
state,
Expand Down Expand Up @@ -214,7 +218,7 @@ function Sidebar({
collapsible?: "offcanvas" | "icon" | "none";
resizable?: boolean | SidebarResizableOptions;
}) {
const { isMobile, state, openMobile, setOpenMobile, setMobileSheetElement } = useSidebar();
const { isMobile, state, openMobile, setOpenMobile, setMobileSheetMounted } = useSidebar();
const desktopSidebarRef = React.useRef<HTMLDivElement>(null);
const offcanvasCollapsed = !isMobile && collapsible === "offcanvas" && state === "collapsed";

Expand Down Expand Up @@ -256,9 +260,16 @@ function Sidebar({
if (isMobile) {
return (
<SidebarInstanceContext value={instanceContextValue}>
<Sheet onOpenChange={setOpenMobile} open={openMobile}>
<Sheet
onOpenChange={setOpenMobile}
onOpenChangeComplete={(open) => {
if (!open) setMobileSheetMounted(false);
}}
open={openMobile}
>
{/* Children stay mounted while closed so their window shortcuts, like thread jumps, keep working. */}
<SheetPopup
ref={setMobileSheetElement}
keepMounted
className={cn(
"w-(--sidebar-width) max-w-none bg-sidebar surface-grain p-0 text-sidebar-foreground",
className,
Expand Down
117 changes: 117 additions & 0 deletions scripts/seed-sidebar-evidence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Seed synthetic sidebar projections in a stopped, isolated, initialized T3 home.

Usage: python3 scripts/seed-sidebar-evidence.py --base-dir /tmp/sidebar-proof/.t3 \
--workspace /tmp/sidebar-proof/workspace [--empty]

Start the server once to migrate an empty isolated home, then stop it first.
Restart with T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD=false so startup does not
create an unrelated project and fourth thread.
This fixture creates no event history and must not prove backend behavior.
Use identical workspace, settings, bindings and a fixed browser clock for pairs.
"""

import argparse
import json
import os
from pathlib import Path
import sqlite3


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-dir", type=Path, required=True)
parser.add_argument("--workspace", type=Path, required=True)
parser.add_argument("--empty", action="store_true")
args = parser.parse_args()
base = args.base_dir.resolve()
if base == (Path.home() / ".t3").resolve() or (base / "userdata").is_symlink():
parser.error("Use a copied, isolated home, never the live T3 home.")
db = base / "userdata/statev2.sqlite"
if db.is_symlink():
parser.error("The isolated database must be a copy, never a symlink.")
if not db.is_file():
parser.error("Initialize an empty isolated home with the server first.")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
try:
is_live_db = db.samefile(Path.home() / ".t3/userdata/statev2.sqlite")
except FileNotFoundError:
is_live_db = False
if is_live_db:
parser.error("The isolated database must be a copy, never a hardlink.")
runtime_file = base / "userdata/server-runtime.json"
if runtime_file.is_file():
runtime = json.loads(runtime_file.read_text())
try:
os.kill(runtime["pid"], 0)
except ProcessLookupError:
pass
else:
parser.error("Stop the isolated server before seeding its database.")
workspace = args.workspace.resolve(strict=True)
if not workspace.is_dir():
parser.error("Workspace must be a directory.")

with sqlite3.connect(db) as connection:
# Refuse existing work, including runnable effects, rather than clearing it.
for table in (
"orchestration_v2_projection_messages",
"orchestration_v2_projection_runs",
"orchestration_v2_projection_runtime_requests",
"orchestration_v2_effect_outbox",
"scheduled_tasks",
):
if connection.execute(f"SELECT count(*) FROM {table}").fetchone()[0]:
parser.error(f"Expected empty synthetic state: {table} is populated.")
project, project_title = "shortcut-fixtures", "Shortcut fixtures"
# Only rows this script writes may be replaced; match them exactly.
if connection.execute(
"SELECT count(*) FROM orchestration_v2_projection_threads WHERE NOT "
"(thread_id IN ('shortcut-fixture-1','shortcut-fixture-2','shortcut-fixture-3') "
"AND project_id IS ?)",
(project,),
).fetchone()[0]:
parser.error("Refusing to replace nonfixture threads.")
if connection.execute(
"SELECT count(*) FROM projection_projects "
"WHERE NOT (project_id IS ? AND title IS ?)",
(project, project_title),
).fetchone()[0]:
parser.error("Refusing to replace nonfixture projects.")

clock = "2026-10-22T08:00:00.000Z"
connection.execute("DELETE FROM orchestration_v2_projection_threads")
connection.execute("DELETE FROM projection_projects")
connection.execute(
"INSERT INTO projection_projects "
"(project_id,title,workspace_root,scripts_json,created_at,updated_at) "
"VALUES (?,?,?,?,?,?)",
(project, project_title, str(workspace), "[]", clock, clock),
)
for index, title in enumerate(() if args.empty else ("Alpha", "Beta", "Gamma"), 1):
thread = f"shortcut-fixture-{index}"
payload = dict(
id=thread, projectId=project, title=f"Thread {title}",
createdBy="system", creationSource="server",
providerInstanceId="codex",
modelSelection=dict(instanceId="codex", model="gpt-6-astra"),
runtimeMode="full-access", interactionMode="default",
branch=None, worktreePath=None, pullRequests=[],
activeProviderThreadId=None, forkedFrom=None,
lineage=dict(parentThreadId=None, relationshipToParent=None, rootThreadId=thread),
createdAt=clock, updatedAt=clock, lastVisitedAt=clock,
archivedAt=None, deletedAt=None, settledOverride=None, settledAt=None,
snoozedUntil=None, snoozedAt=None, activeOrderKey=str(index),
)
connection.execute(
"INSERT INTO orchestration_v2_projection_threads "
"(thread_id,project_id,title,default_provider,provider_instance_id,"
"runtime_mode,interaction_mode,created_at,updated_at,payload_json) "
"VALUES (?,?,?,?,?,?,?,?,?,?)",
(thread, project, payload["title"], "codex", "codex", "full-access",
"default", clock, clock, json.dumps(payload)),
)
print(f"Seeded {0 if args.empty else 3} synthetic stopped threads in {db}")


if __name__ == "__main__":
main()
Loading