Merge remote-tracking branch 'upstream/dev' into refactor-shells

This commit is contained in:
LukeParkerDev 2026-04-23 17:31:07 +10:00
commit f9a633bd0b
1134 changed files with 98917 additions and 58463 deletions

View file

@ -51,10 +51,10 @@
line-height: var(--line-height-large); /* 166.667% */
letter-spacing: var(--letter-spacing-normal);
&:hover {
&:hover:not([data-disabled]) {
background-color: var(--surface-base-hover);
}
&:active {
&:active:not([data-disabled]) {
background-color: var(--surface-base-active);
}
&:focus-visible {

View file

@ -15,7 +15,7 @@ function AccordionRoot(props: AccordionProps) {
{...rest}
data-component="accordion"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
/>
@ -29,7 +29,7 @@ function AccordionItem(props: AccordionItemProps) {
{...rest}
data-slot="accordion-item"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
/>
@ -43,7 +43,7 @@ function AccordionHeader(props: ParentProps<AccordionHeaderProps>) {
{...rest}
data-slot="accordion-header"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>
@ -59,7 +59,7 @@ function AccordionTrigger(props: ParentProps<AccordionTriggerProps>) {
{...rest}
data-slot="accordion-trigger"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>
@ -75,7 +75,7 @@ function AccordionContent(props: ParentProps<AccordionContentProps>) {
{...rest}
data-slot="accordion-content"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>

View file

@ -77,7 +77,7 @@ export const AppIcon: Component<AppIconProps> = (props) => {
alt={local.alt ?? ""}
draggable={local.draggable ?? false}
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
/>

View file

@ -0,0 +1,43 @@
import { describe, expect, test } from "bun:test"
import { patchFiles } from "./apply-patch-file"
import { text } from "./session-diff"
describe("apply patch file", () => {
test("parses patch metadata from the server", () => {
const file = patchFiles([
{
filePath: "/tmp/a.ts",
relativePath: "a.ts",
type: "update",
patch:
"Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n@@ -1,2 +1,2 @@\n one\n-two\n+three\n",
additions: 1,
deletions: 1,
},
])[0]
expect(file).toBeDefined()
expect(file?.view.fileDiff.name).toBe("a.ts")
expect(text(file!.view, "deletions")).toBe("one\ntwo\n")
expect(text(file!.view, "additions")).toBe("one\nthree\n")
})
test("keeps legacy before and after payloads working", () => {
const file = patchFiles([
{
filePath: "/tmp/a.ts",
relativePath: "a.ts",
type: "update",
before: "one\n",
after: "two\n",
additions: 1,
deletions: 1,
},
])[0]
expect(file).toBeDefined()
expect(file?.view.patch).toContain("@@ -1,1 +1,1 @@")
expect(text(file!.view, "deletions")).toBe("one\n")
expect(text(file!.view, "additions")).toBe("two\n")
})
})

View file

@ -0,0 +1,78 @@
import { normalize, type ViewDiff } from "./session-diff"
type Kind = "add" | "update" | "delete" | "move"
type Raw = {
filePath?: string
relativePath?: string
type?: Kind
patch?: string
diff?: string
before?: string
after?: string
additions?: number
deletions?: number
movePath?: string
}
export type ApplyPatchFile = {
filePath: string
relativePath: string
type: Kind
additions: number
deletions: number
movePath?: string
view: ViewDiff
}
function kind(value: unknown) {
if (value === "add" || value === "update" || value === "delete" || value === "move") return value
}
function status(type: Kind): "added" | "deleted" | "modified" {
if (type === "add") return "added"
if (type === "delete") return "deleted"
return "modified"
}
export function patchFile(raw: unknown): ApplyPatchFile | undefined {
if (!raw || typeof raw !== "object") return
const value = raw as Raw
const type = kind(value.type)
const filePath = typeof value.filePath === "string" ? value.filePath : undefined
const relativePath = typeof value.relativePath === "string" ? value.relativePath : filePath
const patch = typeof value.patch === "string" ? value.patch : typeof value.diff === "string" ? value.diff : undefined
const before = typeof value.before === "string" ? value.before : undefined
const after = typeof value.after === "string" ? value.after : undefined
if (!type || !filePath || !relativePath) return
if (!patch && before === undefined && after === undefined) return
const additions = typeof value.additions === "number" ? value.additions : 0
const deletions = typeof value.deletions === "number" ? value.deletions : 0
const movePath = typeof value.movePath === "string" ? value.movePath : undefined
return {
filePath,
relativePath,
type,
additions,
deletions,
movePath,
view: normalize({
file: relativePath,
patch,
before,
after,
additions,
deletions,
status: status(type),
}),
}
}
export function patchFiles(raw: unknown) {
if (!Array.isArray(raw)) return []
return raw.map(patchFile).filter((file): file is ApplyPatchFile => !!file)
}

View file

@ -38,7 +38,7 @@ export function Avatar(props: AvatarProps) {
data-size={split.size || "normal"}
data-has-image={src ? "" : undefined}
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
style={{

View file

@ -100,7 +100,7 @@ export function BasicTool(props: BasicToolProps) {
if (isOpen) {
contentRef.style.overflow = "hidden"
heightAnim = animate(contentRef, { height: "auto" }, SPRING)
heightAnim.finished.then(() => {
void heightAnim.finished.then(() => {
if (!contentRef || !open()) return
contentRef.style.overflow = "visible"
contentRef.style.height = "auto"

View file

@ -20,7 +20,7 @@ export function Button(props: ButtonProps) {
data-variant={split.variant || "secondary"}
data-icon={split.icon}
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>

View file

@ -53,7 +53,7 @@ export function Card(props: CardProps) {
data-variant={variant()}
style={mix(split.style, accent())}
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>
@ -76,7 +76,7 @@ export function CardTitle(props: CardTitleProps) {
{...rest}
data-slot="card-title"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>
@ -97,7 +97,7 @@ export function CardDescription(props: ComponentProps<"div">) {
{...rest}
data-slot="card-description"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>
@ -113,7 +113,7 @@ export function CardActions(props: ComponentProps<"div">) {
{...rest}
data-slot="card-actions"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>

View file

@ -9,7 +9,7 @@
overflow: visible;
&.tool-collapsible {
--tool-content-gap: 8px;
--tool-content-gap: 4px;
gap: var(--tool-content-gap);
}

View file

@ -15,7 +15,7 @@ function CollapsibleRoot(props: CollapsibleProps) {
data-component="collapsible"
data-variant={local.variant || "normal"}
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
{...others}

View file

@ -33,7 +33,7 @@ function ContextMenuTrigger(props: ParentProps<ContextMenuTriggerProps>) {
{...rest}
data-slot="context-menu-trigger"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -49,7 +49,7 @@ function ContextMenuIcon(props: ParentProps<ContextMenuIconProps>) {
{...rest}
data-slot="context-menu-icon"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -69,7 +69,7 @@ function ContextMenuContent(props: ParentProps<ContextMenuContentProps>) {
{...rest}
data-component="context-menu-content"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -85,7 +85,7 @@ function ContextMenuArrow(props: ContextMenuArrowProps) {
{...rest}
data-slot="context-menu-arrow"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
/>
@ -99,7 +99,7 @@ function ContextMenuSeparator(props: ContextMenuSeparatorProps) {
{...rest}
data-slot="context-menu-separator"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
/>
@ -113,7 +113,7 @@ function ContextMenuGroup(props: ParentProps<ContextMenuGroupProps>) {
{...rest}
data-slot="context-menu-group"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -129,7 +129,7 @@ function ContextMenuGroupLabel(props: ParentProps<ContextMenuGroupLabelProps>) {
{...rest}
data-slot="context-menu-group-label"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -145,7 +145,7 @@ function ContextMenuItem(props: ParentProps<ContextMenuItemProps>) {
{...rest}
data-slot="context-menu-item"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -161,7 +161,7 @@ function ContextMenuItemLabel(props: ParentProps<ContextMenuItemLabelProps>) {
{...rest}
data-slot="context-menu-item-label"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -177,7 +177,7 @@ function ContextMenuItemDescription(props: ParentProps<ContextMenuItemDescriptio
{...rest}
data-slot="context-menu-item-description"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -193,7 +193,7 @@ function ContextMenuItemIndicator(props: ParentProps<ContextMenuItemIndicatorPro
{...rest}
data-slot="context-menu-item-indicator"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -209,7 +209,7 @@ function ContextMenuRadioGroup(props: ParentProps<ContextMenuRadioGroupProps>) {
{...rest}
data-slot="context-menu-radio-group"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -225,7 +225,7 @@ function ContextMenuRadioItem(props: ParentProps<ContextMenuRadioItemProps>) {
{...rest}
data-slot="context-menu-radio-item"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -241,7 +241,7 @@ function ContextMenuCheckboxItem(props: ParentProps<ContextMenuCheckboxItemProps
{...rest}
data-slot="context-menu-checkbox-item"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -261,7 +261,7 @@ function ContextMenuSubTrigger(props: ParentProps<ContextMenuSubTriggerProps>) {
{...rest}
data-slot="context-menu-sub-trigger"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -277,7 +277,7 @@ function ContextMenuSubContent(props: ParentProps<ContextMenuSubContentProps>) {
{...rest}
data-component="context-menu-sub-content"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>

View file

@ -28,7 +28,7 @@ export function Dialog(props: DialogProps) {
data-slot="dialog-content"
data-no-header={!props.title && !props.action ? "" : undefined}
classList={{
...(props.classList ?? {}),
...props.classList,
[props.class ?? ""]: !!props.class,
}}
onOpenAutoFocus={(e) => {

View file

@ -11,7 +11,7 @@ export function DockShell(props: ComponentProps<"div">) {
{...rest}
data-dock-surface="shell"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>
@ -27,7 +27,7 @@ export function DockShellForm(props: ComponentProps<"form">) {
{...rest}
data-dock-surface="shell"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>
@ -44,7 +44,7 @@ export function DockTray(props: DockTrayProps) {
data-dock-surface="tray"
data-dock-attach={split.attach || "none"}
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>

View file

@ -33,7 +33,7 @@ function DropdownMenuTrigger(props: ParentProps<DropdownMenuTriggerProps>) {
{...rest}
data-slot="dropdown-menu-trigger"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -49,7 +49,7 @@ function DropdownMenuIcon(props: ParentProps<DropdownMenuIconProps>) {
{...rest}
data-slot="dropdown-menu-icon"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -69,7 +69,7 @@ function DropdownMenuContent(props: ParentProps<DropdownMenuContentProps>) {
{...rest}
data-component="dropdown-menu-content"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -85,7 +85,7 @@ function DropdownMenuArrow(props: DropdownMenuArrowProps) {
{...rest}
data-slot="dropdown-menu-arrow"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
/>
@ -99,7 +99,7 @@ function DropdownMenuSeparator(props: DropdownMenuSeparatorProps) {
{...rest}
data-slot="dropdown-menu-separator"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
/>
@ -113,7 +113,7 @@ function DropdownMenuGroup(props: ParentProps<DropdownMenuGroupProps>) {
{...rest}
data-slot="dropdown-menu-group"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -129,7 +129,7 @@ function DropdownMenuGroupLabel(props: ParentProps<DropdownMenuGroupLabelProps>)
{...rest}
data-slot="dropdown-menu-group-label"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -145,7 +145,7 @@ function DropdownMenuItem(props: ParentProps<DropdownMenuItemProps>) {
{...rest}
data-slot="dropdown-menu-item"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -161,7 +161,7 @@ function DropdownMenuItemLabel(props: ParentProps<DropdownMenuItemLabelProps>) {
{...rest}
data-slot="dropdown-menu-item-label"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -177,7 +177,7 @@ function DropdownMenuItemDescription(props: ParentProps<DropdownMenuItemDescript
{...rest}
data-slot="dropdown-menu-item-description"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -193,7 +193,7 @@ function DropdownMenuItemIndicator(props: ParentProps<DropdownMenuItemIndicatorP
{...rest}
data-slot="dropdown-menu-item-indicator"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -209,7 +209,7 @@ function DropdownMenuRadioGroup(props: ParentProps<DropdownMenuRadioGroupProps>)
{...rest}
data-slot="dropdown-menu-radio-group"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -225,7 +225,7 @@ function DropdownMenuRadioItem(props: ParentProps<DropdownMenuRadioItemProps>) {
{...rest}
data-slot="dropdown-menu-radio-item"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -241,7 +241,7 @@ function DropdownMenuCheckboxItem(props: ParentProps<DropdownMenuCheckboxItemPro
{...rest}
data-slot="dropdown-menu-checkbox-item"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -261,7 +261,7 @@ function DropdownMenuSubTrigger(props: ParentProps<DropdownMenuSubTriggerProps>)
{...rest}
data-slot="dropdown-menu-sub-trigger"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -277,7 +277,7 @@ function DropdownMenuSubContent(props: ParentProps<DropdownMenuSubContentProps>)
{...rest}
data-component="dropdown-menu-sub-content"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>

View file

@ -18,7 +18,7 @@ export const FileIcon: Component<FileIconProps> = (props) => {
data-component="file-icon"
{...rest}
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>

View file

@ -99,7 +99,7 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
{
...createDefaultOptions(props.diffStyle),
...others,
...(local.preloadedDiff.options ?? {}),
...local.preloadedDiff.options,
},
virtualizer,
virtualMetrics,
@ -109,7 +109,7 @@ function DiffSSRViewer<T>(props: SSRDiffFileProps<T>) {
{
...createDefaultOptions(props.diffStyle),
...others,
...(local.preloadedDiff.options ?? {}),
...local.preloadedDiff.options,
},
workerPool,
)

View file

@ -1,4 +1,4 @@
import { sampledChecksum } from "@opencode-ai/util/encode"
import { sampledChecksum } from "@opencode-ai/shared/util/encode"
import {
DEFAULT_VIRTUAL_FILE_METRICS,
type DiffLineAnnotation,
@ -655,7 +655,7 @@ function ViewerShell(props: {
style={styleVariables}
class="relative outline-none"
classList={{
...(props.classList || {}),
...props.classList,
[props.class ?? ""]: !!props.class,
}}
ref={(el) => (props.viewer.wrapper = el)}
@ -698,6 +698,7 @@ function TextViewer<T>(props: TextFileProps<T>) {
if (typeof value === "string") return value
if (Array.isArray(value)) return value.join("\n")
if (value == null) return ""
// oxlint-disable-next-line no-base-to-string -- file contents cast to unknown, coercion is intentional
return String(value)
}
@ -712,11 +713,13 @@ function TextViewer<T>(props: TextFileProps<T>) {
if (typeof value === "string") return value.length
if (Array.isArray(value)) {
return value.reduce(
// oxlint-disable-next-line no-base-to-string -- array parts coerced intentionally
(sum, part) => sum + (typeof part === "string" ? part.length + 1 : String(part).length + 1),
0,
)
}
if (value == null) return 0
// oxlint-disable-next-line no-base-to-string -- file contents cast to unknown, coercion is intentional
return String(value).length
})

View file

@ -20,7 +20,7 @@ export function HoverCard(props: HoverCardProps) {
<Kobalte.Content
data-component="hover-card-content"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>

View file

@ -19,7 +19,7 @@ export function IconButton(props: ComponentProps<"button"> & IconButtonProps) {
data-size={split.size || "normal"}
data-variant={split.variant || "secondary"}
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>

View file

@ -117,7 +117,7 @@ export function Icon(props: IconProps) {
<svg
data-slot="icon-svg"
classList={{
...(local.classList || {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
fill="none"

View file

@ -10,7 +10,7 @@ export function Keybind(props: KeybindProps) {
<span
data-component="keybind"
classList={{
...(props.classList ?? {}),
...props.classList,
[props.class ?? ""]: !!props.class,
}}
>

View file

@ -1,5 +1,5 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
import { createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js"
import { Button } from "./button"
import { FileIcon } from "./file-icon"

View file

@ -107,7 +107,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
// Force a refetch even if the value is unchanged.
// This is important for programmatic changes like Tab completion.
if (prev === value) {
refetch()
void refetch()
return
}
queueMicrotask(() => refetch())

View file

@ -6,7 +6,7 @@
color: var(--text-strong);
font-family: var(--font-family-sans);
font-size: var(--font-size-base); /* 14px */
line-height: var(--line-height-x-large);
line-height: 160%;
/* Spacing for flow */
> *:first-child {
@ -23,11 +23,11 @@
h4,
h5,
h6 {
font-size: var(--font-size-base);
font-size: 14px;
color: var(--text-strong);
font-weight: var(--font-weight-medium);
margin-top: 2rem;
margin-bottom: 0.75rem;
margin-top: 0px;
margin-bottom: 24px;
line-height: var(--line-height-large);
}
@ -40,7 +40,7 @@
/* Paragraphs */
p {
margin-bottom: 1rem;
margin-bottom: 12px;
}
/* Links */
@ -58,10 +58,10 @@
/* Lists */
ul,
ol {
margin-top: 0.5rem;
margin-bottom: 1rem;
margin-top: 8px;
margin-bottom: 12px;
margin-left: 0;
padding-left: 1.5rem;
padding-left: 32px;
list-style-position: outside;
}
@ -75,7 +75,7 @@
}
li {
margin-bottom: 0.5rem;
margin-bottom: 8px;
}
li > p:first-child {
@ -117,12 +117,12 @@
hr {
border: none;
height: 0;
margin: 2.5rem 0;
margin: 40px 0;
}
.shiki {
font-size: 13px;
padding: 8px 12px;
padding: 12px;
border-radius: 6px;
border: 0.5px solid var(--border-weak-base);
}
@ -201,8 +201,8 @@
}
pre {
margin-top: 2rem;
margin-bottom: 2rem;
margin-top: 12px;
margin-bottom: 32px;
overflow: auto;
scrollbar-width: none;
@ -229,7 +229,7 @@
table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
margin: 24px 0;
font-size: var(--font-size-base);
display: block;
overflow-x: auto;
@ -239,7 +239,7 @@
td {
/* Minimal borders for structure, matching TUI "lines" roughly but keeping it web-clean */
border-bottom: 1px solid var(--border-weaker-base);
padding: 0.75rem 0.5rem;
padding: 12px;
text-align: left;
vertical-align: top;
}

View file

@ -2,7 +2,7 @@ import { useMarked } from "../context/marked"
import { useI18n } from "../context/i18n"
import DOMPurify from "dompurify"
import morphdom from "morphdom"
import { checksum } from "@opencode-ai/util/encode"
import { checksum } from "@opencode-ai/shared/util/encode"
import { ComponentProps, createEffect, createResource, createSignal, onCleanup, splitProps } from "solid-js"
import { isServer } from "solid-js/web"
import { stream } from "./markdown-stream"
@ -50,7 +50,7 @@ function escape(text: string) {
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
}
@ -338,7 +338,7 @@ export function Markdown(
<div
data-component="markdown"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
ref={setRoot}

View file

@ -283,9 +283,9 @@
line-height: var(--line-height-normal);
[data-component="markdown"] {
margin-top: 24px;
margin-top: 16px;
font-style: normal;
font-size: var(--font-size-base);
font-size: 13px;
color: var(--text-weak);
strong,
@ -556,9 +556,12 @@
[data-component="exa-tool-output"] {
width: 100%;
padding-top: 8px;
display: flex;
flex-direction: column;
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
line-height: var(--line-height-large);
color: var(--text-base);
}
[data-slot="basic-tool-tool-subtitle"].exa-tool-query {
@ -578,6 +581,8 @@
[data-slot="exa-tool-link"] {
display: block;
max-width: 100%;
font: inherit;
line-height: inherit;
color: var(--text-interactive-base);
text-decoration: underline;
text-underline-offset: 2px;
@ -636,13 +641,13 @@
}
[data-component="context-tool-group-list"] {
padding-top: 6px;
padding-top: 0;
padding-right: 0;
padding-bottom: 4px;
padding-left: 13px;
padding-bottom: 0;
padding-left: 12px;
display: flex;
flex-direction: column;
gap: 8px;
gap: 4px;
[data-slot="context-tool-group-item"] {
min-width: 0;

View file

@ -37,7 +37,6 @@ import { type UiI18n, useI18n } from "../context/i18n"
import { BasicTool, GenericTool } from "./basic-tool"
import { Accordion } from "./accordion"
import { StickyAccordionHeader } from "./sticky-accordion-header"
import { Card } from "./card"
import { Collapsible } from "./collapsible"
import { FileIcon } from "./file-icon"
import { Icon } from "./icon"
@ -46,14 +45,15 @@ import { Checkbox } from "./checkbox"
import { DiffChanges } from "./diff-changes"
import { Markdown } from "./markdown"
import { ImagePreview } from "./image-preview"
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode"
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/shared/util/path"
import { checksum } from "@opencode-ai/shared/util/encode"
import { Tooltip } from "./tooltip"
import { IconButton } from "./icon-button"
import { Spinner } from "./spinner"
import { TextShimmer } from "./text-shimmer"
import { AnimatedCountList } from "./tool-count-summary"
import { ToolStatusTitle } from "./tool-status-title"
import { patchFiles } from "./apply-patch-file"
import { animate } from "motion"
import { useLocation } from "@solidjs/router"
import { attached, inline, kind } from "./message-file"
@ -1162,7 +1162,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
onMouseDown={(e) => e.preventDefault()}
onClick={(event) => {
event.stopPropagation()
handleCopy()
void handleCopy()
}}
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
/>
@ -1273,7 +1273,7 @@ function ToolFileAccordion(props: { path: string; actions?: JSX.Element; childre
<Accordion
multiple
data-scope="apply-patch"
style={{ "--sticky-accordion-offset": "40px" }}
style={{ "--sticky-accordion-offset": "calc(32px + var(--tool-content-gap))" }}
defaultValue={[value()]}
>
<Accordion.Item value={value()}>
@ -2018,24 +2018,12 @@ ToolRegistry.register({
},
})
interface ApplyPatchFile {
filePath: string
relativePath: string
type: "add" | "update" | "delete" | "move"
diff: string
before: string
after: string
additions: number
deletions: number
movePath?: string
}
ToolRegistry.register({
name: "apply_patch",
render(props) {
const i18n = useI18n()
const fileComponent = useFileComponent()
const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[])
const files = createMemo(() => patchFiles(props.metadata.files))
const pending = createMemo(() => props.status === "pending" || props.status === "running")
const single = createMemo(() => {
const list = files()
@ -2077,7 +2065,7 @@ ToolRegistry.register({
<Accordion
multiple
data-scope="apply-patch"
style={{ "--sticky-accordion-offset": "40px" }}
style={{ "--sticky-accordion-offset": "calc(32px + var(--tool-content-gap))" }}
value={expanded()}
onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])}
>
@ -2141,12 +2129,7 @@ ToolRegistry.register({
<Accordion.Content>
<Show when={visible()}>
<div data-component="apply-patch-file-diff">
<Dynamic
component={fileComponent}
mode="diff"
before={{ name: file.filePath, contents: file.before }}
after={{ name: file.movePath ?? file.filePath, contents: file.after }}
/>
<Dynamic component={fileComponent} mode="diff" fileDiff={file.view.fileDiff} />
</div>
</Show>
</Accordion.Content>
@ -2216,12 +2199,7 @@ ToolRegistry.register({
}
>
<div data-component="apply-patch-file-diff">
<Dynamic
component={fileComponent}
mode="diff"
before={{ name: single()!.filePath, contents: single()!.before }}
after={{ name: single()!.movePath ?? single()!.filePath, contents: single()!.after }}
/>
<Dynamic component={fileComponent} mode="diff" fileDiff={single()!.view.fileDiff} />
</div>
</ToolFileAccordion>
</BasicTool>

View file

@ -106,7 +106,7 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
ref={(el: HTMLElement | undefined) => setState("contentRef", el)}
data-component="popover-content"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
style={local.style}

View file

@ -32,7 +32,7 @@ export function ProgressCircle(props: ProgressCircleProps) {
fill="none"
data-component="progress-circle"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>

View file

@ -15,7 +15,7 @@ export function Progress(props: ProgressProps) {
{...others}
data-component="progress"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>

View file

@ -15,7 +15,7 @@ export const ProviderIcon: Component<ProviderIconProps> = (props) => {
data-component="provider-icon"
{...rest}
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>

View file

@ -32,6 +32,7 @@ export const iconNames = [
"perplexity",
"ovhcloud",
"openrouter",
"llmgateway",
"opencode",
"opencode-go",
"openai",

View file

@ -56,7 +56,7 @@ export function RadioGroup<T>(props: RadioGroupProps<T>) {
data-fill={local.fill ? "" : undefined}
data-pad={local.pad ?? "normal"}
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
value={local.current ? getValue(local.current) : undefined}

View file

@ -73,7 +73,7 @@ export function ResizeHandle(props: ResizeHandleProps) {
data-direction={local.direction}
data-edge={local.edge ?? (local.direction === "vertical" ? "start" : "end")}
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
onMouseDown={handleMouseDown}

View file

@ -104,7 +104,7 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
{...itemProps}
data-slot="select-select-item"
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
onPointerEnter={() => move(itemProps.item.rawValue)}
@ -141,7 +141,7 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
variant={props.variant}
style={local.triggerStyle}
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
@ -160,7 +160,7 @@ export function Select<T>(props: SelectProps<T> & Omit<ButtonProps, "children">)
<Kobalte.Portal>
<Kobalte.Content
classList={{
...(local.classList ?? {}),
...local.classList,
[local.class ?? ""]: !!local.class,
}}
data-component="select-content"

View file

@ -1,6 +1,5 @@
import { parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs"
import { sampledChecksum } from "@opencode-ai/util/encode"
import { formatPatch, structuredPatch } from "diff"
import { parseDiffFromFile, type FileDiffMetadata } from "@pierre/diffs"
import { formatPatch, parsePatch, structuredPatch } from "diff"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
type LegacyDiff = {
@ -26,41 +25,51 @@ export type ViewDiff = {
const cache = new Map<string, FileDiffMetadata>()
function empty(file: string, key: string) {
return {
name: file,
type: "change",
hunks: [],
splitLineCount: 0,
unifiedLineCount: 0,
isPartial: true,
deletionLines: [],
additionLines: [],
cacheKey: key,
} satisfies FileDiffMetadata
}
function patch(diff: ReviewDiff) {
if (typeof diff.patch === "string") return diff.patch
return formatPatch(
structuredPatch(
diff.file,
diff.file,
"before" in diff && typeof diff.before === "string" ? diff.before : "",
"after" in diff && typeof diff.after === "string" ? diff.after : "",
"",
"",
{ context: Number.MAX_SAFE_INTEGER },
if (typeof diff.patch === "string") {
const [patch] = parsePatch(diff.patch)
const beforeLines = []
const afterLines = []
for (const hunk of patch.hunks) {
for (const line of hunk.lines) {
if (line.startsWith("-")) {
beforeLines.push(line.slice(1))
} else if (line.startsWith("+")) {
afterLines.push(line.slice(1))
} else {
// context line (starts with ' ')
beforeLines.push(line.slice(1))
afterLines.push(line.slice(1))
}
}
}
return { before: beforeLines.join("\n"), after: afterLines.join("\n"), patch: diff.patch }
}
return {
before: "before" in diff && typeof diff.before === "string" ? diff.before : "",
after: "after" in diff && typeof diff.after === "string" ? diff.after : "",
patch: formatPatch(
structuredPatch(
diff.file,
diff.file,
"before" in diff && typeof diff.before === "string" ? diff.before : "",
"after" in diff && typeof diff.after === "string" ? diff.after : "",
"",
"",
{ context: Number.MAX_SAFE_INTEGER },
),
),
)
}
}
function file(file: string, patch: string) {
function file(file: string, patch: string, before: string, after: string) {
const hit = cache.get(patch)
if (hit) return hit
const key = sampledChecksum(patch) ?? file
const value = parsePatchFiles(patch, key).flatMap((item) => item.files)[0] ?? empty(file, key)
const value = parseDiffFromFile({ name: file, contents: before }, { name: file, contents: after })
cache.set(patch, value)
return value
}
@ -69,11 +78,11 @@ export function normalize(diff: ReviewDiff): ViewDiff {
const next = patch(diff)
return {
file: diff.file,
patch: next,
patch: next.patch,
additions: diff.additions,
deletions: diff.deletions,
status: diff.status,
fileDiff: file(diff.file, next),
fileDiff: file(diff.file, next.patch, next.before, next.after),
}
}

View file

@ -11,8 +11,8 @@ import { Tooltip } from "./tooltip"
import { ScrollView } from "./scroll-view"
import { useFileComponent } from "../context/file"
import { useI18n } from "../context/i18n"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode"
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
import { checksum } from "@opencode-ai/shared/util/encode"
import { createEffect, createMemo, For, Match, onCleanup, Show, Switch, untrack, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { type FileContent, type SnapshotFileDiff, type VcsFileDiff } from "@opencode-ai/sdk/v2"
@ -65,6 +65,26 @@ export type SessionReviewFocus = { file: string; id: string }
type ReviewDiff = (SnapshotFileDiff | VcsFileDiff) & { preloaded?: PreloadMultiFileDiffResult<any> }
type Item = ViewDiff & { preloaded?: PreloadMultiFileDiffResult<any> }
function diff(value: unknown): value is ReviewDiff {
if (!value || typeof value !== "object" || Array.isArray(value)) return false
if (!("file" in value) || typeof value.file !== "string") return false
if (!("additions" in value) || typeof value.additions !== "number") return false
if (!("deletions" in value) || typeof value.deletions !== "number") return false
if ("patch" in value && value.patch !== undefined && typeof value.patch !== "string") return false
if ("before" in value && value.before !== undefined && typeof value.before !== "string") return false
if ("after" in value && value.after !== undefined && typeof value.after !== "string") return false
if (!("status" in value) || value.status === undefined) return true
return value.status === "added" || value.status === "deleted" || value.status === "modified"
}
function list(value: unknown): ReviewDiff[] {
if (Array.isArray(value) && value.every(diff)) return value
if (Array.isArray(value)) return value.filter(diff)
if (diff(value)) return [value]
if (!value || typeof value !== "object") return []
return Object.values(value).filter(diff)
}
export interface SessionReviewProps {
title?: JSX.Element
empty?: JSX.Element
@ -157,7 +177,9 @@ export const SessionReview = (props: SessionReviewProps) => {
const opened = () => store.opened
const open = () => props.open ?? store.open
const items = createMemo<Item[]>(() => props.diffs.map((diff) => ({ ...normalize(diff), preloaded: diff.preloaded })))
const items = createMemo<Item[]>(() =>
list(props.diffs).map((diff) => ({ ...normalize(diff), preloaded: diff.preloaded })),
)
const files = createMemo(() => items().map((diff) => diff.file))
const grouped = createMemo(() => {
const next = new Map<string, SessionReviewComment[]>()
@ -363,9 +385,11 @@ export const SessionReview = (props: SessionReviewProps) => {
<Accordion multiple value={open()} onChange={handleChange}>
<For each={items()}>
{(diff) => {
let wrapper: HTMLDivElement | undefined
const file = diff.file
// binary files have empty diffs that we can't render
const diffCanRender = () => diff.additions !== 0 || diff.deletions !== 0
const expanded = createMemo(() => open().includes(file))
const mounted = createMemo(() => expanded() && (!!store.visible[file] || pinned(file)))
const force = () => !!store.force[file]
@ -474,14 +498,14 @@ export const SessionReview = (props: SessionReviewProps) => {
return (
<Accordion.Item
value={file}
value={diffCanRender() ? file : null!}
id={diffId(file)}
data-file={file}
data-slot="session-review-accordion-item"
data-selected={props.focusedFile === file ? "" : undefined}
>
<StickyAccordionHeader>
<Accordion.Trigger>
<Accordion.Trigger disabled={!diffCanRender()} class="cursor-default">
<div data-slot="session-review-trigger-content">
<div data-slot="session-review-file-info">
<FileIcon node={{ path: file, type: "file" }} />
@ -490,7 +514,7 @@ export const SessionReview = (props: SessionReviewProps) => {
<span data-slot="session-review-directory">{`\u202A${getDirectory(file)}\u202C`}</span>
</Show>
<span data-slot="session-review-filename">{getFilename(file)}</span>
<Show when={props.onViewFile}>
<Show when={props.onViewFile && diffCanRender()}>
<Tooltip value={openFileLabel()} placement="top" gutter={4}>
<button
data-slot="session-review-view-button"
@ -531,9 +555,11 @@ export const SessionReview = (props: SessionReviewProps) => {
<DiffChanges changes={diff} />
</Match>
</Switch>
<span data-slot="session-review-diff-chevron">
<Icon name="chevron-down" size="small" />
</span>
<Show when={diffCanRender()}>
<span data-slot="session-review-diff-chevron">
<Icon name="chevron-down" size="small" />
</span>
</Show>
</div>
</div>
</Accordion.Trigger>
@ -542,7 +568,6 @@ export const SessionReview = (props: SessionReviewProps) => {
<div
data-slot="session-review-diff-wrapper"
ref={(el) => {
wrapper = el
anchors.set(file, el)
nodes.set(file, el)
queue()

View file

@ -26,7 +26,7 @@
align-items: flex-start;
align-self: stretch;
min-width: 0;
gap: 18px;
gap: 0px;
overflow-anchor: none;
}
@ -47,6 +47,7 @@
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
width: 100%;
min-width: 0;
color: var(--text-weak);
@ -94,9 +95,15 @@
[data-slot="session-turn-diffs-header"] {
display: flex;
align-items: baseline;
align-items: center;
gap: 8px;
padding-top: 4px;
padding-bottom: 12px;
position: sticky;
top: var(--sticky-accordion-top, 0px);
z-index: 20;
background-color: var(--background-stronger);
height: 44px;
}
[data-slot="session-turn-diffs-label"] {
@ -220,5 +227,5 @@
}
[data-slot="session-turn-list"] {
gap: 48px;
gap: 24px;
}

View file

@ -8,8 +8,8 @@ import type { SessionStatus } from "@opencode-ai/sdk/v2"
import { useData } from "../context"
import { useFileComponent } from "../context/file"
import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Binary } from "@opencode-ai/shared/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/shared/util/path"
import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { Dynamic } from "solid-js/web"
@ -110,7 +110,7 @@ function partState(part: PartType, showReasoningSummaries: boolean) {
function clean(value: string) {
return value
.replace(/`([^`]+)`/g, "$1")
.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/[*_~]+/g, "")
.trim()
}
@ -267,14 +267,12 @@ export function SessionTurn(
if (!msg) return emptyAssistant
const messages = allMessages() ?? emptyMessages
const index = messageIndex()
if (index < 0) return emptyAssistant
if (messageIndex() < 0) return emptyAssistant
const result: AssistantMessage[] = []
for (let i = index + 1; i < messages.length; i++) {
for (let i = 0; i < messages.length; i++) {
const item = messages[i]
if (!item) continue
if (item.role === "user") break
if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage)
}
return result
@ -313,6 +311,7 @@ export function SessionTurn(
const msg = error()?.data?.message
if (typeof msg === "string") return unwrap(msg)
if (msg === undefined || msg === null) return ""
// oxlint-disable-next-line no-base-to-string -- msg is unknown from error data, coercion is intentional
return unwrap(String(msg))
})
@ -447,7 +446,7 @@ export function SessionTurn(
<div data-component="session-turn-diffs-content">
<Accordion
multiple
style={{ "--sticky-accordion-offset": "40px" }}
style={{ "--sticky-accordion-offset": "44px" }}
value={expanded()}
onChange={(value) => setState("expanded", Array.isArray(value) ? value : value ? [value] : [])}
>

View file

@ -23,7 +23,7 @@ export function Spinner(props: {
viewBox="0 0 15 15"
data-component="spinner"
classList={{
...(props.classList ?? {}),
...props.classList,
[props.class ?? ""]: !!props.class,
}}
fill="currentColor"

View file

@ -8,7 +8,7 @@ export function StickyAccordionHeader(
<Accordion.Header
data-component="sticky-accordion-header"
classList={{
...(props.classList ?? {}),
...props.classList,
[props.class ?? ""]: !!props.class,
}}
>

View file

@ -27,7 +27,7 @@ function TabsRoot(props: TabsProps) {
data-variant={split.variant || "normal"}
data-orientation={split.orientation || "horizontal"}
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
/>
@ -41,7 +41,7 @@ function TabsList(props: TabsListProps) {
{...rest}
data-slot="tabs-list"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
/>
@ -63,7 +63,7 @@ function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
data-slot="tabs-trigger-wrapper"
data-value={props.value}
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
onMouseDown={(e) => {
@ -104,7 +104,7 @@ function TabsContent(props: ParentProps<TabsContentProps>) {
{...rest}
data-slot="tabs-content"
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>

View file

@ -12,7 +12,7 @@ export function Tag(props: TagProps) {
data-component="tag"
data-size={split.size || "normal"}
classList={{
...(split.classList ?? {}),
...split.classList,
[split.class ?? ""]: !!split.class,
}}
>

View file

@ -75,7 +75,7 @@ export function TextField(props: TextFieldProps) {
}
function handleClick() {
if (local.copyable) handleCopy()
if (local.copyable) void handleCopy()
}
return (

View file

@ -102,7 +102,7 @@ export function TextReveal(props: {
requestAnimationFrame(() => setState("ready", true))
return
}
fonts.ready.finally(() => {
void fonts.ready.finally(() => {
widen(win())
requestAnimationFrame(() => setState("ready", true))
})

View file

@ -442,7 +442,7 @@ function AnimatedHeading(props) {
onMount(() => {
measure()
document.fonts?.ready.finally(() => {
void document.fonts?.ready.finally(() => {
measure()
requestAnimationFrame(() => setState("ready", true))
})

View file

@ -1,5 +1,5 @@
// @ts-nocheck
import { createSignal, createMemo, createEffect, on, For, Show, Index, batch } from "solid-js"
import { createSignal, createMemo, createEffect, on, For, Show, batch } from "solid-js"
import { createStore, produce } from "solid-js/store"
import type {
Message,
@ -9,7 +9,6 @@ import type {
TextPart,
ReasoningPart,
ToolPart,
CompactionPart,
FilePart,
AgentPart,
} from "@opencode-ai/sdk/v2"
@ -319,7 +318,7 @@ const TOOL_SAMPLES = {
tool: "shell",
input: { command: "bun test --filter session", description: "Run session tests" },
output:
"bun test v1.3.11\n\n✓ session-turn.test.tsx (3 tests) 45ms\n✓ message-part.test.tsx (7 tests) 120ms\n\nTest Suites: 2 passed, 2 total\nTests: 10 passed, 10 total\nTime: 0.89s",
"bun test v1.3.13\n\n✓ session-turn.test.tsx (3 tests) 45ms\n✓ message-part.test.tsx (7 tests) 120ms\n\nTest Suites: 2 passed, 2 total\nTests: 10 passed, 10 total\nTime: 0.89s",
title: "Run session tests",
metadata: { command: "bun test --filter session" },
},
@ -555,10 +554,6 @@ function toolPart(sample: (typeof TOOL_SAMPLES)[keyof typeof TOOL_SAMPLES], stat
} as ToolPart
}
function compactionPart(): CompactionPart {
return { id: uid(), type: "compaction", auto: true } as CompactionPart
}
// ---------------------------------------------------------------------------
// CSS Controls definition
// ---------------------------------------------------------------------------
@ -568,6 +563,7 @@ const MD = "markdown.css"
const MP = "message-part.css"
const ST = "session-turn.css"
const CL = "collapsible.css"
const BT = "basic-tool.css"
/**
* Source mapping for a CSS control.
@ -607,10 +603,10 @@ const CSS_CONTROLS: CSSControl[] = [
// --- Timeline spacing ---
{
key: "turn-gap",
label: "Turn gap",
label: "Above user messages",
group: "Timeline Spacing",
type: "range",
initial: "48",
initial: "32",
selector: '[data-slot="session-turn-list"]',
property: "gap",
min: "0",
@ -621,10 +617,10 @@ const CSS_CONTROLS: CSSControl[] = [
},
{
key: "container-gap",
label: "Container gap",
label: "Below user messages",
group: "Timeline Spacing",
type: "range",
initial: "18",
initial: "0",
selector: '[data-slot="session-turn-message-container"]',
property: "gap",
min: "0",
@ -1040,12 +1036,40 @@ const CSS_CONTROLS: CSSControl[] = [
},
// --- Tool parts ---
{
key: "tool-subtitle-font-size",
label: "Subtitle font size",
group: "Tool Parts",
type: "range",
initial: "14",
selector: '[data-slot="basic-tool-tool-subtitle"]',
property: "font-size",
min: "10",
max: "22",
step: "1",
unit: "px",
source: { file: BT, anchor: '[data-slot="basic-tool-tool-subtitle"]', prop: "font-size", format: px },
},
{
key: "exa-output-font-size",
label: "Search output font size",
group: "Tool Parts",
type: "range",
initial: "14",
selector: '[data-component="exa-tool-output"]',
property: "font-size",
min: "10",
max: "22",
step: "1",
unit: "px",
source: { file: MP, anchor: '[data-component="exa-tool-output"]', prop: "font-size", format: px },
},
{
key: "tool-content-gap",
label: "Trigger/content gap",
group: "Tool Parts",
type: "range",
initial: "8",
initial: "4",
selector: '[data-component="collapsible"].tool-collapsible',
property: "--tool-content-gap",
min: "0",
@ -1059,7 +1083,7 @@ const CSS_CONTROLS: CSSControl[] = [
label: "Explored tool gap",
group: "Explored Group",
type: "range",
initial: "14",
initial: "4",
selector: '[data-component="context-tool-group-list"]',
property: "gap",
min: "0",

View file

@ -30,7 +30,7 @@ function ToastRoot(props: ToastRootComponentProps) {
<Kobalte
data-component="toast"
classList={{
...(props.classList ?? {}),
...props.classList,
[props.class ?? ""]: !!props.class,
}}
{...props}

View file

@ -129,7 +129,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
onMouseDown={(e) => e.preventDefault()}
onClick={(e) => {
e.stopPropagation()
copy()
void copy()
}}
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.toolErrorCard.copyError")}
/>

View file

@ -86,7 +86,7 @@ export function ToolStatusTitle(props: {
finish()
return
}
fonts.ready.finally(() => {
void fonts.ready.finally(() => {
measure()
finish()
})

View file

@ -99,6 +99,8 @@ export function Tooltip(props: TooltipProps) {
onCleanup(() => obs.disconnect())
})
let justClickedTrigger = false
return (
<Switch>
<Match when={local.inactive}>{local.children}</Match>
@ -112,6 +114,10 @@ export function Tooltip(props: TooltipProps) {
onOpenChange={(open) => {
if (local.forceOpen) return
if (state.block && open) return
if (justClickedTrigger) {
justClickedTrigger = false
return
}
setState("open", open)
}}
>
@ -137,6 +143,12 @@ export function Tooltip(props: TooltipProps) {
data-force-open={local.forceOpen}
class={local.contentClass}
style={local.contentStyle}
onPointerDownOutside={(e) => {
if (ref === e.target || (e.target instanceof Node && ref?.contains(e.target))) {
justClickedTrigger = true
}
e.preventDefault()
}}
>
{local.value}
{/* <KobalteTooltip.Arrow data-slot="tooltip-arrow" /> */}

View file

@ -1,5 +1,5 @@
import { type SelectedLineRange } from "@pierre/diffs"
import { diffLineIndex, diffRowIndex, findDiffSide } from "./diff-selection"
import { diffLineIndex, diffRowIndex } from "./diff-selection"
export type CommentSide = "additions" | "deletions"

View file

@ -25,7 +25,7 @@ function createPool(lineDiffType: "none" | "word-alt") {
},
)
pool.initialize()
void pool.initialize()
return pool
}

View file

@ -82,7 +82,7 @@ a {
cursor: default;
}
*[data-tauri-drag-region] {
#root:not([aria-hidden]) *[data-tauri-drag-region] {
app-region: drag;
}