From 78940d5b7ee2f3e5020f87b400db1785b37a7d71 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 1 Jan 2026 08:48:35 -0600 Subject: wip(app): file context --- packages/app/src/pages/session.tsx | 307 ++++++++++++++++++++++++------------- 1 file changed, 201 insertions(+), 106 deletions(-) (limited to 'packages/app/src/pages') diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index f738fec33..f0e6a6e1d 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -5,8 +5,8 @@ import { Show, Match, Switch, - createResource, createMemo, + createResource, createEffect, on, createRenderEffect, @@ -14,7 +14,8 @@ import { } from "solid-js" import { Dynamic } from "solid-js/web" -import { useLocal, type LocalFile } from "@/context/local" +import { useLocal } from "@/context/local" +import { selectionFromLines, useFile, type SelectedLineRange } from "@/context/file" import { createStore } from "solid-js/store" import { PromptInput } from "@/components/prompt-input" import { SessionContextUsage } from "@/components/session-context-usage" @@ -276,6 +277,7 @@ function Header(props: { onMobileMenuToggle?: () => void }) { export default function Page() { const layout = useLayout() const local = useLocal() + const file = useFile() const sync = useSync() const terminal = useTerminal() const dialog = useDialog() @@ -289,6 +291,58 @@ export default function Page() { const permission = usePermission() const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey())) + + function normalizeTab(tab: string) { + if (!tab.startsWith("file://")) return tab + return file.tab(tab) + } + + function normalizeTabs(list: string[]) { + const seen = new Set() + const next: string[] = [] + for (const item of list) { + const value = normalizeTab(item) + if (seen.has(value)) continue + seen.add(value) + next.push(value) + } + return next + } + + const openTab = (value: string) => { + const next = normalizeTab(value) + tabs().open(next) + + const path = file.pathFromTab(next) + if (path) file.load(path) + } + + createEffect(() => { + const active = tabs().active() + if (!active) return + + const path = file.pathFromTab(active) + if (path) file.load(path) + }) + + createEffect(() => { + const current = tabs().all() + if (current.length === 0) return + + const next = normalizeTabs(current) + if (same(current, next)) return + + tabs().setAll(next) + + const active = tabs().active() + if (!active) return + if (!active.startsWith("file://")) return + + const normalized = normalizeTab(active) + if (active === normalized) return + tabs().setActive(normalized) + }) + const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const revertMessageID = createMemo(() => info()?.revert?.messageID) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) @@ -322,7 +376,6 @@ export default function Page() { ) const [store, setStore] = createStore({ - clickTimer: undefined as number | undefined, activeDraggable: undefined as string | undefined, activeTerminalDraggable: undefined as string | undefined, userInteracted: false, @@ -659,30 +712,6 @@ export default function Page() { document.removeEventListener("keydown", handleKeyDown) }) - const resetClickTimer = () => { - if (!store.clickTimer) return - clearTimeout(store.clickTimer) - setStore("clickTimer", undefined) - } - - const startClickTimer = () => { - const newClickTimer = setTimeout(() => { - setStore("clickTimer", undefined) - }, 300) - setStore("clickTimer", newClickTimer as unknown as number) - } - - const handleTabClick = async (tab: string) => { - if (store.clickTimer) { - resetClickTimer() - } else { - if (tab.startsWith("file://")) { - local.file.open(tab.replace("file://", "")) - } - startClickTimer() - } - } - const handleDragStart = (event: unknown) => { const id = getDraggableId(event) if (!id) return @@ -748,57 +777,24 @@ export default function Page() { ) } - const FileVisual = (props: { file: LocalFile; active?: boolean }): JSX.Element => { + const FileVisual = (props: { path: string; active?: boolean }): JSX.Element => { return (
- - {props.file.name} - - + {getFilename(props.path)}
) } - const SortableTab = (props: { - tab: string - onTabClick: (tab: string) => void - onTabClose: (tab: string) => void - }): JSX.Element => { + const SortableTab = (props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element => { const sortable = createSortable(props.tab) - const [file] = createResource( - () => props.tab, - async (tab) => { - if (tab.startsWith("file://")) { - return local.file.node(tab.replace("file://", "")) - } - return undefined - }, - ) + const path = createMemo(() => file.pathFromTab(props.tab)) return ( // @ts-ignore
@@ -811,11 +807,8 @@ export default function Page() { } hideCloseButton - onClick={() => props.onTabClick(props.tab)} > - - {(f) => } - + {(p) => }
@@ -1377,7 +1370,7 @@ export default function Page() { > - +
@@ -1414,9 +1407,7 @@ export default function Page() { - - {(tab) => } - + {(tab) => }
{(tab) => { - const [file] = createResource( - () => tab, - async (tab) => { - if (tab.startsWith("file://")) { - return local.file.node(tab.replace("file://", "")) - } - return undefined - }, + let scroll: HTMLDivElement | undefined + let scrollFrame: number | undefined + let pendingTop: number | undefined + + const path = createMemo(() => file.pathFromTab(tab)) + const state = createMemo(() => { + const p = path() + if (!p) return + return file.get(p) + }) + const contents = createMemo(() => state()?.content?.content ?? "") + const selectedLines = createMemo(() => { + const p = path() + if (!p) return null + return file.selectedLines(p) ?? null + }) + const selection = createMemo(() => { + const range = selectedLines() + if (!range) return + return selectionFromLines(range) + }) + const selectionLabel = createMemo(() => { + const sel = selection() + if (!sel) return + if (sel.startLine === sel.endLine) return `L${sel.startLine}` + return `L${sel.startLine}-${sel.endLine}` + }) + + const restoreScroll = () => { + const el = scroll + const p = path() + if (!el || !p) return + + const top = file.scrollTop(p) + if (top === undefined) return + if (el.scrollTop === top) return + el.scrollTop = top + } + + const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { + const p = path() + if (!p) return + + pendingTop = event.currentTarget.scrollTop + if (scrollFrame !== undefined) return + + scrollFrame = requestAnimationFrame(() => { + scrollFrame = undefined + + const top = pendingTop + pendingTop = undefined + if (top === undefined) return + + file.setScrollTop(p, top) + }) + } + + createEffect( + on( + () => state()?.loaded, + (loaded) => { + if (!loaded) return + requestAnimationFrame(restoreScroll) + }, + { defer: true }, + ), + ) + + createEffect( + on( + () => file.ready(), + (ready) => { + if (!ready) return + requestAnimationFrame(restoreScroll) + }, + { defer: true }, + ), ) + + onCleanup(() => { + if (scrollFrame === undefined) return + cancelAnimationFrame(scrollFrame) + }) + return ( - - - - {(f) => ( - { + scroll = el + restoreScroll() + }} + onScroll={handleScroll} + > + + {(sel) => ( +
+ +
+ )} +
+ + + { + const p = path() + if (!p) return + file.setSelectedLines(p, range) + }} + overflow="scroll" + class="select-text pb-40" + /> + + +
Loading...
+
+ + {(err) =>
{err()}
}
@@ -1493,19 +1596,11 @@ export default function Page() { - {(draggedFile) => { - const [file] = createResource( - () => draggedFile(), - async (tab) => { - if (tab.startsWith("file://")) { - return local.file.node(tab.replace("file://", "")) - } - return undefined - }, - ) + {(tab) => { + const path = createMemo(() => file.pathFromTab(tab())) return (
- {(f) => } + {(p) => }
) }} -- cgit v1.2.3