chore: merge dev into v2 (#35591)
Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
This commit is contained in:
parent
f87998f37f
commit
9e0d3976e1
332 changed files with 24739 additions and 4586 deletions
|
|
@ -230,6 +230,7 @@
|
|||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
text-transform: capitalize;
|
||||
color: var(--task-agent-legacy-color, var(--text-strong));
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-tool-subtitle"] {
|
||||
|
|
@ -247,6 +248,57 @@
|
|||
}
|
||||
}
|
||||
|
||||
body[data-new-layout] {
|
||||
[data-component="task-tool-card"] {
|
||||
padding: 8px 12px 8px 10px;
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
border-color: transparent;
|
||||
background: var(--task-agent-background, light-dark(#fafafa, rgba(250, 250, 250, 0.15)));
|
||||
box-shadow: inset 0 0 0 0.5px var(--task-agent-border, var(--v2-border-border-base));
|
||||
|
||||
[data-component="task-tool-spinner"],
|
||||
[data-component="task-tool-title"] {
|
||||
color: var(--task-agent-color, var(--text-strong));
|
||||
}
|
||||
|
||||
[data-component="task-tool-title"] {
|
||||
height: 20px;
|
||||
font-family: var(--v2-font-family-sans, "Inter", sans-serif);
|
||||
font-style: normal;
|
||||
font-weight: 530;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.04px;
|
||||
flex: none;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-tool-subtitle"] {
|
||||
font-family: var(--v2-font-family-sans, "Inter", sans-serif);
|
||||
font-style: normal;
|
||||
font-weight: 440;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.04px;
|
||||
color: light-dark(var(--v2-text-text-base), #fafafa);
|
||||
font-variation-settings: "slnt" 0;
|
||||
flex: none;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
[data-component="session-progress-indicator-v2"] {
|
||||
color: var(--task-agent-color, light-dark(var(--v2-text-text-base), #ffffff));
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
border-color: transparent;
|
||||
background: var(--task-agent-background, light-dark(#fafafa, rgba(250, 250, 250, 0.15)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body:not([data-new-layout]) {
|
||||
[data-component="tool-trigger"] {
|
||||
[data-slot="basic-tool-tool-spinner"] {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ export interface BasicToolProps {
|
|||
animated?: boolean
|
||||
onSubtitleClick?: () => void
|
||||
onTriggerClick?: JSX.EventHandlerUnion<HTMLElement, MouseEvent>
|
||||
onTriggerKeyDown?: JSX.EventHandlerUnion<HTMLElement, KeyboardEvent>
|
||||
triggerHref?: string
|
||||
triggerAsLink?: boolean
|
||||
clickable?: boolean
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +256,7 @@ export function BasicTool(props: BasicToolProps) {
|
|||
return (
|
||||
<Collapsible open={open()} onOpenChange={handleOpenChange} class="tool-collapsible">
|
||||
<Show
|
||||
when={props.triggerHref}
|
||||
when={props.triggerAsLink || props.triggerHref}
|
||||
fallback={
|
||||
<Collapsible.Trigger
|
||||
data-hide-details={props.hideDetails ? "true" : undefined}
|
||||
|
|
@ -264,16 +266,17 @@ export function BasicTool(props: BasicToolProps) {
|
|||
</Collapsible.Trigger>
|
||||
}
|
||||
>
|
||||
{(href) => (
|
||||
<Collapsible.Trigger
|
||||
as="a"
|
||||
href={href()}
|
||||
data-hide-details={props.hideDetails ? "true" : undefined}
|
||||
onClick={props.onTriggerClick}
|
||||
>
|
||||
{trigger()}
|
||||
</Collapsible.Trigger>
|
||||
)}
|
||||
<Collapsible.Trigger
|
||||
as="a"
|
||||
href={props.triggerHref}
|
||||
role={!props.triggerHref && props.clickable ? "button" : undefined}
|
||||
tabIndex={!props.triggerHref && props.clickable ? 0 : undefined}
|
||||
data-hide-details={props.hideDetails ? "true" : undefined}
|
||||
onClick={props.onTriggerClick}
|
||||
onKeyDown={props.onTriggerKeyDown}
|
||||
>
|
||||
{trigger()}
|
||||
</Collapsible.Trigger>
|
||||
</Show>
|
||||
<Show when={props.animated && hasChildren() && !props.hideDetails}>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { FileContent } from "@opencode-ai/sdk/v2"
|
||||
import { createEffect, createMemo, createResource, Match, on, Show, Switch, type JSX } from "solid-js"
|
||||
import { createEffect, createMemo, Match, on, onCleanup, Show, Switch, untrack, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useI18n } from "@opencode-ai/ui/context/i18n"
|
||||
import {
|
||||
dataUrlFromMediaValue,
|
||||
|
|
@ -30,6 +31,13 @@ function mediaValue(cfg: FileMediaOptions, mode: "image" | "audio") {
|
|||
|
||||
export function FileMedia(props: { media?: FileMediaOptions; fallback: () => JSX.Element }) {
|
||||
const i18n = useI18n()
|
||||
const [remote, setRemote] = createStore<{
|
||||
key?: string
|
||||
loading?: boolean
|
||||
error?: boolean
|
||||
src?: string
|
||||
mime?: string
|
||||
}>({})
|
||||
const cfg = () => props.media
|
||||
const kind = createMemo(() => {
|
||||
const media = cfg()
|
||||
|
|
@ -81,50 +89,66 @@ export function FileMedia(props: { media?: FileMediaOptions; fallback: () => JSX
|
|||
}
|
||||
})
|
||||
|
||||
const [loaded] = createResource(request, async (input) => {
|
||||
return input.readFile(input.path).then(
|
||||
createEffect(() => {
|
||||
const input = request()
|
||||
if (!input) {
|
||||
setRemote({ key: undefined, loading: false, error: false, src: undefined, mime: undefined })
|
||||
return
|
||||
}
|
||||
|
||||
let active = true
|
||||
// Keep the previous media visible while re-reading the same file (e.g. a vcs
|
||||
// diff refresh); only a key change resets to the loading placeholder.
|
||||
if (untrack(() => remote.key) === input.key) setRemote({ loading: true, error: false })
|
||||
else setRemote({ key: input.key, loading: true, error: false, src: undefined, mime: undefined })
|
||||
void input.readFile(input.path).then(
|
||||
(result) => {
|
||||
if (!active) return
|
||||
const src = dataUrlFromMediaValue(result as any, input.kind)
|
||||
if (!src) {
|
||||
input.onError?.({ kind: input.kind })
|
||||
return { key: input.key, error: true as const }
|
||||
setRemote({ key: input.key, loading: false, error: true, src: undefined, mime: undefined })
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
setRemote({
|
||||
key: input.key,
|
||||
loading: false,
|
||||
error: false,
|
||||
src,
|
||||
mime: input.kind === "audio" ? normalizeMimeType(result?.mimeType) : undefined,
|
||||
}
|
||||
})
|
||||
},
|
||||
() => {
|
||||
if (!active) return
|
||||
input.onError?.({ kind: input.kind })
|
||||
return { key: input.key, error: true as const }
|
||||
setRemote({ key: input.key, loading: false, error: true, src: undefined, mime: undefined })
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const remote = createMemo(() => {
|
||||
const input = request()
|
||||
const value = loaded()
|
||||
if (!input || !value || value.key !== input.key) return
|
||||
return value
|
||||
onCleanup(() => {
|
||||
active = false
|
||||
})
|
||||
})
|
||||
|
||||
const src = createMemo(() => {
|
||||
const value = remote()
|
||||
return direct() ?? (value && "src" in value ? value.src : undefined)
|
||||
const input = request()
|
||||
if (!input || remote.key !== input.key || remote.error) return direct()
|
||||
return direct() ?? remote.src
|
||||
})
|
||||
const status = createMemo(() => {
|
||||
if (direct()) return "ready" as const
|
||||
if (!request()) return "idle" as const
|
||||
if (loaded.loading) return "loading" as const
|
||||
if (remote()?.error) return "error" as const
|
||||
const input = request()
|
||||
if (!input) return "idle" as const
|
||||
if (remote.key !== input.key || remote.loading) return "loading" as const
|
||||
if (remote.error) return "error" as const
|
||||
if (src()) return "ready" as const
|
||||
return "idle" as const
|
||||
})
|
||||
const audioMime = createMemo(() => {
|
||||
const value = remote()
|
||||
return value && "mime" in value ? value.mime : undefined
|
||||
const input = request()
|
||||
if (!input || remote.key !== input.key) return
|
||||
return remote.mime
|
||||
})
|
||||
|
||||
const svgSource = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -456,11 +456,18 @@ function useAnnotationRerender<A>(opts: {
|
|||
current: () => AnnotationTarget<A> | undefined
|
||||
annotations: () => A[]
|
||||
}) {
|
||||
const applied = new WeakSet<AnnotationTarget<A>>()
|
||||
createEffect(() => {
|
||||
opts.viewer.rendered()
|
||||
const active = opts.current()
|
||||
if (!active) return
|
||||
active.setLineAnnotations(opts.annotations())
|
||||
const annotations = opts.annotations()
|
||||
// renderViewer always draws with empty annotations, so skip the extra rerender
|
||||
// when this instance has nothing applied and nothing to apply.
|
||||
if (annotations.length === 0 && !applied.has(active)) return
|
||||
if (annotations.length === 0) applied.delete(active)
|
||||
else applied.add(active)
|
||||
active.setLineAnnotations(annotations)
|
||||
active.rerender()
|
||||
requestAnimationFrame(() => opts.viewer.find.refresh({ reset: true }))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ type HoverCommentLine = {
|
|||
side?: "additions" | "deletions"
|
||||
}
|
||||
|
||||
type LineCommentStateProps<T> = {
|
||||
export type LineCommentStateProps<T> = {
|
||||
opened: Accessor<T | null>
|
||||
setOpened: (id: T | null) => void
|
||||
selected: Accessor<SelectedLineRange | null>
|
||||
|
|
@ -45,7 +45,7 @@ type LineCommentStateProps<T> = {
|
|||
hoverSelected?: (range: SelectedLineRange) => void
|
||||
}
|
||||
|
||||
type LineCommentShape = {
|
||||
export type LineCommentShape = {
|
||||
id: string
|
||||
selection: SelectedLineRange
|
||||
comment: string
|
||||
|
|
@ -95,9 +95,14 @@ type DraftProps = {
|
|||
submitLabel?: string
|
||||
}
|
||||
|
||||
export function createLineCommentAnnotationRenderer<T>(props: {
|
||||
renderComment: (comment: T) => CommentProps
|
||||
renderDraft: (range: SelectedLineRange) => DraftProps
|
||||
// Generic host machinery shared by the v1 and v2 annotation renderers: each
|
||||
// annotation key gets a detached DOM host with its own Solid root, updated in
|
||||
// place through a signal so Pierre can reparent the host without re-rendering.
|
||||
export function createLineCommentAnnotationRenderer<T, C, D>(props: {
|
||||
renderComment: (comment: T) => C
|
||||
renderDraft: (range: SelectedLineRange) => D
|
||||
commentElement: (view: Accessor<C>) => JSX.Element
|
||||
draftElement: (view: Accessor<D>) => JSX.Element
|
||||
}) {
|
||||
const nodes = new Map<
|
||||
string,
|
||||
|
|
@ -123,37 +128,7 @@ export function createLineCommentAnnotationRenderer<T>(props: {
|
|||
if (next.kind !== "comment") return props.renderComment(active.comment)
|
||||
return props.renderComment(next.comment)
|
||||
})
|
||||
return (
|
||||
<Show
|
||||
when={view().editor}
|
||||
fallback={
|
||||
<LineComment
|
||||
inline
|
||||
id={view().id}
|
||||
open={view().open}
|
||||
comment={view().comment}
|
||||
selection={view().selection}
|
||||
actions={view().actions}
|
||||
onClick={view().onClick}
|
||||
onMouseEnter={view().onMouseEnter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LineCommentEditor
|
||||
inline
|
||||
id={view().id}
|
||||
value={view().editor!.value}
|
||||
selection={view().editor!.selection}
|
||||
onInput={view().editor!.onInput}
|
||||
onCancel={view().editor!.onCancel}
|
||||
onSubmit={view().editor!.onSubmit}
|
||||
onPopoverFocusOut={view().editor!.onPopoverFocusOut}
|
||||
cancelLabel={view().editor!.cancelLabel}
|
||||
submitLabel={view().editor!.submitLabel}
|
||||
mention={view().editor!.mention}
|
||||
/>
|
||||
</Show>
|
||||
)
|
||||
return props.commentElement(view)
|
||||
}
|
||||
|
||||
const view = createMemo(() => {
|
||||
|
|
@ -161,18 +136,7 @@ export function createLineCommentAnnotationRenderer<T>(props: {
|
|||
if (next.kind !== "draft") return props.renderDraft(active.range)
|
||||
return props.renderDraft(next.range)
|
||||
})
|
||||
return (
|
||||
<LineCommentEditor
|
||||
inline
|
||||
value={view().value}
|
||||
selection={view().selection}
|
||||
onInput={view().onInput}
|
||||
onCancel={view().onCancel}
|
||||
onSubmit={view().onSubmit}
|
||||
onPopoverFocusOut={view().onPopoverFocusOut}
|
||||
mention={view().mention}
|
||||
/>
|
||||
)
|
||||
return props.draftElement(view)
|
||||
}, host)
|
||||
|
||||
const node = { host, dispose, setMeta: setCurrent }
|
||||
|
|
@ -205,6 +169,55 @@ export function createLineCommentAnnotationRenderer<T>(props: {
|
|||
return { render, reconcile, cleanup }
|
||||
}
|
||||
|
||||
function lineCommentElement(view: Accessor<CommentProps>) {
|
||||
return (
|
||||
<Show
|
||||
when={view().editor}
|
||||
fallback={
|
||||
<LineComment
|
||||
inline
|
||||
id={view().id}
|
||||
open={view().open}
|
||||
comment={view().comment}
|
||||
selection={view().selection}
|
||||
actions={view().actions}
|
||||
onClick={view().onClick}
|
||||
onMouseEnter={view().onMouseEnter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LineCommentEditor
|
||||
inline
|
||||
id={view().id}
|
||||
value={view().editor!.value}
|
||||
selection={view().editor!.selection}
|
||||
onInput={view().editor!.onInput}
|
||||
onCancel={view().editor!.onCancel}
|
||||
onSubmit={view().editor!.onSubmit}
|
||||
onPopoverFocusOut={view().editor!.onPopoverFocusOut}
|
||||
cancelLabel={view().editor!.cancelLabel}
|
||||
submitLabel={view().editor!.submitLabel}
|
||||
mention={view().editor!.mention}
|
||||
/>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function lineCommentDraftElement(view: Accessor<DraftProps>) {
|
||||
return (
|
||||
<LineCommentEditor
|
||||
inline
|
||||
value={view().value}
|
||||
selection={view().selection}
|
||||
onInput={view().onInput}
|
||||
onCancel={view().onCancel}
|
||||
onSubmit={view().onSubmit}
|
||||
onPopoverFocusOut={view().onPopoverFocusOut}
|
||||
mention={view().mention}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
|
||||
const [state, setState] = createStore({
|
||||
draft: "",
|
||||
|
|
@ -314,7 +327,9 @@ export function createLineCommentController<T extends LineCommentShape>(
|
|||
): {
|
||||
note: ReturnType<typeof createLineCommentState<string>>
|
||||
annotations: Accessor<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>
|
||||
renderAnnotation: ReturnType<typeof createManagedLineCommentAnnotationRenderer<T>>["renderAnnotation"]
|
||||
renderAnnotation: ReturnType<
|
||||
typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>
|
||||
>["renderAnnotation"]
|
||||
renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer>
|
||||
onLineSelected: (range: SelectedLineRange | null) => void
|
||||
onLineSelectionEnd: (range: SelectedLineRange | null) => void
|
||||
|
|
@ -324,7 +339,9 @@ export function createLineCommentController<T extends LineCommentShape>(
|
|||
): {
|
||||
note: ReturnType<typeof createLineCommentState<string>>
|
||||
annotations: Accessor<LineCommentAnnotation<T>[]>
|
||||
renderAnnotation: ReturnType<typeof createManagedLineCommentAnnotationRenderer<T>>["renderAnnotation"]
|
||||
renderAnnotation: ReturnType<
|
||||
typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>
|
||||
>["renderAnnotation"]
|
||||
renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer>
|
||||
onLineSelected: (range: SelectedLineRange | null) => void
|
||||
onLineSelectionEnd: (range: SelectedLineRange | null) => void
|
||||
|
|
@ -353,8 +370,10 @@ export function createLineCommentController<T extends LineCommentShape>(
|
|||
draftKey: props.draftKey,
|
||||
})
|
||||
|
||||
const { renderAnnotation } = createManagedLineCommentAnnotationRenderer<T>({
|
||||
const { renderAnnotation } = createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>({
|
||||
annotations,
|
||||
commentElement: lineCommentElement,
|
||||
draftElement: lineCommentDraftElement,
|
||||
renderComment: (comment) => {
|
||||
const edit = () => note.openEditor(comment.id, comment.selection, comment.comment)
|
||||
const remove = () => {
|
||||
|
|
@ -471,77 +490,111 @@ export function createLineCommentAnnotations<T>(
|
|||
const line = (range: SelectedLineRange) => Math.max(range.start, range.end)
|
||||
|
||||
if ("getSide" in props) {
|
||||
return createMemo<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>(() => {
|
||||
return createMemo<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>(
|
||||
() => {
|
||||
const list = props.comments().map((comment) => {
|
||||
const range = props.getCommentSelection(comment)
|
||||
return {
|
||||
side: props.getSide(range),
|
||||
lineNumber: line(range),
|
||||
metadata: {
|
||||
kind: "comment",
|
||||
key: `comment:${props.getCommentId(comment)}`,
|
||||
comment,
|
||||
} satisfies LineCommentAnnotationMeta<T>,
|
||||
}
|
||||
})
|
||||
|
||||
const range = props.draftRange()
|
||||
if (!range) return list
|
||||
|
||||
return [
|
||||
...list,
|
||||
{
|
||||
side: props.getSide(range),
|
||||
lineNumber: line(range),
|
||||
metadata: {
|
||||
kind: "draft",
|
||||
key: `draft:${props.draftKey()}`,
|
||||
range,
|
||||
} satisfies LineCommentAnnotationMeta<T>,
|
||||
},
|
||||
]
|
||||
},
|
||||
[],
|
||||
// Stable identity for unchanged annotations avoids no-op diff rerenders downstream.
|
||||
{ equals: sameAnnotationLists },
|
||||
)
|
||||
}
|
||||
|
||||
return createMemo<LineCommentAnnotation<T>[]>(
|
||||
() => {
|
||||
const list = props.comments().map((comment) => {
|
||||
const range = props.getCommentSelection(comment)
|
||||
return {
|
||||
side: props.getSide(range),
|
||||
const entry: LineCommentAnnotation<T> = {
|
||||
lineNumber: line(range),
|
||||
metadata: {
|
||||
kind: "comment",
|
||||
key: `comment:${props.getCommentId(comment)}`,
|
||||
comment,
|
||||
} satisfies LineCommentAnnotationMeta<T>,
|
||||
},
|
||||
}
|
||||
|
||||
return entry
|
||||
})
|
||||
|
||||
const range = props.draftRange()
|
||||
if (!range) return list
|
||||
|
||||
return [
|
||||
...list,
|
||||
{
|
||||
side: props.getSide(range),
|
||||
lineNumber: line(range),
|
||||
metadata: {
|
||||
kind: "draft",
|
||||
key: `draft:${props.draftKey()}`,
|
||||
range,
|
||||
} satisfies LineCommentAnnotationMeta<T>,
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
return createMemo<LineCommentAnnotation<T>[]>(() => {
|
||||
const list = props.comments().map((comment) => {
|
||||
const range = props.getCommentSelection(comment)
|
||||
const entry: LineCommentAnnotation<T> = {
|
||||
const draft: LineCommentAnnotation<T> = {
|
||||
lineNumber: line(range),
|
||||
metadata: {
|
||||
kind: "comment",
|
||||
key: `comment:${props.getCommentId(comment)}`,
|
||||
comment,
|
||||
kind: "draft",
|
||||
key: `draft:${props.draftKey()}`,
|
||||
range,
|
||||
},
|
||||
}
|
||||
|
||||
return entry
|
||||
})
|
||||
return [...list, draft]
|
||||
},
|
||||
[],
|
||||
{ equals: sameAnnotationLists },
|
||||
)
|
||||
}
|
||||
|
||||
const range = props.draftRange()
|
||||
if (!range) return list
|
||||
type AnnotationListItem = {
|
||||
lineNumber: number
|
||||
side?: unknown
|
||||
metadata: { kind: string; key: string; comment?: unknown; range?: unknown }
|
||||
}
|
||||
|
||||
const draft: LineCommentAnnotation<T> = {
|
||||
lineNumber: line(range),
|
||||
metadata: {
|
||||
kind: "draft",
|
||||
key: `draft:${props.draftKey()}`,
|
||||
range,
|
||||
},
|
||||
}
|
||||
|
||||
return [...list, draft]
|
||||
function sameAnnotationLists(previous: AnnotationListItem[], next: AnnotationListItem[]) {
|
||||
if (previous.length !== next.length) return false
|
||||
return previous.every((item, index) => {
|
||||
const other = next[index]!
|
||||
return (
|
||||
item.lineNumber === other.lineNumber &&
|
||||
item.side === other.side &&
|
||||
item.metadata.kind === other.metadata.kind &&
|
||||
item.metadata.key === other.metadata.key &&
|
||||
item.metadata.comment === other.metadata.comment &&
|
||||
item.metadata.range === other.metadata.range
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function createManagedLineCommentAnnotationRenderer<T>(props: {
|
||||
export function createManagedLineCommentAnnotationRenderer<T, C, D>(props: {
|
||||
annotations: Accessor<LineCommentAnnotation<T>[]>
|
||||
renderComment: (comment: T) => CommentProps
|
||||
renderDraft: (range: SelectedLineRange) => DraftProps
|
||||
renderComment: (comment: T) => C
|
||||
renderDraft: (range: SelectedLineRange) => D
|
||||
commentElement: (view: Accessor<C>) => JSX.Element
|
||||
draftElement: (view: Accessor<D>) => JSX.Element
|
||||
}) {
|
||||
const renderer = createLineCommentAnnotationRenderer<T>({
|
||||
const renderer = createLineCommentAnnotationRenderer<T, C, D>({
|
||||
renderComment: props.renderComment,
|
||||
renderDraft: props.renderDraft,
|
||||
commentElement: props.commentElement,
|
||||
draftElement: props.draftElement,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ import { animate } from "motion"
|
|||
import { useLocation } from "@solidjs/router"
|
||||
import { attached, inline, kind } from "./message-file"
|
||||
import { readPartText } from "./message-part-text"
|
||||
import { SessionProgressIndicatorV2 } from "../v2/components/session-progress-indicator-v2"
|
||||
|
||||
async function writeClipboard(text: string): Promise<boolean> {
|
||||
const body = typeof document === "undefined" ? undefined : document.body
|
||||
|
|
@ -370,6 +371,44 @@ const agentTones: Record<string, string> = {
|
|||
plan: "var(--icon-agent-plan-base)",
|
||||
}
|
||||
|
||||
const v2AgentTones: Record<string, { color: string; border: string; background: string }> = {
|
||||
build: {
|
||||
color: "var(--v2-agent-build-solid)",
|
||||
border: "var(--v2-agent-build-border)",
|
||||
background: "var(--v2-agent-build-background)",
|
||||
},
|
||||
explore: {
|
||||
color: "var(--v2-agent-explore-solid)",
|
||||
border: "var(--v2-agent-explore-border)",
|
||||
background: "var(--v2-agent-explore-background)",
|
||||
},
|
||||
plan: {
|
||||
color: "var(--v2-agent-plan-solid)",
|
||||
border: "var(--v2-agent-plan-border)",
|
||||
background: "var(--v2-agent-plan-background)",
|
||||
},
|
||||
}
|
||||
|
||||
const agentThemeColors: Record<string, string> = {
|
||||
primary: "var(--text-interactive-base)",
|
||||
secondary: "var(--text-base)",
|
||||
accent: "var(--icon-info-base)",
|
||||
success: "var(--icon-success-base)",
|
||||
warning: "var(--icon-warning-base)",
|
||||
error: "var(--icon-critical-base)",
|
||||
info: "var(--icon-info-base)",
|
||||
}
|
||||
|
||||
const v2AgentThemeColors: Record<string, string> = {
|
||||
primary: "var(--v2-text-text-accent)",
|
||||
secondary: "var(--v2-text-text-muted)",
|
||||
accent: "var(--v2-icon-icon-accent)",
|
||||
success: "var(--v2-state-fg-success)",
|
||||
warning: "var(--v2-state-fg-warning)",
|
||||
error: "var(--v2-state-fg-danger)",
|
||||
info: "var(--v2-state-fg-info)",
|
||||
}
|
||||
|
||||
const agentPalette = [
|
||||
"var(--icon-agent-ask-base)",
|
||||
"var(--icon-agent-build-base)",
|
||||
|
|
@ -394,16 +433,31 @@ function tone(name: string) {
|
|||
function taskAgent(
|
||||
raw: unknown,
|
||||
list?: readonly { name: string; color?: string }[],
|
||||
): { name?: string; color?: string } {
|
||||
): { name?: string; color?: string; v2Color?: string; border?: string; background?: string } {
|
||||
if (typeof raw !== "string" || !raw) return {}
|
||||
const key = raw.toLowerCase()
|
||||
const item = list?.find((entry) => entry.name === raw || entry.name.toLowerCase() === key)
|
||||
const v2Tone = item?.color ? undefined : v2AgentTones[key]
|
||||
const color = agentColor(item?.color, agentThemeColors) ?? agentTones[key] ?? tone(key)
|
||||
const v2Color = agentColor(item?.color, v2AgentThemeColors) ?? v2Tone?.color ?? color
|
||||
return {
|
||||
name: item?.name ?? `${raw[0]!.toUpperCase()}${raw.slice(1)}`,
|
||||
color: item?.color ?? agentTones[key] ?? tone(key),
|
||||
color,
|
||||
v2Color,
|
||||
border: v2Tone?.border ?? `color-mix(in srgb, ${v2Color} 48%, transparent)`,
|
||||
background: v2Tone?.background ?? `color-mix(in srgb, ${v2Color} 12%, transparent)`,
|
||||
}
|
||||
}
|
||||
|
||||
function agentColor(value: string | undefined, themeColors: Record<string, string>) {
|
||||
if (!value) return
|
||||
return themeColors[value] ?? value
|
||||
}
|
||||
|
||||
function newLayout() {
|
||||
return typeof document !== "undefined" && document.body.hasAttribute("data-new-layout")
|
||||
}
|
||||
|
||||
function webSearchProviderLabel(provider: unknown) {
|
||||
if (provider === "parallel") return "Parallel Web Search"
|
||||
if (provider === "exa") return "Exa Web Search"
|
||||
|
|
@ -1000,16 +1054,24 @@ export function AssistantMessageDisplay(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean; onSizeChange?: () => void }) {
|
||||
export function ContextToolGroup(props: {
|
||||
parts: ToolPart[]
|
||||
busy?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onSizeChange?: () => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const [localOpen, setLocalOpen] = createSignal(false)
|
||||
const open = () => props.open ?? localOpen()
|
||||
const pending = createMemo(
|
||||
() =>
|
||||
!!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
|
||||
)
|
||||
const summary = createMemo(() => contextToolSummary(props.parts))
|
||||
const handleOpenChange = (value: boolean) => {
|
||||
setOpen(value)
|
||||
if (props.open === undefined) setLocalOpen(value)
|
||||
props.onOpenChange?.(value)
|
||||
props.onSizeChange?.()
|
||||
}
|
||||
|
||||
|
|
@ -1718,7 +1780,13 @@ ToolRegistry.register({
|
|||
trigger={{ title: i18n.t("ui.tool.list"), subtitle: getDirectory(props.input.path || "/") }}
|
||||
>
|
||||
<Show when={props.output}>
|
||||
<div data-component="tool-output" data-scrollable>
|
||||
<div
|
||||
data-component="tool-output"
|
||||
data-scrollable
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
||||
>
|
||||
<Markdown text={props.output!} />
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -1742,7 +1810,13 @@ ToolRegistry.register({
|
|||
}}
|
||||
>
|
||||
<Show when={props.output}>
|
||||
<div data-component="tool-output" data-scrollable>
|
||||
<div
|
||||
data-component="tool-output"
|
||||
data-scrollable
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
||||
>
|
||||
<Markdown text={props.output!} />
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -1769,7 +1843,13 @@ ToolRegistry.register({
|
|||
}}
|
||||
>
|
||||
<Show when={props.output}>
|
||||
<div data-component="tool-output" data-scrollable>
|
||||
<div
|
||||
data-component="tool-output"
|
||||
data-scrollable
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
||||
>
|
||||
<Markdown text={props.output!} />
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -1864,6 +1944,9 @@ ToolRegistry.register({
|
|||
const agent = createMemo(() => taskAgent(props.input.subagent_type, data.store.agent))
|
||||
const title = createMemo(() => agent().name ?? i18n.t("ui.tool.agent.default"))
|
||||
const tone = createMemo(() => agent().color)
|
||||
const v2Tone = createMemo(() => agent().v2Color)
|
||||
const border = createMemo(() => agent().border)
|
||||
const background = createMemo(() => agent().background)
|
||||
const subtitle = createMemo(() => {
|
||||
const value =
|
||||
typeof props.input.description === "string" && props.input.description
|
||||
|
|
@ -1895,19 +1978,35 @@ ToolRegistry.register({
|
|||
event.preventDefault()
|
||||
open()
|
||||
}
|
||||
const navigateKey = (event: KeyboardEvent) => {
|
||||
if (!clickable() || href()) return
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
open()
|
||||
}
|
||||
|
||||
const trigger = () => (
|
||||
<div data-component="task-tool-card">
|
||||
<div
|
||||
data-component="task-tool-card"
|
||||
style={{
|
||||
"--task-agent-color": v2Tone(),
|
||||
"--task-agent-legacy-color": tone(),
|
||||
"--task-agent-border": border(),
|
||||
"--task-agent-background": background(),
|
||||
}}
|
||||
>
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
<div data-slot="basic-tool-tool-info-main">
|
||||
<Show when={running()}>
|
||||
<span data-component="task-tool-spinner" style={{ color: tone() ?? "var(--icon-interactive-base)" }}>
|
||||
<Spinner />
|
||||
<Show when={newLayout()} fallback={<Spinner />}>
|
||||
<SessionProgressIndicatorV2
|
||||
style={{ color: v2Tone() ?? "light-dark(var(--v2-text-text-base), #ffffff)" }}
|
||||
/>
|
||||
</Show>
|
||||
</span>
|
||||
</Show>
|
||||
<span data-component="task-tool-title" style={{ color: tone() ?? "var(--text-strong)" }}>
|
||||
{title()}
|
||||
</span>
|
||||
<span data-component="task-tool-title">{title()}</span>
|
||||
<Show when={subtitle()}>
|
||||
<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
|
||||
</Show>
|
||||
|
|
@ -1927,9 +2026,11 @@ ToolRegistry.register({
|
|||
status={props.status}
|
||||
trigger={trigger()}
|
||||
hideDetails
|
||||
triggerAsLink
|
||||
triggerHref={href()}
|
||||
clickable={clickable()}
|
||||
onTriggerClick={navigate}
|
||||
onTriggerKeyDown={navigateKey}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
|
@ -1987,7 +2088,13 @@ ToolRegistry.register({
|
|||
/>
|
||||
</TooltipV2>
|
||||
</div>
|
||||
<div data-slot="bash-scroll" data-scrollable>
|
||||
<div
|
||||
data-slot="bash-scroll"
|
||||
data-scrollable
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
aria-label={i18n.t("ui.scrollView.ariaLabel")}
|
||||
>
|
||||
<pre data-slot="bash-pre">
|
||||
<code>{text()}</code>
|
||||
</pre>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue