mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-31 06:34:19 +01:00
wip: desktop work
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
[data-slot="collapsible-trigger"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
height: 32px;
|
||||
padding: 6px 8px 6px 12px;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
|
||||
28
packages/ui/src/components/diff-changes.css
Normal file
28
packages/ui/src/components/diff-changes.css
Normal file
@@ -0,0 +1,28 @@
|
||||
[data-component="diff-changes"] {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
[data-slot="additions"] {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
text-align: right;
|
||||
color: var(--text-diff-add-base);
|
||||
}
|
||||
|
||||
[data-slot="deletions"] {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
text-align: right;
|
||||
color: var(--text-diff-delete-base);
|
||||
}
|
||||
}
|
||||
24
packages/ui/src/components/diff-changes.tsx
Normal file
24
packages/ui/src/components/diff-changes.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { FileDiff } from "@opencode-ai/sdk"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
|
||||
export function DiffChanges(props: { diff: FileDiff | FileDiff[] }) {
|
||||
const additions = createMemo(() =>
|
||||
Array.isArray(props.diff)
|
||||
? props.diff.reduce((acc, diff) => acc + (diff.additions ?? 0), 0)
|
||||
: props.diff.additions,
|
||||
)
|
||||
const deletions = createMemo(() =>
|
||||
Array.isArray(props.diff)
|
||||
? props.diff.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0)
|
||||
: props.diff.deletions,
|
||||
)
|
||||
const total = createMemo(() => (additions() ?? 0) + (deletions() ?? 0))
|
||||
return (
|
||||
<Show when={total() > 0}>
|
||||
<div data-component="diff-changes">
|
||||
<span data-slot="additions">{`+${additions()}`}</span>
|
||||
<span data-slot="deletions">{`-${deletions()}`}</span>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -4,12 +4,16 @@ export * from "./checkbox"
|
||||
export * from "./collapsible"
|
||||
export * from "./dialog"
|
||||
export * from "./diff"
|
||||
export * from "./diff-changes"
|
||||
export * from "./icon"
|
||||
export * from "./icon-button"
|
||||
export * from "./input"
|
||||
export * from "./fonts"
|
||||
export * from "./list"
|
||||
export * from "./message-part"
|
||||
export * from "./select"
|
||||
export * from "./select-dialog"
|
||||
export * from "./tabs"
|
||||
export * from "./tool-display"
|
||||
export * from "./tool-registry"
|
||||
export * from "./tooltip"
|
||||
|
||||
22
packages/ui/src/components/message-part.css
Normal file
22
packages/ui/src/components/message-part.css
Normal file
@@ -0,0 +1,22 @@
|
||||
[data-component="assistant-message"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
[data-component="user-message"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-base);
|
||||
display: -webkit-box;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
87
packages/ui/src/components/message-part.tsx
Normal file
87
packages/ui/src/components/message-part.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Component, createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import {
|
||||
AssistantMessage,
|
||||
Message as MessageType,
|
||||
Part as PartType,
|
||||
TextPart,
|
||||
ToolPart,
|
||||
UserMessage,
|
||||
} from "@opencode-ai/sdk"
|
||||
|
||||
export interface MessageProps {
|
||||
message: MessageType
|
||||
parts: PartType[]
|
||||
}
|
||||
|
||||
export interface MessagePartProps {
|
||||
part: PartType
|
||||
message: MessageType
|
||||
hideDetails?: boolean
|
||||
}
|
||||
|
||||
export type PartComponent = Component<MessagePartProps>
|
||||
|
||||
const PART_MAPPING: Record<string, PartComponent | undefined> = {}
|
||||
|
||||
export function registerPartComponent(type: string, component: PartComponent) {
|
||||
PART_MAPPING[type] = component
|
||||
}
|
||||
|
||||
export function Message(props: MessageProps) {
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={props.message.role === "user" && props.message}>
|
||||
{(userMessage) => (
|
||||
<UserMessageDisplay message={userMessage() as UserMessage} parts={props.parts} />
|
||||
)}
|
||||
</Match>
|
||||
<Match when={props.message.role === "assistant" && props.message}>
|
||||
{(assistantMessage) => (
|
||||
<AssistantMessageDisplay
|
||||
message={assistantMessage() as AssistantMessage}
|
||||
parts={props.parts}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[] }) {
|
||||
const filteredParts = createMemo(() => {
|
||||
return props.parts?.filter((x) => {
|
||||
if (x.type === "reasoning") return false
|
||||
return x.type !== "tool" || (x as ToolPart).tool !== "todoread"
|
||||
})
|
||||
})
|
||||
return (
|
||||
<div data-component="assistant-message">
|
||||
<For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
||||
const text = createMemo(() =>
|
||||
props.parts
|
||||
?.filter((p) => p.type === "text" && !(p as TextPart).synthetic)
|
||||
?.map((p) => (p as TextPart).text)
|
||||
?.join(""),
|
||||
)
|
||||
return <div data-component="user-message">{text()}</div>
|
||||
}
|
||||
|
||||
export function Part(props: MessagePartProps) {
|
||||
const component = createMemo(() => PART_MAPPING[props.part.type])
|
||||
return (
|
||||
<Show when={component()}>
|
||||
<Dynamic
|
||||
component={component()}
|
||||
part={props.part}
|
||||
message={props.message}
|
||||
hideDetails={props.hideDetails}
|
||||
/>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
76
packages/ui/src/components/tool-display.css
Normal file
76
packages/ui/src/components/tool-display.css
Normal file
@@ -0,0 +1,76 @@
|
||||
[data-component="tool-trigger"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
gap: 20px;
|
||||
justify-content: space-between;
|
||||
|
||||
[data-slot="tool-trigger-content"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
[data-slot="tool-icon"] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="tool-info"] {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="tool-info-structured"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
[data-slot="tool-info-main"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
[data-slot="tool-title"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-base);
|
||||
|
||||
&.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tool-subtitle"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
[data-slot="tool-arg"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
}
|
||||
95
packages/ui/src/components/tool-display.tsx
Normal file
95
packages/ui/src/components/tool-display.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { children, For, Match, Show, Switch, type JSX } from "solid-js"
|
||||
import { Collapsible } from "./collapsible"
|
||||
import { Icon, IconProps } from "./icon"
|
||||
|
||||
export type TriggerTitle = {
|
||||
title: string
|
||||
titleClass?: string
|
||||
subtitle?: string
|
||||
subtitleClass?: string
|
||||
args?: string[]
|
||||
argsClass?: string
|
||||
action?: JSX.Element
|
||||
}
|
||||
|
||||
const isTriggerTitle = (val: any): val is TriggerTitle => {
|
||||
return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
|
||||
}
|
||||
|
||||
export interface BasicToolProps {
|
||||
icon: IconProps["name"]
|
||||
trigger: TriggerTitle | JSX.Element
|
||||
children?: JSX.Element
|
||||
hideDetails?: boolean
|
||||
}
|
||||
|
||||
export function BasicTool(props: BasicToolProps) {
|
||||
const resolved = children(() => props.children)
|
||||
return (
|
||||
<Collapsible>
|
||||
<Collapsible.Trigger>
|
||||
<div data-component="tool-trigger">
|
||||
<div data-slot="tool-trigger-content">
|
||||
<Icon name={props.icon} size="small" data-slot="tool-icon" />
|
||||
<div data-slot="tool-info">
|
||||
<Switch>
|
||||
<Match when={isTriggerTitle(props.trigger) && props.trigger}>
|
||||
{(trigger) => (
|
||||
<div data-slot="tool-info-structured">
|
||||
<div data-slot="tool-info-main">
|
||||
<span
|
||||
data-slot="tool-title"
|
||||
classList={{
|
||||
[trigger().titleClass ?? ""]: !!trigger().titleClass,
|
||||
}}
|
||||
>
|
||||
{trigger().title}
|
||||
</span>
|
||||
<Show when={trigger().subtitle}>
|
||||
<span
|
||||
data-slot="tool-subtitle"
|
||||
classList={{
|
||||
[trigger().subtitleClass ?? ""]: !!trigger().subtitleClass,
|
||||
}}
|
||||
>
|
||||
{trigger().subtitle}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={trigger().args?.length}>
|
||||
<For each={trigger().args}>
|
||||
{(arg) => (
|
||||
<span
|
||||
data-slot="tool-arg"
|
||||
classList={{
|
||||
[trigger().argsClass ?? ""]: !!trigger().argsClass,
|
||||
}}
|
||||
>
|
||||
{arg}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={trigger().action}>{trigger().action}</Show>
|
||||
</div>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={true}>{props.trigger as JSX.Element}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={resolved() && !props.hideDetails}>
|
||||
<Collapsible.Arrow />
|
||||
</Show>
|
||||
</div>
|
||||
</Collapsible.Trigger>
|
||||
<Show when={resolved() && !props.hideDetails}>
|
||||
<Collapsible.Content>{resolved()}</Collapsible.Content>
|
||||
</Show>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
export function GenericTool(props: { tool: string; hideDetails?: boolean }) {
|
||||
return <BasicTool icon="mcp" trigger={{ title: props.tool }} hideDetails={props.hideDetails} />
|
||||
}
|
||||
33
packages/ui/src/components/tool-registry.tsx
Normal file
33
packages/ui/src/components/tool-registry.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Component } from "solid-js"
|
||||
|
||||
export interface ToolProps {
|
||||
input: Record<string, any>
|
||||
metadata: Record<string, any>
|
||||
tool: string
|
||||
output?: string
|
||||
hideDetails?: boolean
|
||||
}
|
||||
|
||||
export type ToolComponent = Component<ToolProps>
|
||||
|
||||
const state: Record<
|
||||
string,
|
||||
{
|
||||
name: string
|
||||
render?: ToolComponent
|
||||
}
|
||||
> = {}
|
||||
|
||||
export function registerTool(input: { name: string; render?: ToolComponent }) {
|
||||
state[input.name] = input
|
||||
return input
|
||||
}
|
||||
|
||||
export function getTool(name: string) {
|
||||
return state[name]?.render
|
||||
}
|
||||
|
||||
export const ToolRegistry = {
|
||||
register: registerTool,
|
||||
render: getTool,
|
||||
}
|
||||
Reference in New Issue
Block a user