From 725104572e2b6d64dcfc145d4748124186427c7b Mon Sep 17 00:00:00 2001 From: Dax Date: Mon, 15 Sep 2025 03:28:08 -0400 Subject: feat: add desktop/web app package (#2606) Co-authored-by: adamdotdevin <2363879+adamdottv@users.noreply.github.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: GitHub Action --- packages/app/src/pages/index.tsx | 608 ++++++++++++++++++++++++++++++++++++++ packages/app/src/pages/layout.tsx | 5 + 2 files changed, 613 insertions(+) create mode 100644 packages/app/src/pages/index.tsx create mode 100644 packages/app/src/pages/layout.tsx (limited to 'packages/app/src/pages') diff --git a/packages/app/src/pages/index.tsx b/packages/app/src/pages/index.tsx new file mode 100644 index 000000000..5922b28db --- /dev/null +++ b/packages/app/src/pages/index.tsx @@ -0,0 +1,608 @@ +import { FileIcon, Icon, IconButton, Tooltip } from "@/ui" +import { Tabs } from "@/ui/tabs" +import FileTree from "@/components/file-tree" +import { createSignal, For, Match, onCleanup, onMount, Show, Switch } from "solid-js" +import { useLocal, useSDK } from "@/context" +import { Code } from "@/components/code" +import { + DragDropProvider, + DragDropSensors, + DragOverlay, + SortableProvider, + createSortable, + closestCenter, + useDragDropContext, +} from "@thisbeyond/solid-dnd" +import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd" +import type { LocalFile } from "@/context/local" +import SessionList from "@/components/session-list" +import SessionTimeline from "@/components/session-timeline" + +export default function Page() { + const sdk = useSDK() + const local = useLocal() + const [clickTimer, setClickTimer] = createSignal() + const [activeItem, setActiveItem] = createSignal(undefined) + const [inputValue, setInputValue] = createSignal("") + const [isDragging, setIsDragging] = createSignal<"left" | "right" | undefined>(undefined) + const [leftScrolled, setLeftScrolled] = createSignal(false) + + // TODO: remove + local.model.set({ providerID: "opencode", modelID: "grok-code" }) + + let inputRef: HTMLInputElement | undefined = undefined + + const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control" + + onMount(() => { + document.addEventListener("keydown", handleKeyDown) + }) + + onCleanup(() => { + document.removeEventListener("keydown", handleKeyDown) + }) + + const handleKeyDown = (e: KeyboardEvent) => { + const inputFocused = document.activeElement === inputRef + if (inputFocused) { + if (e.key === "Escape") { + inputRef?.blur() + } + return + } + + if (local.file.active()) { + if (e.getModifierState(MOD)) { + if (e.key.toLowerCase() === "a") { + return + } + if (e.key.toLowerCase() === "c") { + return + } + } + } + + if (e.key.length === 1 && e.key !== "Unidentified") { + inputRef?.focus() + } + } + + const navigateChange = (dir: 1 | -1) => { + const active = local.file.active() + if (!active) return + const current = local.file.changeIndex(active.path) + const next = current == undefined ? (dir === 1 ? 0 : -1) : current + dir + local.file.setChangeIndex(active.path, next) + } + + const resetClickTimer = () => { + if (!clickTimer()) return + clearTimeout(clickTimer()) + setClickTimer(undefined) + } + + const startClickTimer = () => { + const newClickTimer = setTimeout(() => { + setClickTimer(undefined) + }, 300) + setClickTimer(newClickTimer as unknown as number) + } + + const handleFileClick = async (file: LocalFile) => { + if (clickTimer()) { + resetClickTimer() + local.file.update(file.path, { ...file, pinned: true }) + } else { + local.file.open(file.path) + startClickTimer() + } + } + + const handleTabChange = (path: string) => { + local.file.open(path) + } + + const handleTabClose = (file: LocalFile) => { + local.file.close(file.path) + } + + const onDragStart = (event: any) => { + setActiveItem(event.draggable.id as string) + } + + const onDragOver = (event: DragEvent) => { + const { draggable, droppable } = event + if (draggable && droppable) { + const currentFiles = local.file.opened().map((f) => f.path) + const fromIndex = currentFiles.indexOf(draggable.id.toString()) + const toIndex = currentFiles.indexOf(droppable.id.toString()) + if (fromIndex !== toIndex) { + local.file.move(draggable.id.toString(), toIndex) + } + } + } + + const onDragEnd = () => { + setActiveItem(undefined) + } + + const handleLeftDragStart = (e: MouseEvent) => { + e.preventDefault() + setIsDragging("left") + const startX = e.clientX + const startWidth = local.layout.leftWidth() + + const handleMouseMove = (e: MouseEvent) => { + const deltaX = e.clientX - startX + const newWidth = startWidth + deltaX + local.layout.setLeftWidth(newWidth) + } + + const handleMouseUp = () => { + setIsDragging(undefined) + document.removeEventListener("mousemove", handleMouseMove) + document.removeEventListener("mouseup", handleMouseUp) + } + + document.addEventListener("mousemove", handleMouseMove) + document.addEventListener("mouseup", handleMouseUp) + } + + const handleRightDragStart = (e: MouseEvent) => { + e.preventDefault() + setIsDragging("right") + const startX = e.clientX + const startWidth = local.layout.rightWidth() + + const handleMouseMove = (e: MouseEvent) => { + const deltaX = startX - e.clientX + const newWidth = startWidth + deltaX + local.layout.setRightWidth(newWidth) + } + + const handleMouseUp = () => { + setIsDragging(undefined) + document.removeEventListener("mousemove", handleMouseMove) + document.removeEventListener("mouseup", handleMouseUp) + } + + document.addEventListener("mousemove", handleMouseMove) + document.addEventListener("mouseup", handleMouseUp) + } + + const handleSubmit = async (e: SubmitEvent) => { + e.preventDefault() + const prompt = inputValue() + setInputValue("") + inputRef?.blur() + + const session = + (local.layout.rightPane() ? local.session.active() : undefined) ?? + (await sdk.session.create().then((x) => x.data!)) + local.session.setActive(session!.id) + local.layout.openRightPane() + + const response = await sdk.session.prompt({ + path: { id: session!.id }, + body: { + agent: local.agent.current()!.name, + model: local.model.current(), + parts: [ + { + type: "text", + text: prompt, + }, + ...local.file + .opened() + .filter((f) => f.selection || local.file.active()?.path === f.path) + .flatMap((f) => [ + { + type: "file" as const, + mime: "text/plain", + url: `file://${f.absolute}${f.selection ? `?start=${f.selection.startLine}&end=${f.selection.endLine}` : ""}`, + filename: f.name, + source: { + type: "file" as const, + text: { + value: "@" + f.name, + start: 0, // f.start, + end: 0, // f.end, + }, + path: f.absolute, + }, + }, + ]), + ], + }, + }) + + console.log("response", response) + } + + return ( +
+
+ +
+ + + Files + + + Changes + + +
+ setLeftScrolled(e.currentTarget.scrollTop > 0)} + > + + +
+ +
+ + +
No changes yet
+
+ +
+
handleLeftDragStart(e)} + > +
+
+ +
+
+ }> + {(activeSession) => ( +
+
+
+ local.session.clearActive()} + class="text-text-muted hover:text-text" + > + + +

+ {activeSession().title || "Untitled Session"} +

+
+
+ +
+ )} +
+
+
+
+
+
handleRightDragStart(e)} + > +
+
+ +
+ + + + +
+ + f.path)}> + + {(file) => } + + + +
+ + {(() => { + const f = local.file.active()! + const view = local.file.view(f.path) + return ( +
+ +
+ + navigateChange(-1)}> + + + + + navigateChange(1)}> + + + +
+
+ + local.file.setView(f.path, "raw")} + > + + + + + local.file.setView(f.path, "diff-unified")} + > + + + + + local.file.setView(f.path, "diff-split")} + > + + + +
+ ) + })()} +
+ + local.layout.toggleRightPane()}> + + + +
+
+ + {(file) => ( + + {(() => { + const view = local.file.view(file.path) + const showRaw = view === "raw" || !file.content?.diff + const code = showRaw ? (file.content?.content ?? "") : (file.content?.diff ?? "") + return + })()} + + )} + +
+ + {activeItem() && + (() => { + const draggedFile = local.file.node(activeItem()!) + return ( +
+ +
+ ) + })()} +
+
+
+
+
+ + local.file.close(local.file.active()?.path ?? "")} + /> + + x.selection)}> + {(file) => local.file.select(file.path, undefined)} />} + +
+ (inputRef = el)} + type="text" + value={inputValue()} + onInput={(e) => setInputValue(e.currentTarget.value)} + placeholder="It all starts with a prompt..." + class="w-full p-1 pb-4 text-text font-light placeholder-text-muted/70 text-sm focus:outline-none" + /> +
+ + {local.agent.current()?.name ?? "unknown"} /{" "} + {local.model.parsed().provider} / {local.model.parsed().model} + +
+ + + + + + +
+
+
+
+
+
+ ) +} + +const TabVisual = (props: { file: LocalFile }) => { + const local = useLocal() + return ( +
+ + + {props.file.name} + + + + + M + + + A + + + D + + + +
+ ) +} + +const SortableTab = (props: { + file: LocalFile + onTabClick: (file: LocalFile) => void + onTabClose: (file: LocalFile) => void +}) => { + const sortable = createSortable(props.file.path) + + return ( + // @ts-ignore +
+ +
+ props.onTabClick(props.file)}> + + + props.onTabClose(props.file)} + > + + +
+
+
+ ) +} + +const FileTag = (props: { file: LocalFile; default?: boolean; onClose: () => void }) => ( +
+ + }> + + + + + +
+ {props.file.name} + + + ({props.file.selection!.startLine}-{props.file.selection!.endLine}) + + +
+
+) + +const ConstrainDragYAxis = () => { + const context = useDragDropContext() + if (!context) return <> + const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context + const transformer: Transformer = { + id: "constrain-y-axis", + order: 100, + callback: (transform) => ({ ...transform, y: 0 }), + } + onDragStart((event: any) => { + addTransformer("draggables", event.draggable.id, transformer) + }) + onDragEnd((event: any) => { + removeTransformer("draggables", event.draggable.id, transformer.id) + }) + return <> +} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx new file mode 100644 index 000000000..d2e33a2ca --- /dev/null +++ b/packages/app/src/pages/layout.tsx @@ -0,0 +1,5 @@ +import { type ParentProps } from "solid-js" + +export default function Layout(props: ParentProps) { + return
{props.children}
+} -- cgit v1.2.3