From fe8f6d7a3eef34e932bd43d244460d417865de88 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 24 Oct 2025 08:26:17 -0500 Subject: [PATCH] wip: desktop work --- packages/desktop/src/components/diff.tsx | 140 +++++++++++++++++++ packages/desktop/src/context/local.tsx | 6 - packages/desktop/src/pages/index.tsx | 155 ++++++++++++--------- packages/opencode/test/fixture/fixture.ts | 3 +- packages/ui/src/components/collapsible.css | 46 ++++++ packages/ui/src/components/collapsible.tsx | 35 +++++ packages/ui/src/components/index.ts | 1 + packages/ui/src/demo.tsx | 36 ++++- 8 files changed, 350 insertions(+), 72 deletions(-) create mode 100644 packages/desktop/src/components/diff.tsx create mode 100644 packages/ui/src/components/collapsible.css create mode 100644 packages/ui/src/components/collapsible.tsx diff --git a/packages/desktop/src/components/diff.tsx b/packages/desktop/src/components/diff.tsx new file mode 100644 index 00000000..4667bbb3 --- /dev/null +++ b/packages/desktop/src/components/diff.tsx @@ -0,0 +1,140 @@ +import { type FileContents, FileDiff, type DiffLineAnnotation } from "@pierre/precision-diffs" + +export interface DiffProps { + before: FileContents + after: FileContents +} + +export function Diff(props: DiffProps) { + let container!: HTMLDivElement + + console.log(props) + + interface ThreadMetadata { + threadId: string + } + + const lineAnnotations: DiffLineAnnotation[] = [ + { + side: "additions", + // The line number specified for an annotation is the visual line number + // you see in the number column of a diff + lineNumber: 16, + metadata: { threadId: "68b329da9893e34099c7d8ad5cb9c940" }, + }, + ] + + const instance = new FileDiff({ + // You can provide a 'theme' prop that maps to any + // built in shiki theme or you can register a custom + // theme. We also include 2 custom themes + // + // 'pierre-night' and 'pierre-light + // + // For the rest of the available shiki themes, check out: + // https://shiki.style/themes + theme: "none", + // Or can also provide a 'themes' prop, which allows the code to adapt + // to your OS light or dark theme + // themes: { dark: 'pierre-night', light: 'pierre-light' }, + // When using the 'themes' prop, 'themeType' allows you to force 'dark' + // or 'light' theme, or inherit from the OS ('system') theme. + themeType: "system", + // Disable the line numbers for your diffs, generally not recommended + disableLineNumbers: false, + // Whether code should 'wrap' with long lines or 'scroll'. + overflow: "scroll", + // Normally you shouldn't need this prop, but if you don't provide a + // valid filename or your file doesn't have an extension you may want to + // override the automatic detection. You can specify that language here: + // https://shiki.style/languages + // lang?: SupportedLanguages; + // 'diffStyle' controls whether the diff is presented side by side or + // in a unified (single column) view + diffStyle: "split", + // Line decorators to help highlight changes. + // 'bars' (default): + // Shows some red-ish or green-ish (theme dependent) bars on the left + // edge of relevant lines + // + // 'classic': + // shows '+' characters on additions and '-' characters on deletions + // + // 'none': + // No special diff indicators are shown + diffIndicators: "bars", + // By default green-ish or red-ish background are shown on added and + // deleted lines respectively. Disable that feature here + disableBackground: false, + // Diffs are split up into hunks, this setting customizes what to show + // between each hunk. + // + // 'line-info' (default): + // Shows a bar that tells you how many lines are collapsed. If you are + // using the oldFile/newFile API then you can click those bars to + // expand the content between them + // + // 'metadata': + // Shows the content you'd see in a normal patch file, usually in some + // format like '@@ -60,6 +60,22 @@'. You cannot use these to expand + // hidden content + // + // 'simple': + // Just a subtle bar separator between each hunk + hunkSeparators: "line-info", + // On lines that have both additions and deletions, we can run a + // separate diff check to mark parts of the lines that change. + // 'none': + // Do not show these secondary highlights + // + // 'char': + // Show changes at a per character granularity + // + // 'word': + // Show changes but rounded up to word boundaries + // + // 'word-alt' (default): + // Similar to 'word', however we attempt to minimize single character + // gaps between highlighted changes + lineDiffType: "word-alt", + // If lines exceed these character lengths then we won't perform the + // line lineDiffType check + maxLineDiffLength: 1000, + // If any line in the diff exceeds this value then we won't attempt to + // syntax highlight the diff + maxLineLengthForHighlighting: 1000, + // Enabling this property will hide the file header with file name and + // diff stats. + disableFileHeader: false, + // You can optionally pass a render function for rendering out line + // annotations. Just return the dom node to render + renderAnnotation(annotation: DiffLineAnnotation): HTMLElement { + // Despite the diff itself being rendered in the shadow dom, + // annotations are inserted via the web components 'slots' api and you + // can use all your normal normal css and styling for them + const element = document.createElement("div") + element.innerText = annotation.metadata.threadId + return element + }, + }) + + // If you ever want to update the options for an instance, simple call + // 'setOptions' with the new options. Bear in mind, this does NOT merge + // existing properties, it's a full replace + instance.setOptions({ + ...instance.options, + theme: "pierre-dark", + themes: undefined, + }) + + // When ready to render, simply call .render with old/new file, optional + // annotations and a container element to hold the diff + instance.render({ + oldFile: props.before, + newFile: props.after, + lineAnnotations, + containerWrapper: container, + }) + + return
+} diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 981039bb..6ed8ec17 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -467,11 +467,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ ) }) - const activeAssistantMessagesWithText = createMemo(() => { - if (!store.active || !activeAssistantMessages()) return [] - return activeAssistantMessages()?.filter((m) => sync.data.part[m.id].find((p) => p.type === "text")) - }) - const model = createMemo(() => { if (!last()) return const model = sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID] @@ -510,7 +505,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ active, activeMessage, activeAssistantMessages, - activeAssistantMessagesWithText, lastUserMessage, cost, last, diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx index 1542853e..f6ea4cb9 100644 --- a/packages/desktop/src/pages/index.tsx +++ b/packages/desktop/src/pages/index.tsx @@ -1,7 +1,7 @@ import { Button, List, SelectDialog, Tooltip, IconButton, Tabs, Icon } from "@opencode-ai/ui" import { FileIcon } from "@/ui" import FileTree from "@/components/file-tree" -import { For, onCleanup, onMount, Show, Match, Switch, createSignal, createEffect } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createSignal, createEffect, createMemo } from "solid-js" import { useLocal, type LocalFile, type TextSelection } from "@/context/local" import { createStore } from "solid-js/store" import { getDirectory, getFilename } from "@/utils" @@ -21,6 +21,7 @@ import type { JSX } from "solid-js" import { Code } from "@/components/code" import { useSync } from "@/context/sync" import { useSDK } from "@/context/sdk" +import { Diff } from "@/components/diff" export default function Page() { const local = useLocal() @@ -374,27 +375,36 @@ export default function Page() { onSelect={(s) => local.session.setActive(s?.id)} onHover={(s) => (!!s ? sync.session.sync(s?.id) : undefined)} > - {(session) => ( - -
-
- - {session.title} - - - {DateTime.fromMillis(session.time.updated).toRelative()} - -
-
- 2 files changed -
- +43 - -2 + {(session) => { + const diffs = createMemo(() => session.summary?.diffs ?? []) + const filesChanged = createMemo(() => diffs().length) + const additions = createMemo(() => diffs().reduce((acc, diff) => (acc ?? 0) + (diff.additions ?? 0), 0)) + const deletions = createMemo(() => diffs().reduce((acc, diff) => (acc ?? 0) + (diff.deletions ?? 0), 0)) + + return ( + +
+
+ + {session.title} + + + {DateTime.fromMillis(session.time.updated).toRelative()} + +
+
+ {`${filesChanged() || "No"} file${filesChanged() !== 1 ? "s" : ""} changed`} + +
+ {`+${additions()}`} + {`-${deletions()}`} +
+
-
- - )} + + ) + }}
@@ -521,60 +531,77 @@ export default function Page() { {(activeSession) => (
-
    - - {(message) => ( -
  • local.session.setActiveMessage(message.id)} - > -
    - - - - - - - - - -
    -
    1}> +
      + + {(message) => ( +
    • local.session.setActiveMessage(message.id)} > - {local.session.getMessageText(message)} -
    -
  • - )} -
    -
+
+ + + + + + + + + +
+
+ {local.session.getMessageText(message)} +
+ + )} + + +
- {(message) => ( -
-
-
- {local.session.getMessageText(message)} + {(message) => { + console.log(message) + return ( +
+
+
+ {local.session.getMessageText(message)} +
+
{message.summary?.text}
-
- {message.summary?.text || - local.session.getMessageText(local.session.activeAssistantMessagesWithText())} +
+ + {(diff) => ( + + )} +
-
-
- )} + ) + }}
diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index 0b83bb31..0d3e0c91 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/packages/opencode/test/fixture/fixture.ts @@ -1,4 +1,5 @@ import { $ } from "bun" +import { realpathSync } from "fs" import os from "os" import path from "path" @@ -17,7 +18,7 @@ export async function tmpdir(options?: TmpDirOptions) { await options?.dispose?.(dirpath) await $`rm -rf ${dirpath}`.quiet() }, - path: dirpath, + path: realpathSync(dirpath), extra: extra as T, } return result diff --git a/packages/ui/src/components/collapsible.css b/packages/ui/src/components/collapsible.css new file mode 100644 index 00000000..441d0083 --- /dev/null +++ b/packages/ui/src/components/collapsible.css @@ -0,0 +1,46 @@ +[data-component="collapsible"] { + display: flex; + flex-direction: column; + + [data-slot="trigger"] { + cursor: pointer; + user-select: none; + + &:focus-visible { + outline: 2px solid var(--border-focus); + outline-offset: 2px; + } + + &[data-disabled] { + cursor: not-allowed; + opacity: 0.5; + } + } + + [data-slot="content"] { + overflow: hidden; + /* animation: slideUp 250ms ease-out; */ + + /* &[data-expanded] { */ + /* animation: slideDown 250ms ease-out; */ + /* } */ + } +} + +@keyframes slideDown { + from { + height: 0; + } + to { + height: var(--kb-collapsible-content-height); + } +} + +@keyframes slideUp { + from { + height: var(--kb-collapsible-content-height); + } + to { + height: 0; + } +} diff --git a/packages/ui/src/components/collapsible.tsx b/packages/ui/src/components/collapsible.tsx new file mode 100644 index 00000000..011d103c --- /dev/null +++ b/packages/ui/src/components/collapsible.tsx @@ -0,0 +1,35 @@ +import { Collapsible as Kobalte, CollapsibleRootProps } from "@kobalte/core/collapsible" +import { ComponentProps, ParentProps, splitProps } from "solid-js" + +export interface CollapsibleProps extends ParentProps { + class?: string + classList?: ComponentProps<"div">["classList"] +} + +function CollapsibleRoot(props: CollapsibleProps) { + const [local, others] = splitProps(props, ["class", "classList"]) + + return ( + + ) +} + +function CollapsibleTrigger(props: ComponentProps) { + return +} + +function CollapsibleContent(props: ComponentProps) { + return +} + +export const Collapsible = Object.assign(CollapsibleRoot, { + Trigger: CollapsibleTrigger, + Content: CollapsibleContent, +}) diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 71cfd3a8..3691363c 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -1,4 +1,5 @@ export * from "./button" +export * from "./collapsible" export * from "./dialog" export * from "./icon" export * from "./icon-button" diff --git a/packages/ui/src/demo.tsx b/packages/ui/src/demo.tsx index 85fd20c0..7c507d7d 100644 --- a/packages/ui/src/demo.tsx +++ b/packages/ui/src/demo.tsx @@ -1,6 +1,19 @@ import type { Component } from "solid-js" import { createSignal } from "solid-js" -import { Button, Select, Tabs, Tooltip, Fonts, List, Dialog, Icon, IconButton, Input, SelectDialog } from "./components" +import { + Button, + Select, + Tabs, + Tooltip, + Fonts, + List, + Dialog, + Icon, + IconButton, + Input, + SelectDialog, + Collapsible, +} from "./components" import "./index.css" const Demo: Component = () => { @@ -180,6 +193,27 @@ const Demo: Component = () => { {(item) =>
{item}
} +

Collapsible

+
+ + + + + +
+

This is collapsible content that can be toggled open and closed.

+

It animates smoothly using CSS animations.

+
+
+
+
)