From 3fa280d21878ae391674a21758199df3d2d8c3b5 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 3 Oct 2025 09:04:28 -0500 Subject: chore: app -> desktop --- packages/app/src/components/code.tsx | 846 ----------------------- packages/app/src/components/editor-pane.tsx | 381 ---------- packages/app/src/components/file-tree.tsx | 110 --- packages/app/src/components/markdown.tsx | 23 - packages/app/src/components/prompt-form.tsx | 295 -------- packages/app/src/components/resizeable-pane.tsx | 217 ------ packages/app/src/components/select-dialog.tsx | 225 ------ packages/app/src/components/select.tsx | 107 --- packages/app/src/components/session-list.tsx | 28 - packages/app/src/components/session-timeline.tsx | 459 ------------ packages/app/src/components/sidebar-nav.tsx | 48 -- 11 files changed, 2739 deletions(-) delete mode 100644 packages/app/src/components/code.tsx delete mode 100644 packages/app/src/components/editor-pane.tsx delete mode 100644 packages/app/src/components/file-tree.tsx delete mode 100644 packages/app/src/components/markdown.tsx delete mode 100644 packages/app/src/components/prompt-form.tsx delete mode 100644 packages/app/src/components/resizeable-pane.tsx delete mode 100644 packages/app/src/components/select-dialog.tsx delete mode 100644 packages/app/src/components/select.tsx delete mode 100644 packages/app/src/components/session-list.tsx delete mode 100644 packages/app/src/components/session-timeline.tsx delete mode 100644 packages/app/src/components/sidebar-nav.tsx (limited to 'packages/app/src/components') diff --git a/packages/app/src/components/code.tsx b/packages/app/src/components/code.tsx deleted file mode 100644 index 40a40aa9a..000000000 --- a/packages/app/src/components/code.tsx +++ /dev/null @@ -1,846 +0,0 @@ -import { bundledLanguages, type BundledLanguage, type ShikiTransformer } from "shiki" -import { splitProps, type ComponentProps, createEffect, onMount, onCleanup, createMemo, createResource } from "solid-js" -import { useLocal, useShiki } from "@/context" -import type { TextSelection } from "@/context/local" -import { getFileExtension, getNodeOffsetInLine, getSelectionInContainer } from "@/utils" - -type DefinedSelection = Exclude - -interface Props extends ComponentProps<"div"> { - code: string - path: string -} - -export function Code(props: Props) { - const ctx = useLocal() - const highlighter = useShiki() - const [local, others] = splitProps(props, ["class", "classList", "code", "path"]) - const lang = createMemo(() => { - const ext = getFileExtension(local.path) - if (ext in bundledLanguages) return ext - return "text" - }) - - let container: HTMLDivElement | undefined - let isProgrammaticSelection = false - - const ranges = createMemo(() => { - const items = ctx.context.all() as Array<{ type: "file"; path: string; selection?: DefinedSelection }> - const result: DefinedSelection[] = [] - for (const item of items) { - if (item.path !== local.path) continue - const selection = item.selection - if (!selection) continue - result.push(selection) - } - return result - }) - - const createLineNumberTransformer = (selections: DefinedSelection[]): ShikiTransformer => { - const highlighted = new Set() - for (const selection of selections) { - const startLine = selection.startLine - const endLine = selection.endLine - const start = Math.max(1, Math.min(startLine, endLine)) - const end = Math.max(start, Math.max(startLine, endLine)) - const count = end - start + 1 - if (count <= 0) continue - const values = Array.from({ length: count }, (_, index) => start + index) - for (const value of values) highlighted.add(value) - } - return { - name: "line-number-highlight", - line(node, index) { - if (!highlighted.has(index)) return - this.addClassToHast(node, "line-number-highlight") - const children = node.children - if (!Array.isArray(children)) return - for (const child of children) { - if (!child || typeof child !== "object") continue - const element = child as { type?: string; properties?: { className?: string[] } } - if (element.type !== "element") continue - const className = element.properties?.className - if (!Array.isArray(className)) continue - const matches = className.includes("diff-oldln") || className.includes("diff-newln") - if (!matches) continue - if (className.includes("line-number-highlight")) continue - className.push("line-number-highlight") - } - }, - } - } - - const [html] = createResource( - () => ranges(), - async (activeRanges) => { - if (!highlighter.getLoadedLanguages().includes(lang())) { - await highlighter.loadLanguage(lang() as BundledLanguage) - } - return highlighter.codeToHtml(local.code || "", { - lang: lang() && lang() in bundledLanguages ? lang() : "text", - theme: "opencode", - transformers: [transformerUnifiedDiff(), transformerDiffGroups(), createLineNumberTransformer(activeRanges)], - }) as string - }, - ) - - onMount(() => { - if (!container) return - - let ticking = false - const onScroll = () => { - if (!container) return - if (ctx.file.active()?.path !== local.path) return - if (ticking) return - ticking = true - requestAnimationFrame(() => { - ticking = false - ctx.file.scroll(local.path, container!.scrollTop) - }) - } - - const onSelectionChange = () => { - if (!container) return - if (isProgrammaticSelection) return - if (ctx.file.active()?.path !== local.path) return - const d = getSelectionInContainer(container) - if (!d) return - const p = ctx.file.node(local.path)?.selection - if (p && p.startLine === d.sl && p.endLine === d.el && p.startChar === d.sch && p.endChar === d.ech) return - ctx.file.select(local.path, { startLine: d.sl, startChar: d.sch, endLine: d.el, endChar: d.ech }) - } - - const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control" - const onKeyDown = (e: KeyboardEvent) => { - if (ctx.file.active()?.path !== local.path) return - const ae = document.activeElement as HTMLElement | undefined - const tag = (ae?.tagName || "").toLowerCase() - const inputFocused = !!ae && (tag === "input" || tag === "textarea" || ae.isContentEditable) - if (inputFocused) return - if (e.getModifierState(MOD) && e.key.toLowerCase() === "a") { - e.preventDefault() - if (!container) return - const element = container.querySelector("code") as HTMLElement | undefined - if (!element) return - const lines = Array.from(element.querySelectorAll(".line")) - if (!lines.length) return - const r = document.createRange() - const last = lines[lines.length - 1] - r.selectNodeContents(last) - const lastLen = r.toString().length - ctx.file.select(local.path, { startLine: 1, startChar: 0, endLine: lines.length, endChar: lastLen }) - } - } - - container.addEventListener("scroll", onScroll) - document.addEventListener("selectionchange", onSelectionChange) - document.addEventListener("keydown", onKeyDown) - - onCleanup(() => { - container?.removeEventListener("scroll", onScroll) - document.removeEventListener("selectionchange", onSelectionChange) - document.removeEventListener("keydown", onKeyDown) - }) - }) - - // Restore scroll position from store when content is ready - createEffect(() => { - const content = html() - if (!container || !content) return - const top = ctx.file.node(local.path)?.scrollTop - if (top !== undefined && container.scrollTop !== top) container.scrollTop = top - }) - - // Sync selection from store -> DOM - createEffect(() => { - const content = html() - if (!container || !content) return - if (ctx.file.active()?.path !== local.path) return - const codeEl = container.querySelector("code") as HTMLElement | undefined - if (!codeEl) return - const target = ctx.file.node(local.path)?.selection - const current = getSelectionInContainer(container) - const sel = window.getSelection() - if (!sel) return - if (!target) { - if (current) { - isProgrammaticSelection = true - sel.removeAllRanges() - queueMicrotask(() => { - isProgrammaticSelection = false - }) - } - return - } - const matches = !!( - current && - current.sl === target.startLine && - current.sch === target.startChar && - current.el === target.endLine && - current.ech === target.endChar - ) - if (matches) return - const lines = Array.from(codeEl.querySelectorAll(".line")) - if (lines.length === 0) return - let sIdx = Math.max(0, target.startLine - 1) - let eIdx = Math.max(0, target.endLine - 1) - let sChar = Math.max(0, target.startChar || 0) - let eChar = Math.max(0, target.endChar || 0) - if (sIdx > eIdx || (sIdx === eIdx && sChar > eChar)) { - const ti = sIdx - sIdx = eIdx - eIdx = ti - const tc = sChar - sChar = eChar - eChar = tc - } - if (eChar === 0 && eIdx > sIdx) { - eIdx = eIdx - 1 - eChar = Number.POSITIVE_INFINITY - } - if (sIdx >= lines.length) return - if (eIdx >= lines.length) eIdx = lines.length - 1 - const s = getNodeOffsetInLine(lines[sIdx], sChar) ?? { node: lines[sIdx], offset: 0 } - const e = getNodeOffsetInLine(lines[eIdx], eChar) ?? { node: lines[eIdx], offset: lines[eIdx].childNodes.length } - const range = document.createRange() - range.setStart(s.node, s.offset) - range.setEnd(e.node, e.offset) - isProgrammaticSelection = true - sel.removeAllRanges() - sel.addRange(range) - queueMicrotask(() => { - isProgrammaticSelection = false - }) - }) - - // Build/toggle split layout and apply folding (both unified and split) - createEffect(() => { - const content = html() - if (!container || !content) return - const view = ctx.file.view(local.path) - - const pres = Array.from(container.querySelectorAll("pre")) - if (pres.length === 0) return - const originalPre = pres[0] - - const split = container.querySelector(".diff-split") - if (view === "diff-split") { - applySplitDiff(container) - const next = container.querySelector(".diff-split") - if (next) next.style.display = "" - originalPre.style.display = "none" - } else { - if (split) split.style.display = "none" - originalPre.style.display = "" - } - - const expanded = ctx.file.folded(local.path) - if (view === "diff-split") { - const left = container.querySelector(".diff-split pre:nth-child(1) code") - const right = container.querySelector(".diff-split pre:nth-child(2) code") - if (left) - applyDiffFolding(left, 3, { expanded, onExpand: (key) => ctx.file.unfold(local.path, key), side: "left" }) - if (right) - applyDiffFolding(right, 3, { expanded, onExpand: (key) => ctx.file.unfold(local.path, key), side: "right" }) - } else { - const code = container.querySelector("pre code") - if (code) - applyDiffFolding(code, 3, { - expanded, - onExpand: (key) => ctx.file.unfold(local.path, key), - }) - } - }) - - // Highlight groups + scroll coupling - const clearHighlights = () => { - if (!container) return - container.querySelectorAll(".diff-selected").forEach((el) => el.classList.remove("diff-selected")) - } - - const applyHighlight = (idx: number, scroll?: boolean) => { - if (!container) return - const view = ctx.file.view(local.path) - if (view === "raw") return - - clearHighlights() - - const nodes: HTMLElement[] = [] - if (view === "diff-split") { - const left = container.querySelector(".diff-split pre:nth-child(1) code") - const right = container.querySelector(".diff-split pre:nth-child(2) code") - if (left) - nodes.push(...Array.from(left.querySelectorAll(`[data-chgrp="${idx}"][data-diff="remove"]`))) - if (right) - nodes.push(...Array.from(right.querySelectorAll(`[data-chgrp="${idx}"][data-diff="add"]`))) - } else { - const code = container.querySelector("pre code") - if (code) nodes.push(...Array.from(code.querySelectorAll(`[data-chgrp="${idx}"]`))) - } - - for (const n of nodes) n.classList.add("diff-selected") - if (scroll && nodes.length) nodes[0].scrollIntoView({ block: "center", behavior: "smooth" }) - } - - const countGroups = () => { - if (!container) return 0 - const code = container.querySelector("pre code") - if (!code) return 0 - const set = new Set() - for (const el of Array.from(code.querySelectorAll(".diff-line[data-chgrp]"))) { - const v = el.getAttribute("data-chgrp") - if (v != undefined) set.add(v) - } - return set.size - } - - let lastIdx: number | undefined = undefined - let lastView: string | undefined - let lastContent: string | undefined - let lastRawIdx: number | undefined = undefined - createEffect(() => { - const content = html() - if (!container || !content) return - const view = ctx.file.view(local.path) - const raw = ctx.file.changeIndex(local.path) - if (raw === undefined) return - const total = countGroups() - if (total <= 0) return - const next = ((raw % total) + total) % total - - const navigated = lastRawIdx !== undefined && lastRawIdx !== raw - - if (next !== raw) { - ctx.file.setChangeIndex(local.path, next) - applyHighlight(next, true) - } else { - if (lastView !== view || lastContent !== content) applyHighlight(next) - if ((lastIdx !== undefined && lastIdx !== next) || navigated) applyHighlight(next, true) - } - - lastRawIdx = raw - lastIdx = next - lastView = view - lastContent = content - }) - - return ( -
{ - container = el - }} - innerHTML={html()} - class=" - font-mono text-xs tracking-wide overflow-y-auto h-full - [&]:[counter-reset:line] - [&_pre]:focus-visible:outline-none - [&_pre]:overflow-x-auto [&_pre]:no-scrollbar - [&_code]:min-w-full [&_code]:inline-block - [&_.tab]:relative - [&_.tab::before]:content['⇥'] - [&_.tab::before]:absolute - [&_.tab::before]:opacity-0 - [&_.space]:relative - [&_.space::before]:content-['·'] - [&_.space::before]:absolute - [&_.space::before]:opacity-0 - [&_.line]:inline-block [&_.line]:w-full - [&_.line]:hover:bg-background-element - [&_.line::before]:sticky [&_.line::before]:left-0 - [&_.line::before]:w-12 [&_.line::before]:pr-4 - [&_.line::before]:z-10 - [&_.line::before]:bg-background-panel - [&_.line::before]:text-text-muted/60 - [&_.line::before]:text-right [&_.line::before]:inline-block - [&_.line::before]:select-none - [&_.line::before]:[counter-increment:line] - [&_.line::before]:content-[counter(line)] - [&_.line-number-highlight]:bg-accent/20 - [&_.line-number-highlight::before]:bg-accent/40! - [&_.line-number-highlight::before]:text-background-panel! - [&_code.code-diff_.line::before]:content-[''] - [&_code.code-diff_.line::before]:w-0 - [&_code.code-diff_.line::before]:pr-0 - [&_.diff-split_code.code-diff::before]:w-10 - [&_.diff-split_.diff-newln]:left-0 - [&_.diff-oldln]:sticky [&_.diff-oldln]:left-0 - [&_.diff-oldln]:w-10 [&_.diff-oldln]:pr-2 - [&_.diff-oldln]:z-40 - [&_.diff-oldln]:text-text-muted/60 - [&_.diff-oldln]:text-right [&_.diff-oldln]:inline-block - [&_.diff-oldln]:select-none - [&_.diff-oldln]:bg-background-panel - [&_.diff-newln]:sticky [&_.diff-newln]:left-10 - [&_.diff-newln]:w-10 [&_.diff-newln]:pr-2 - [&_.diff-newln]:z-40 - [&_.diff-newln]:text-text-muted/60 - [&_.diff-newln]:text-right [&_.diff-newln]:inline-block - [&_.diff-newln]:select-none - [&_.diff-newln]:bg-background-panel - [&_.diff-add]:bg-success/20! - [&_.diff-add.diff-selected]:bg-success/50! - [&_.diff-add_.diff-oldln]:bg-success! - [&_.diff-add_.diff-oldln]:text-background-panel! - [&_.diff-add_.diff-newln]:bg-success! - [&_.diff-add_.diff-newln]:text-background-panel! - [&_.diff-remove]:bg-error/20! - [&_.diff-remove.diff-selected]:bg-error/50! - [&_.diff-remove_.diff-newln]:bg-error! - [&_.diff-remove_.diff-newln]:text-background-panel! - [&_.diff-remove_.diff-oldln]:bg-error! - [&_.diff-remove_.diff-oldln]:text-background-panel! - [&_.diff-sign]:inline-block [&_.diff-sign]:px-2 [&_.diff-sign]:select-none - [&_.diff-blank]:bg-background-element - [&_.diff-blank_.diff-oldln]:bg-background-element - [&_.diff-blank_.diff-newln]:bg-background-element - [&_.diff-collapsed]:block! [&_.diff-collapsed]:w-full [&_.diff-collapsed]:relative - [&_.diff-collapsed]:cursor-pointer [&_.diff-collapsed]:select-none - [&_.diff-collapsed]:bg-info/20 [&_.diff-collapsed]:hover:bg-info/40! - [&_.diff-collapsed]:text-info/80 [&_.diff-collapsed]:hover:text-info - [&_.diff-collapsed]:text-xs - [&_.diff-collapsed_.diff-oldln]:bg-info! - [&_.diff-collapsed_.diff-newln]:bg-info! - " - classList={{ - ...(local.classList || {}), - [local.class ?? ""]: !!local.class, - }} - {...others} - >
- ) -} - -function transformerUnifiedDiff(): ShikiTransformer { - const kinds = new Map() - const meta = new Map() - let isDiff = false - - return { - name: "unified-diff", - preprocess(input) { - kinds.clear() - meta.clear() - isDiff = false - - const ls = input.split(/\r?\n/) - const out: Array = [] - let oldNo = 0 - let newNo = 0 - let inHunk = false - - for (let i = 0; i < ls.length; i++) { - const s = ls[i] - - const m = s.match(/^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@/) - if (m) { - isDiff = true - inHunk = true - oldNo = parseInt(m[1], 10) - newNo = parseInt(m[3], 10) - continue - } - - if ( - /^diff --git /.test(s) || - /^Index: /.test(s) || - /^--- /.test(s) || - /^\+\+\+ /.test(s) || - /^[=]{3,}$/.test(s) || - /^\*{3,}$/.test(s) || - /^\\ No newline at end of file$/.test(s) - ) { - isDiff = true - continue - } - - if (!inHunk) { - out.push(s) - continue - } - - if (/^\+/.test(s)) { - out.push(s) - const ln = out.length - kinds.set(ln, "add") - meta.set(ln, { new: newNo, sign: "+" }) - newNo++ - continue - } - - if (/^-/.test(s)) { - out.push(s) - const ln = out.length - kinds.set(ln, "remove") - meta.set(ln, { old: oldNo, sign: "-" }) - oldNo++ - continue - } - - if (/^ /.test(s)) { - out.push(s) - const ln = out.length - kinds.set(ln, "context") - meta.set(ln, { old: oldNo, new: newNo }) - oldNo++ - newNo++ - continue - } - - // fallback in hunks - out.push(s) - } - - return out.join("\n").trimEnd() - }, - code(node) { - if (isDiff) this.addClassToHast(node, "code-diff") - }, - pre(node) { - if (isDiff) this.addClassToHast(node, "code-diff") - }, - line(node, line) { - if (!isDiff) return - const kind = kinds.get(line) - if (!kind) return - - const m = meta.get(line) || {} - - this.addClassToHast(node, "diff-line") - this.addClassToHast(node, `diff-${kind}`) - node.properties = node.properties || {} - ;(node.properties as any)["data-diff"] = kind - if (m.old != undefined) (node.properties as any)["data-old"] = String(m.old) - if (m.new != undefined) (node.properties as any)["data-new"] = String(m.new) - - const oldSpan = { - type: "element", - tagName: "span", - properties: { className: ["diff-oldln"] }, - children: [{ type: "text", value: m.old != undefined ? String(m.old) : " " }], - } - const newSpan = { - type: "element", - tagName: "span", - properties: { className: ["diff-newln"] }, - children: [{ type: "text", value: m.new != undefined ? String(m.new) : " " }], - } - - if (kind === "add" || kind === "remove" || kind === "context") { - const first = (node.children && (node.children as any[])[0]) as any - if (first && first.type === "element" && first.children && first.children.length > 0) { - const t = first.children[0] - if (t && t.type === "text" && typeof t.value === "string" && t.value.length > 0) { - const ch = t.value[0] - if (ch === "+" || ch === "-" || ch === " ") t.value = t.value.slice(1) - } - } - } - - const signSpan = { - type: "element", - tagName: "span", - properties: { className: ["diff-sign"] }, - children: [{ type: "text", value: (m as any).sign || " " }], - } - - // @ts-expect-error hast typing across versions - node.children = [oldSpan, newSpan, signSpan, ...(node.children || [])] - }, - } -} - -function transformerDiffGroups(): ShikiTransformer { - let group = -1 - let inGroup = false - return { - name: "diff-groups", - pre() { - group = -1 - inGroup = false - }, - line(node) { - const props = (node.properties || {}) as any - const kind = props["data-diff"] as string | undefined - if (kind === "add" || kind === "remove") { - if (!inGroup) { - group += 1 - inGroup = true - } - ;(node.properties as any)["data-chgrp"] = String(group) - } else { - inGroup = false - } - }, - } -} - -function applyDiffFolding( - root: HTMLElement, - context = 3, - options?: { expanded?: string[]; onExpand?: (key: string) => void; side?: "left" | "right" }, -) { - if (!root.classList.contains("code-diff")) return - - // Cleanup: unwrap previous collapsed blocks and remove toggles - const blocks = Array.from(root.querySelectorAll(".diff-collapsed-block")) - for (const block of blocks) { - const p = block.parentNode - if (!p) { - block.remove() - continue - } - while (block.firstChild) p.insertBefore(block.firstChild, block) - block.remove() - } - const toggles = Array.from(root.querySelectorAll(".diff-collapsed")) - for (const t of toggles) t.remove() - - const lines = Array.from(root.querySelectorAll(".diff-line")) - if (lines.length === 0) return - - const n = lines.length - const isChange = lines.map((l) => l.dataset["diff"] === "add" || l.dataset["diff"] === "remove") - const isContext = lines.map((l) => l.dataset["diff"] === "context") - if (!isChange.some(Boolean)) return - - const visible = new Array(n).fill(false) as boolean[] - for (let i = 0; i < n; i++) if (isChange[i]) visible[i] = true - for (let i = 0; i < n; i++) { - if (isChange[i]) { - const s = Math.max(0, i - context) - const e = Math.min(n - 1, i + context) - for (let j = s; j <= e; j++) if (isContext[j]) visible[j] = true - } - } - - type Range = { start: number; end: number } - const ranges: Range[] = [] - let i = 0 - while (i < n) { - if (!visible[i] && isContext[i]) { - let j = i - while (j + 1 < n && !visible[j + 1] && isContext[j + 1]) j++ - ranges.push({ start: i, end: j }) - i = j + 1 - } else { - i++ - } - } - - for (const r of ranges) { - const start = lines[r.start] - const end = lines[r.end] - const count = r.end - r.start + 1 - const minCollapse = 20 - if (count < minCollapse) { - continue - } - - // Wrap the entire collapsed chunk (including trailing newline) so it takes no space - const block = document.createElement("span") - block.className = "diff-collapsed-block" - start.parentElement?.insertBefore(block, start) - - let cur: Node | undefined = start - while (cur) { - const next: Node | undefined = cur.nextSibling || undefined - block.appendChild(cur) - if (cur === end) { - // Also move the newline after the last line into the block - if (next && next.nodeType === Node.TEXT_NODE && (next.textContent || "").startsWith("\n")) { - block.appendChild(next) - } - break - } - cur = next - } - - block.style.display = "none" - const row = document.createElement("span") - row.className = "line diff-collapsed" - row.setAttribute("data-kind", "collapsed") - row.setAttribute("data-count", String(count)) - row.setAttribute("tabindex", "0") - row.setAttribute("role", "button") - - const oldln = document.createElement("span") - oldln.className = "diff-oldln" - oldln.textContent = " " - - const newln = document.createElement("span") - newln.className = "diff-newln" - newln.textContent = " " - - const sign = document.createElement("span") - sign.className = "diff-sign" - sign.textContent = "…" - - const label = document.createElement("span") - label.textContent = `show ${count} unchanged line${count > 1 ? "s" : ""}` - - const key = `o${start.dataset["old"] || ""}-${end.dataset["old"] || ""}:n${start.dataset["new"] || ""}-${end.dataset["new"] || ""}` - - const show = (record = true) => { - if (record) options?.onExpand?.(key) - const p = block.parentNode - if (p) { - while (block.firstChild) p.insertBefore(block.firstChild, block) - block.remove() - } - row.remove() - } - - row.addEventListener("click", () => show(true)) - row.addEventListener("keydown", (ev) => { - if (ev.key === "Enter" || ev.key === " ") { - ev.preventDefault() - show(true) - } - }) - - block.parentElement?.insertBefore(row, block) - if (!options?.side || options.side === "left") row.appendChild(oldln) - if (!options?.side || options.side === "right") row.appendChild(newln) - row.appendChild(sign) - row.appendChild(label) - - if (options?.expanded && options.expanded.includes(key)) { - show(false) - } - } -} - -function applySplitDiff(container: HTMLElement) { - const pres = Array.from(container.querySelectorAll("pre")) - if (pres.length === 0) return - const originalPre = pres[0] - const originalCode = originalPre.querySelector("code") as HTMLElement | undefined - if (!originalCode || !originalCode.classList.contains("code-diff")) return - - // Rebuild split each time to match current content - const existing = container.querySelector(".diff-split") - if (existing) existing.remove() - - const grid = document.createElement("div") - grid.className = "diff-split grid grid-cols-2 gap-x-6" - - const makeColumn = () => { - const pre = document.createElement("pre") - pre.className = originalPre.className - const code = document.createElement("code") - code.className = originalCode.className - pre.appendChild(code) - return { pre, code } - } - - const left = makeColumn() - const right = makeColumn() - - // Helpers - const cloneSide = (line: HTMLElement, side: "old" | "new"): HTMLElement => { - const clone = line.cloneNode(true) as HTMLElement - const oldln = clone.querySelector(".diff-oldln") - const newln = clone.querySelector(".diff-newln") - if (side === "old") { - if (newln) newln.remove() - } else { - if (oldln) oldln.remove() - } - return clone - } - - const blankLine = (side: "old" | "new", kind: "add" | "remove"): HTMLElement => { - const span = document.createElement("span") - span.className = "line diff-line diff-blank" - span.setAttribute("data-diff", kind) - const ln = document.createElement("span") - ln.className = side === "old" ? "diff-oldln" : "diff-newln" - ln.textContent = " " - span.appendChild(ln) - return span - } - - const lines = Array.from(originalCode.querySelectorAll(".diff-line")) - let i = 0 - while (i < lines.length) { - const cur = lines[i] - const kind = cur.dataset["diff"] - - if (kind === "context") { - left.code.appendChild(cloneSide(cur, "old")) - left.code.appendChild(document.createTextNode("\n")) - right.code.appendChild(cloneSide(cur, "new")) - right.code.appendChild(document.createTextNode("\n")) - i++ - continue - } - - if (kind === "remove") { - // Batch consecutive removes and following adds, then pair - const removes: HTMLElement[] = [] - const adds: HTMLElement[] = [] - let j = i - while (j < lines.length && lines[j].dataset["diff"] === "remove") { - removes.push(lines[j]) - j++ - } - let k = j - while (k < lines.length && lines[k].dataset["diff"] === "add") { - adds.push(lines[k]) - k++ - } - - const pairs = Math.min(removes.length, adds.length) - for (let p = 0; p < pairs; p++) { - left.code.appendChild(cloneSide(removes[p], "old")) - left.code.appendChild(document.createTextNode("\n")) - right.code.appendChild(cloneSide(adds[p], "new")) - right.code.appendChild(document.createTextNode("\n")) - } - for (let p = pairs; p < removes.length; p++) { - left.code.appendChild(cloneSide(removes[p], "old")) - left.code.appendChild(document.createTextNode("\n")) - right.code.appendChild(blankLine("new", "remove")) - right.code.appendChild(document.createTextNode("\n")) - } - for (let p = pairs; p < adds.length; p++) { - left.code.appendChild(blankLine("old", "add")) - left.code.appendChild(document.createTextNode("\n")) - right.code.appendChild(cloneSide(adds[p], "new")) - right.code.appendChild(document.createTextNode("\n")) - } - - i = k - continue - } - - if (kind === "add") { - // Run of adds not preceded by removes - const adds: HTMLElement[] = [] - let j = i - while (j < lines.length && lines[j].dataset["diff"] === "add") { - adds.push(lines[j]) - j++ - } - for (let p = 0; p < adds.length; p++) { - left.code.appendChild(blankLine("old", "add")) - left.code.appendChild(document.createTextNode("\n")) - right.code.appendChild(cloneSide(adds[p], "new")) - right.code.appendChild(document.createTextNode("\n")) - } - i = j - continue - } - - // Any other kind: mirror as context - left.code.appendChild(cloneSide(cur, "old")) - left.code.appendChild(document.createTextNode("\n")) - right.code.appendChild(cloneSide(cur, "new")) - right.code.appendChild(document.createTextNode("\n")) - i++ - } - - grid.appendChild(left.pre) - grid.appendChild(right.pre) - container.appendChild(grid) -} diff --git a/packages/app/src/components/editor-pane.tsx b/packages/app/src/components/editor-pane.tsx deleted file mode 100644 index faf70811d..000000000 --- a/packages/app/src/components/editor-pane.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import { For, Match, Show, Switch, createSignal, splitProps } from "solid-js" -import { Tabs } from "@/ui/tabs" -import { FileIcon, Icon, IconButton, Logo, Tooltip } from "@/ui" -import { - DragDropProvider, - DragDropSensors, - DragOverlay, - SortableProvider, - closestCenter, - createSortable, - useDragDropContext, -} from "@thisbeyond/solid-dnd" -import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd" -import type { LocalFile } from "@/context/local" -import { Code } from "@/components/code" -import PromptForm from "@/components/prompt-form" -import { useLocal, useSDK, useSync } from "@/context" -import { getFilename } from "@/utils" -import type { JSX } from "solid-js" - -interface EditorPaneProps { - layoutKey: string - timelinePane: string - onFileClick: (file: LocalFile) => void - onOpenModelSelect: () => void - onInputRefChange: (element: HTMLTextAreaElement | null) => void -} - -export default function EditorPane(props: EditorPaneProps): JSX.Element { - const [localProps] = splitProps(props, [ - "layoutKey", - "timelinePane", - "onFileClick", - "onOpenModelSelect", - "onInputRefChange", - ]) - const local = useLocal() - const sdk = useSDK() - const sync = useSync() - const [activeItem, setActiveItem] = createSignal(undefined) - - 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 handleTabChange = (path: string) => { - local.file.open(path) - } - - const handleTabClose = (file: LocalFile) => { - local.file.close(file.path) - } - - const handlePromptSubmit = async (prompt: string) => { - const existingSession = local.layout.visible(localProps.layoutKey, localProps.timelinePane) - ? local.session.active() - : undefined - let session = existingSession - if (!session) { - const created = await sdk.session.create() - session = created.data ?? undefined - } - if (!session) return - local.session.setActive(session.id) - local.layout.show(localProps.layoutKey, localProps.timelinePane) - - await sdk.session.prompt({ - path: { id: session.id }, - body: { - agent: local.agent.current()!.name, - model: { - modelID: local.model.current()!.id, - providerID: local.model.current()!.provider.id, - }, - parts: [ - { - type: "text", - text: prompt, - }, - ...(local.context.active() - ? [ - { - type: "file" as const, - mime: "text/plain", - url: `file://${local.context.active()!.absolute}`, - filename: local.context.active()!.name, - source: { - type: "file" as const, - text: { - value: "@" + local.context.active()!.name, - start: 0, - end: 0, - }, - path: local.context.active()!.absolute, - }, - }, - ] - : []), - ...local.context.all().flatMap((file) => [ - { - type: "file" as const, - mime: "text/plain", - url: `file://${sync.absolute(file.path)}${file.selection ? `?start=${file.selection.startLine}&end=${file.selection.endLine}` : ""}`, - filename: getFilename(file.path), - source: { - type: "file" as const, - text: { - value: "@" + getFilename(file.path), - start: 0, - end: 0, - }, - path: sync.absolute(file.path), - }, - }, - ]), - ], - }, - }) - } - - const handleDragStart = (event: unknown) => { - const id = getDraggableId(event) - if (!id) return - setActiveItem(id) - } - - const handleDragOver = (event: DragEvent) => { - const { draggable, droppable } = event - if (draggable && droppable) { - const currentFiles = local.file.opened().map((file) => file.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 handleDragEnd = () => { - setActiveItem(undefined) - } - - return ( -
- - - - - -
- - file.path)}> - - {(file) => ( - - )} - - - -
- - {(() => { - const activeFile = local.file.active()! - const view = local.file.view(activeFile.path) - return ( -
- -
- - navigateChange(-1)}> - - - - - navigateChange(1)}> - - - -
-
- - local.file.setView(activeFile.path, "raw")} - > - - - - - local.file.setView(activeFile.path, "diff-unified")} - > - - - - - local.file.setView(activeFile.path, "diff-split")} - > - - - -
- ) - })()} -
- - local.layout.toggle(localProps.layoutKey, localProps.timelinePane)} - > - - - -
-
- - {(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 - })()} - - )} - -
- - {(() => { - const id = activeItem() - if (!id) return null - const draggedFile = local.file.node(id) - if (!draggedFile) return null - return ( -
- -
- ) - })()} -
-
- localProps.onInputRefChange(element ?? null)} - /> -
- ) -} - -function TabVisual(props: { file: LocalFile }): JSX.Element { - return ( -
- - - {props.file.name} - - - - - M - - - A - - - D - - - -
- ) -} - -function SortableTab(props: { - file: LocalFile - onTabClick: (file: LocalFile) => void - onTabClose: (file: LocalFile) => void -}): JSX.Element { - const sortable = createSortable(props.file.path) - - return ( - // @ts-ignore -
- -
- props.onTabClick(props.file)}> - - - props.onTabClose(props.file)} - > - - -
-
-
- ) -} - -function ConstrainDragYAxis(): JSX.Element { - 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) => { - const id = getDraggableId(event) - if (!id) return - addTransformer("draggables", id, transformer) - }) - onDragEnd((event) => { - const id = getDraggableId(event) - if (!id) return - removeTransformer("draggables", id, transformer.id) - }) - return <> -} - -const getDraggableId = (event: unknown): string | undefined => { - if (typeof event !== "object" || event === null) return undefined - if (!("draggable" in event)) return undefined - const draggable = (event as { draggable?: { id?: unknown } }).draggable - if (!draggable) return undefined - return typeof draggable.id === "string" ? draggable.id : undefined -} diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx deleted file mode 100644 index d31255ced..000000000 --- a/packages/app/src/components/file-tree.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useLocal } from "@/context" -import type { LocalFile } from "@/context/local" -import { Collapsible, FileIcon, Tooltip } from "@/ui" -import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js" -import { Dynamic } from "solid-js/web" - -export default function FileTree(props: { - path: string - class?: string - nodeClass?: string - level?: number - onFileClick?: (file: LocalFile) => void -}) { - const local = useLocal() - const level = props.level ?? 0 - - const Node = (p: ParentProps & ComponentProps<"div"> & { node: LocalFile; as?: "div" | "button" }) => ( - { - const evt = e as globalThis.DragEvent - evt.dataTransfer!.effectAllowed = "copy" - evt.dataTransfer!.setData("text/plain", `file:${p.node.path}`) - - // Create custom drag image without margins - const dragImage = document.createElement("div") - dragImage.className = - "flex items-center gap-x-2 px-2 py-1 bg-background-element rounded-md border border-border-1" - dragImage.style.position = "absolute" - dragImage.style.top = "-1000px" - - // Copy only the icon and text content without padding - const icon = e.currentTarget.querySelector("svg") - const text = e.currentTarget.querySelector("span") - if (icon && text) { - dragImage.innerHTML = icon.outerHTML + text.outerHTML - } - - document.body.appendChild(dragImage) - evt.dataTransfer!.setDragImage(dragImage, 0, 12) - setTimeout(() => document.body.removeChild(dragImage), 0) - }} - {...p} - > - {p.children} - - {p.node.name} - - - - - - ) - - return ( -
- - {(node) => ( - - - - (open ? local.file.expand(node.path) : local.file.collapse(node.path))} - > - - - - - - - - - - - - - props.onFileClick?.(node)}> -
- - - - - - )} - -
- ) -} diff --git a/packages/app/src/components/markdown.tsx b/packages/app/src/components/markdown.tsx deleted file mode 100644 index a60fad149..000000000 --- a/packages/app/src/components/markdown.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useMarked } from "@/context" -import { createResource } from "solid-js" - -function strip(text: string): string { - const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/ - const match = text.match(wrappedRe) - return match ? match[2] : text -} -export function Markdown(props: { text: string; class?: string }) { - const marked = useMarked() - const [html] = createResource( - () => strip(props.text), - async (markdown) => { - return marked.parse(markdown) - }, - ) - return ( -
- ) -} diff --git a/packages/app/src/components/prompt-form.tsx b/packages/app/src/components/prompt-form.tsx deleted file mode 100644 index 9d7c45a32..000000000 --- a/packages/app/src/components/prompt-form.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js" -import { Button, FileIcon, Icon, IconButton, Tooltip } from "@/ui" -import { Select } from "@/components/select" -import { useLocal } from "@/context" -import type { FileContext, LocalFile } from "@/context/local" -import { getFilename } from "@/utils" -import { createSpeechRecognition } from "@/utils/speech" - -interface PromptFormProps { - class?: string - classList?: Record - onSubmit: (prompt: string) => Promise | void - onOpenModelSelect: () => void - onInputRefChange?: (element: HTMLTextAreaElement | undefined) => void -} - -export default function PromptForm(props: PromptFormProps) { - const local = useLocal() - - const [prompt, setPrompt] = createSignal("") - const [isDragOver, setIsDragOver] = createSignal(false) - - const placeholderText = "Start typing or speaking..." - - const { - isSupported, - isRecording, - interim: interimTranscript, - start: startSpeech, - stop: stopSpeech, - } = createSpeechRecognition({ - onFinal: (text) => setPrompt((prev) => (prev && !prev.endsWith(" ") ? prev + " " : prev) + text), - }) - - let inputRef: HTMLTextAreaElement | undefined = undefined - let overlayContainerRef: HTMLDivElement | undefined = undefined - let shouldAutoScroll = true - - const promptContent = createMemo(() => { - const base = prompt() || "" - const interim = isRecording() ? interimTranscript() : "" - if (!base && !interim) { - return {placeholderText} - } - const needsSpace = base && interim && !base.endsWith(" ") && !interim.startsWith(" ") - return ( - <> - {base} - {interim && ( - - {needsSpace ? " " : ""} - {interim} - - )} - - ) - }) - - createEffect(() => { - prompt() - interimTranscript() - queueMicrotask(() => { - if (!inputRef) return - if (!overlayContainerRef) return - if (!shouldAutoScroll) { - overlayContainerRef.scrollTop = inputRef.scrollTop - return - } - scrollPromptToEnd() - }) - }) - - const handlePromptKeyDown = (event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) => { - if (event.isComposing) return - if (event.key === "Enter" && !event.shiftKey) { - event.preventDefault() - inputRef?.form?.requestSubmit() - } - } - - const handlePromptScroll = (event: Event & { currentTarget: HTMLTextAreaElement }) => { - const target = event.currentTarget - shouldAutoScroll = target.scrollTop + target.clientHeight >= target.scrollHeight - 4 - if (overlayContainerRef) overlayContainerRef.scrollTop = target.scrollTop - } - - const scrollPromptToEnd = () => { - if (!inputRef) return - const maxInputScroll = inputRef.scrollHeight - inputRef.clientHeight - const next = maxInputScroll > 0 ? maxInputScroll : 0 - inputRef.scrollTop = next - if (overlayContainerRef) overlayContainerRef.scrollTop = next - shouldAutoScroll = true - } - - const handleSubmit = async (event: SubmitEvent) => { - event.preventDefault() - const currentPrompt = prompt() - setPrompt("") - shouldAutoScroll = true - if (overlayContainerRef) overlayContainerRef.scrollTop = 0 - if (inputRef) { - inputRef.scrollTop = 0 - inputRef.blur() - } - - await props.onSubmit(currentPrompt) - } - - onCleanup(() => { - props.onInputRefChange?.(undefined) - }) - - return ( -
-
{ - const evt = event as unknown as globalThis.DragEvent - if (evt.dataTransfer?.types.includes("text/plain")) { - evt.preventDefault() - setIsDragOver(true) - } - }} - onDragLeave={(event) => { - if (event.currentTarget === event.target) { - setIsDragOver(false) - } - }} - onDragOver={(event) => { - const evt = event as unknown as globalThis.DragEvent - if (evt.dataTransfer?.types.includes("text/plain")) { - evt.preventDefault() - evt.dataTransfer.dropEffect = "copy" - } - }} - onDrop={(event) => { - const evt = event as unknown as globalThis.DragEvent - evt.preventDefault() - setIsDragOver(false) - - const data = evt.dataTransfer?.getData("text/plain") - if (data && data.startsWith("file:")) { - const filePath = data.slice(5) - const fileNode = local.file.node(filePath) - if (fileNode) { - local.context.add({ - type: "file", - path: filePath, - }) - } - } - }} - > - 0 || local.context.active()}> -
- - local.context.removeActive()} /> - - - {(file) => local.context.remove(file.key)} />} - -
-
-
- -
{ - overlayContainerRef = element ?? undefined - }} - class="pointer-events-none absolute inset-0 overflow-hidden" - > -
- {promptContent()} -
-
-
-
-
- handleInput(e.currentTarget.value)} - onKeyDown={handleKey} - placeholder={props.placeholder} - class="w-full pl-10 pr-4 py-2 rounded-t-md - text-sm text-text placeholder-text-muted/70 - focus:outline-none" - autofocus - spellcheck={false} - autocorrect="off" - autocomplete="off" - autocapitalize="off" - /> -
- {/* -
- -
-
*/} - - { - setStore("filter", "") - resetSelection() - }} - > - - - -
-
-
-
(scrollRef = el)} class="relative flex-1 overflow-y-auto"> - 0} - fallback={
No results
} - > - - {(group) => ( - <> - -
- {group.category} -
-
-
- - {(item) => ( - - )} - -
- - )} -
-
-
-
-
- - - ↑↓ - - Navigate - - - - ↵ - - Select - - - - ESC - - Close - -
- {`${flat().length} results`} -
- - - - ) -} diff --git a/packages/app/src/components/select.tsx b/packages/app/src/components/select.tsx deleted file mode 100644 index 3df8c9999..000000000 --- a/packages/app/src/components/select.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Select as KobalteSelect } from "@kobalte/core/select" -import { createMemo } from "solid-js" -import type { ComponentProps } from "solid-js" -import { Icon } from "@/ui/icon" -import { pipe, groupBy, entries, map } from "remeda" -import { Button, type ButtonProps } from "@/ui" - -export interface SelectProps { - placeholder?: string - options: T[] - current?: T - value?: (x: T) => string - label?: (x: T) => string - groupBy?: (x: T) => string - onSelect?: (value: T | undefined) => void - class?: ComponentProps<"div">["class"] - classList?: ComponentProps<"div">["classList"] -} - -export function Select(props: SelectProps & ButtonProps) { - const grouped = createMemo(() => { - const result = pipe( - props.options, - groupBy((x) => (props.groupBy ? props.groupBy(x) : "")), - // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))), - entries(), - map(([k, v]) => ({ category: k, options: v })), - ) - return result - }) - - return ( - - value={props.current} - options={grouped()} - optionValue={(x) => (props.value ? props.value(x) : (x as string))} - optionTextValue={(x) => (props.label ? props.label(x) : (x as string))} - optionGroupChildren="options" - placeholder={props.placeholder} - sectionComponent={(props) => ( - - {props.section.rawValue.category} - - )} - itemComponent={(itemProps) => ( - - - {props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)} - - - - - - )} - onChange={(v) => { - props.onSelect?.(v ?? undefined) - }} - > - - > - {(state) => { - const selected = state.selectedOption() ?? props.current - if (!selected) return props.placeholder || "" - if (props.label) return props.label(selected) - return selected as string - }} - - - - - - - - - - - - ) -} diff --git a/packages/app/src/components/session-list.tsx b/packages/app/src/components/session-list.tsx deleted file mode 100644 index e0819780d..000000000 --- a/packages/app/src/components/session-list.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useSync, useLocal } from "@/context" -import { Button, Tooltip } from "@/ui" -import { VList } from "virtua/solid" - -export default function SessionList() { - const sync = useSync() - const local = useLocal() - - return ( - - {(session) => ( - - - - )} - - ) -} diff --git a/packages/app/src/components/session-timeline.tsx b/packages/app/src/components/session-timeline.tsx deleted file mode 100644 index 07d93031e..000000000 --- a/packages/app/src/components/session-timeline.tsx +++ /dev/null @@ -1,459 +0,0 @@ -import { useLocal, useSync } from "@/context" -import { Collapsible, Icon } from "@/ui" -import type { Part, ToolPart } from "@opencode-ai/sdk" -import { DateTime } from "luxon" -import { - createSignal, - onMount, - For, - Match, - splitProps, - Switch, - type ComponentProps, - type ParentProps, - createEffect, - createMemo, - Show, -} from "solid-js" -import { getFilename } from "@/utils" -import { Markdown } from "./markdown" -import { Code } from "./code" -import { createElementSize } from "@solid-primitives/resize-observer" -import { createScrollPosition } from "@solid-primitives/scroll" - -function Part(props: ParentProps & ComponentProps<"div">) { - const [local, others] = splitProps(props, ["class", "classList", "children"]) - return ( -
-

{local.children}

-
- ) -} - -function CollapsiblePart(props: { title: ParentProps["children"] } & ParentProps & ComponentProps) { - return ( - - - {props.title} - - -

- {props.children} -

-
-
- ) -} - -function ReadToolPart(props: { part: ToolPart }) { - const sync = useSync() - const local = useLocal() - return ( - - - Reading file... - - - {(state) => { - const path = state().input["filePath"] as string - return ( - local.file.open(path)}> - Read {getFilename(path)} - - ) - }} - - - {(state) => ( -
- - Read {getFilename(state().input["filePath"] as string)} - -
{sync.sanitize(state().error)}
-
- )} -
-
- ) -} - -function EditToolPart(props: { part: ToolPart }) { - const sync = useSync() - return ( - - - Preparing edit... - - - {(state) => ( - - Edit {getFilename(state().input["filePath"] as string)} - - } - > - - - )} - - - {(state) => ( - - Edit {getFilename(state().input["filePath"] as string)} - - } - > -
{sync.sanitize(state().error)}
-
- )} -
-
- ) -} - -function WriteToolPart(props: { part: ToolPart }) { - const sync = useSync() - return ( - - - Preparing write... - - - {(state) => ( - - Write {getFilename(state().input["filePath"] as string)} - - } - > -
-
- )} -
- - {(state) => ( -
- - Write {getFilename(state().input["filePath"] as string)} - -
{sync.sanitize(state().error)}
-
- )} -
-
- ) -} - -function BashToolPart(props: { part: ToolPart }) { - const sync = useSync() - return ( - - - Writing shell command... - - - {(state) => ( - - Run command: {state().input["command"]} - - } - > - - - )} - - - {(state) => ( - - Shell {state().input["command"]} - - } - > -
{sync.sanitize(state().error)}
-
- )} -
-
- ) -} - -function ToolPart(props: { part: ToolPart }) { - // read - // edit - // write - // bash - // ls - // glob - // grep - // todowrite - // todoread - // webfetch - // websearch - // patch - // task - return ( -
- - {props.part.type}:{props.part.tool} - - } - > - - - - - - - - - - - - - -
- ) -} - -export default function SessionTimeline(props: { session: string; class?: string }) { - const sync = useSync() - const [scrollElement, setScrollElement] = createSignal(undefined) - const [root, setRoot] = createSignal(undefined) - const [tail, setTail] = createSignal(true) - const size = createElementSize(root) - const scroll = createScrollPosition(scrollElement) - - onMount(() => sync.session.sync(props.session)) - const session = createMemo(() => sync.session.get(props.session)) - const messages = createMemo(() => sync.data.message[props.session] ?? []) - const working = createMemo(() => { - const last = messages()[messages().length - 1] - if (!last) return false - if (last.role === "user") return true - return !last.time.completed - }) - - const getScrollParent = (el: HTMLElement | null): HTMLElement | undefined => { - let p = el?.parentElement - while (p && p !== document.body) { - const s = getComputedStyle(p) - if (s.overflowY === "auto" || s.overflowY === "scroll") return p - p = p.parentElement - } - return undefined - } - - createEffect(() => { - if (!root()) return - setScrollElement(getScrollParent(root()!)) - }) - - const scrollToBottom = () => { - const element = scrollElement() - if (!element) return - element.scrollTop = element.scrollHeight - } - - createEffect(() => { - size.height - if (tail()) scrollToBottom() - }) - - createEffect(() => { - if (working()) { - setTail(true) - scrollToBottom() - } - }) - - let lastScrollY = 0 - createEffect(() => { - if (scroll.y < lastScrollY) { - setTail(false) - } - lastScrollY = scroll.y - }) - - const valid = (part: Part) => { - if (!part) return false - switch (part.type) { - case "step-start": - case "step-finish": - case "file": - case "patch": - return false - case "text": - return !part.synthetic - case "reasoning": - return part.text.trim() - default: - return true - } - } - - const duration = (part: Part) => { - switch (part.type) { - default: - if ( - "time" in part && - part.time && - "start" in part.time && - part.time.start && - "end" in part.time && - part.time.end - ) { - const start = DateTime.fromMillis(part.time.start) - const end = DateTime.fromMillis(part.time.end) - return end.diff(start).toFormat("s") - } - return "" - } - } - - return ( -
-
    - - {(message) => ( - - {(part) => ( -
  • - {part.type}
}> - - {(part) => ( - - -
-

- {part().text} -

-

- {DateTime.fromMillis(message.time.created).toRelative()} ·{" "} - {sync.data.config.username ?? "user"} -

-
-
- - - -
- )} -
- - {(part) => ( - Thinking}> - - Thought for {duration(part())}s - - - } - > - - - )} - - {(part) => } - - - )} - - )} - - - - - -
- - Raw Session Data - -
-
- -
    -
  • - - -
    - - session - -
    -
    - - - -
    -
  • - - {(message) => ( - <> -
  • - - -
    - - {message.role === "user" ? "user" : "assistant"} - -
    -
    - - - -
    -
  • - - {(part) => ( -
  • - - -
    - - {part.type} - -
    -
    - - - -
    -
  • - )} -
    - - )} -
    -
-
-
-
-
- ) -} diff --git a/packages/app/src/components/sidebar-nav.tsx b/packages/app/src/components/sidebar-nav.tsx deleted file mode 100644 index 24750bdba..000000000 --- a/packages/app/src/components/sidebar-nav.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { For } from "solid-js" -import { Icon, Link, Logo, Tooltip } from "@/ui" -import { useLocation } from "@solidjs/router" - -const navigation = [ - { name: "Sessions", href: "/sessions", icon: "dashboard" as const }, - { name: "Commands", href: "/commands", icon: "slash" as const }, - { name: "Agents", href: "/agents", icon: "bolt" as const }, - { name: "Providers", href: "/providers", icon: "cloud" as const }, - { name: "Tools (MCP)", href: "/tools", icon: "hammer" as const }, - { name: "LSP", href: "/lsp", icon: "code" as const }, - { name: "Settings", href: "/settings", icon: "settings" as const }, -] - -export default function SidebarNav() { - const location = useLocation() - return ( - - ) -} -- cgit v1.2.3