chore: merge dev into v2

This commit is contained in:
Aiden Cline 2026-07-09 20:04:04 +00:00
commit 10d9d87bc6
129 changed files with 3722 additions and 933 deletions

View file

@ -185,6 +185,14 @@
background-color 0.15s ease,
color 0.15s ease;
[data-component="task-tool-surface"] {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1 1 auto;
}
[data-slot="basic-tool-tool-info-structured"] {
flex: 1 1 auto;
min-width: 0;
@ -250,16 +258,30 @@
body[data-new-layout] {
[data-component="task-tool-card"] {
padding: 8px 12px 8px 10px;
border-radius: 8px;
gap: 8px;
padding: 0;
border-radius: 0;
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));
background: transparent;
box-shadow: none;
[data-component="task-tool-surface"] {
padding: 8px 12px;
border-radius: 8px;
background: var(--v2-background-bg-layer-01);
}
[data-component="task-tool-spinner"],
[data-component="task-tool-title"] {
color: var(--task-agent-color, var(--text-strong));
[data-component="task-tool-icon"] {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
[data-component="icon"] {
color: var(--task-agent-color, var(--v2-icon-icon-base));
}
}
[data-component="task-tool-title"] {
@ -270,6 +292,7 @@ body[data-new-layout] {
font-size: 13px;
line-height: 20px;
letter-spacing: -0.04px;
color: var(--v2-text-text-base);
flex: none;
flex-grow: 0;
}
@ -281,7 +304,7 @@ body[data-new-layout] {
font-size: 13px;
line-height: 20px;
letter-spacing: -0.04px;
color: light-dark(var(--v2-text-text-base), #fafafa);
color: var(--v2-text-text-base);
font-variation-settings: "slnt" 0;
flex: none;
flex-grow: 0;
@ -294,7 +317,11 @@ body[data-new-layout] {
&:hover,
&:focus-visible {
border-color: transparent;
background: var(--task-agent-background, light-dark(#fafafa, rgba(250, 250, 250, 0.15)));
background: transparent;
[data-component="task-tool-surface"] {
background: var(--v2-background-bg-layer-01);
}
}
}
}

View file

@ -369,22 +369,12 @@ 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 v2AgentTones: Record<string, string> = {
build: "var(--v2-agent-build-solid)",
explore: "var(--v2-agent-explore-solid)",
plan: "var(--v2-agent-plan-solid)",
review: "var(--v2-agent-review-solid)",
writer: "var(--v2-agent-writer-solid)",
}
const agentThemeColors: Record<string, string> = {
@ -431,19 +421,17 @@ function tone(name: string) {
function taskAgent(
raw: unknown,
list?: readonly { name: string; color?: string }[],
): { name?: string; color?: string; v2Color?: string; border?: string; background?: string } {
): { name?: string; color?: string; v2Color?: 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
const v2Color = agentColor(item?.color, v2AgentThemeColors) ?? v2Tone ?? color
return {
name: item?.name ?? `${raw[0]!.toUpperCase()}${raw.slice(1)}`,
color,
v2Color,
border: v2Tone?.border ?? `color-mix(in srgb, ${v2Color} 48%, transparent)`,
background: v2Tone?.background ?? `color-mix(in srgb, ${v2Color} 12%, transparent)`,
}
}
@ -1934,8 +1922,6 @@ ToolRegistry.register({
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
@ -1980,25 +1966,34 @@ ToolRegistry.register({
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)" }}>
<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">{title()}</span>
<Show when={subtitle()}>
<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
</Show>
<div data-component="task-tool-surface">
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<Show
when={running()}
fallback={
<Show when={newLayout()}>
<span data-component="task-tool-icon">
<Icon name="subagent" size="small" />
</span>
</Show>
}
>
<span data-component="task-tool-spinner" style={{ color: tone() ?? "var(--icon-interactive-base)" }}>
<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">{title()}</span>
<Show when={subtitle()}>
<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
</Show>
</div>
</div>
</div>
<Show when={clickable()}>

View file

@ -359,6 +359,34 @@ const TOOL_SAMPLES = {
title: "Agent (Explore)",
metadata: { sessionId: "sub-session-1" },
},
"task build": {
tool: "task",
input: { description: "Implement the settings page", subagent_type: "build", prompt: "Build it" },
output: "Implemented the settings page.",
title: "Agent (Build)",
metadata: { sessionId: "sub-session-2" },
},
"task plan": {
tool: "task",
input: { description: "Plan the migration strategy", subagent_type: "plan", prompt: "Plan it" },
output: "Drafted a 4-step migration plan.",
title: "Agent (Plan)",
metadata: { sessionId: "sub-session-3" },
},
"task themed": {
tool: "task",
input: { description: "Review the diff for regressions", subagent_type: "review", prompt: "Review it" },
output: "No regressions found.",
title: "Agent (Review)",
metadata: { sessionId: "sub-session-4" },
},
"task fallback": {
tool: "task",
input: { description: "Write release notes", subagent_type: "writer", prompt: "Write them" },
output: "Release notes drafted.",
title: "Agent (Writer)",
metadata: { sessionId: "sub-session-5" },
},
webfetch: {
tool: "webfetch",
input: { url: "https://solidjs.com/docs/latest/api" },
@ -1197,6 +1225,7 @@ function Playground() {
const data = createMemo(() => ({
session: [session()],
agent: [{ name: "build" }, { name: "plan" }, { name: "explore" }, { name: "review" }],
session_status: {},
session_diff: {},
message: { [session().id]: state.messages },
@ -1323,6 +1352,19 @@ function Playground() {
])
}
const addAgentTasksTurn = () => {
addFullTurn("Run subagents with every agent type", [
toolPart(TOOL_SAMPLES.task),
toolPart(TOOL_SAMPLES["task build"]),
toolPart(TOOL_SAMPLES["task plan"]),
toolPart(TOOL_SAMPLES["task themed"]),
toolPart(TOOL_SAMPLES["task fallback"]),
toolPart(TOOL_SAMPLES.task, "running"),
toolPart(TOOL_SAMPLES["task build"], "running"),
toolPart(TOOL_SAMPLES["task plan"], "running"),
])
}
const addKitchenSink = () => {
// User message variants
addUser("short")
@ -1708,6 +1750,9 @@ function Playground() {
<button style={btnStyle} onClick={addReasoningFullTurn}>
full turn
</button>
<button style={btnStyle} onClick={addAgentTasksTurn}>
agent tasks
</button>
<button style={btnAccent} onClick={addKitchenSink}>
kitchen sink
</button>
@ -1947,7 +1992,7 @@ function Playground() {
{/* Main area: timeline preview */}
<div
ref={previewRef!}
style={{ flex: "1", overflow: "auto", "min-width": "0", "background-color": "var(--background-stronger)" }}
style={{ flex: "1", overflow: "auto", "min-width": "0", "background-color": "var(--v2-background-bg-base)" }}
>
<DataProvider data={data()} directory="/project">
<FileComponentProvider component={FileStub}>

View file

@ -0,0 +1,43 @@
import { Show, type JSX, type ParentProps } from "solid-js"
import "./session-review-v2.css"
export function SessionFilePanelV2(props: {
sidebar?: JSX.Element
toolbar: boolean
toolbarStart?: JSX.Element
toolbarEnd?: JSX.Element
children?: JSX.Element
}) {
return (
<div data-component="session-review-v2">
<div data-slot="session-review-v2-body">
{props.sidebar}
<div data-slot="session-review-v2-preview">
<Show when={props.toolbar}>
<div data-slot="session-review-v2-toolbar">
<div data-slot="session-review-v2-toolbar-group" class="session-review-v2-toolbar-group--start">
{props.toolbarStart}
</div>
<Show when={props.toolbarEnd}>
{(toolbar) => (
<div data-slot="session-review-v2-toolbar-group" class="session-review-v2-toolbar-group--segments">
{toolbar()}
</div>
)}
</Show>
</div>
</Show>
{props.children}
</div>
</div>
</div>
)
}
export function SessionFilePanelV2Title(props: ParentProps) {
return <div data-slot="session-review-v2-toolbar-title">{props.children}</div>
}
export function SessionFilePanelV2Empty(props: ParentProps) {
return <div data-slot="session-review-v2-empty">{props.children}</div>
}

View file

@ -12,7 +12,7 @@ import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Show, createEffect, createMemo, createSignal, type JSX } from "solid-js"
import { getWorkerPool } from "../../pierre/worker"
import "./session-review-v2.css"
import { SessionFilePanelV2, SessionFilePanelV2Empty } from "./session-file-panel-v2"
export const SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT = 240
export const SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN = 200
@ -45,6 +45,11 @@ export type SessionReviewV2SidebarProps = {
filter: string
onFilterChange: (value: string) => void
onFilterKeyDown?: JSX.EventHandlerUnion<HTMLInputElement, KeyboardEvent>
filterAutofocus?: boolean
filterRef?: (element: HTMLInputElement) => void
filterControls?: string
filterActiveDescendant?: string
filterExpanded?: boolean
width?: number
onWidthChange?: (width: number) => void
minWidth?: number
@ -69,55 +74,62 @@ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) {
return (
<div data-component="session-review-v2-sidebar-root">
<aside
data-slot="session-review-v2-sidebar"
data-resizing={resizing() ? "" : undefined}
aria-hidden={!props.open}
inert={!props.open}
style={{ width: props.open ? `${width()}px` : "0px" }}
>
<div data-slot="session-review-v2-sidebar-header">
<div data-slot="session-review-v2-sidebar-title">{props.title}</div>
{props.stats}
</div>
<div data-slot="session-review-v2-sidebar-filter">
<TextInputV2
type="search"
value={props.filter}
onInput={(event) => props.onFilterChange(event.currentTarget.value)}
onKeyDown={props.onFilterKeyDown}
showClearButton={props.filter.length > 0}
clearLabel={i18n.t("ui.list.clearFilter")}
onClearClick={() => props.onFilterChange("")}
placeholder={i18n.t("ui.sessionReviewV2.filterFiles")}
aria-label={i18n.t("ui.sessionReviewV2.filterFiles")}
leadingIcon={
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M12.25 12.25L10.0625 10.0625M11.0833 6.41667C11.0833 8.994 8.994 11.0833 6.41667 11.0833C3.83934 11.0833 1.75 8.994 1.75 6.41667C1.75 3.83934 3.83934 1.75 6.41667 1.75C8.994 1.75 11.0833 3.83934 11.0833 6.41667Z"
stroke="currentColor"
stroke-linecap="square"
/>
</svg>
}
/>
</div>
<ScrollView
data-slot="session-review-v2-sidebar-tree"
class="group/file-tree-v2"
thumbVisibility="scroll"
viewportRef={props.viewportRef}
<Show when={props.open}>
<aside
data-slot="session-review-v2-sidebar"
data-resizing={resizing() ? "" : undefined}
style={{ width: `${width()}px` }}
>
{props.children}
</ScrollView>
</aside>
<div data-slot="session-review-v2-sidebar-header">
<div data-slot="session-review-v2-sidebar-title">{props.title}</div>
{props.stats}
</div>
<div data-slot="session-review-v2-sidebar-filter">
<TextInputV2
type="search"
value={props.filter}
onInput={(event) => props.onFilterChange(event.currentTarget.value)}
onKeyDown={props.onFilterKeyDown}
autofocus={props.filterAutofocus}
ref={props.filterRef}
role={props.filterControls ? "combobox" : undefined}
aria-autocomplete={props.filterControls ? "list" : undefined}
aria-controls={props.filterControls}
aria-activedescendant={props.filterActiveDescendant}
aria-expanded={props.filterControls ? props.filterExpanded : undefined}
showClearButton={props.filter.length > 0}
clearLabel={i18n.t("ui.list.clearFilter")}
onClearClick={() => props.onFilterChange("")}
placeholder={i18n.t("ui.sessionReviewV2.filterFiles")}
aria-label={i18n.t("ui.sessionReviewV2.filterFiles")}
leadingIcon={
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M12.25 12.25L10.0625 10.0625M11.0833 6.41667C11.0833 8.994 8.994 11.0833 6.41667 11.0833C3.83934 11.0833 1.75 8.994 1.75 6.41667C1.75 3.83934 3.83934 1.75 6.41667 1.75C8.994 1.75 11.0833 3.83934 11.0833 6.41667Z"
stroke="currentColor"
stroke-linecap="square"
/>
</svg>
}
/>
</div>
<ScrollView
data-slot="session-review-v2-sidebar-tree"
class="group/file-tree-v2"
thumbVisibility="scroll"
viewportRef={props.viewportRef}
>
{props.children}
</ScrollView>
</aside>
</Show>
<Show when={props.open && props.onWidthChange}>
<div data-slot="session-review-v2-sidebar-resize" onPointerDown={() => setResizing(true)}>
<ResizeHandle
@ -185,130 +197,125 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
cycle(event.key === "<" ? prev() : next())
})
return (
<div data-component="session-review-v2">
<div data-slot="session-review-v2-body">
{props.sidebar}
<div data-slot="session-review-v2-preview">
<Show when={props.hasDiffs} fallback={props.empty}>
<div data-slot="session-review-v2-toolbar">
<div data-slot="session-review-v2-toolbar-group" class="session-review-v2-toolbar-group--start">
{props.sidebarToggle}
<Show when={showCollapsedMeta()}>
<div data-slot="session-review-v2-toolbar-collapsed-meta">
<Show when={title()}>
<div data-slot="session-review-v2-toolbar-title">{title()}</div>
</Show>
{stats()}
<Show when={canCycle()}>
<span data-slot="session-review-v2-file-position">
{fileIndex() + 1}/{props.files.length}
</span>
</Show>
</div>
</Show>
<div data-slot="session-review-v2-toolbar-group">
<TooltipV2
openDelay={2000}
value={
<>
{i18n.t("ui.sessionReviewV2.previousFile")}
<KeybindV2 keys={["<"]} variant="neutral" />
</>
}
>
<IconButton
icon="arrow-left"
variant="ghost"
size="small"
class="session-review-v2-file-nav-button"
disabled={!canCycle()}
onClick={() => {
const file = prev()
if (!file) return
props.onSelectFile(file)
}}
aria-label={i18n.t("ui.sessionReviewV2.previousFile")}
/>
</TooltipV2>
<TooltipV2
openDelay={2000}
value={
<>
{i18n.t("ui.sessionReviewV2.nextFile")}
<KeybindV2 keys={[">"]} variant="neutral" />
</>
}
>
<IconButton
icon="arrow-right"
variant="ghost"
size="small"
class="session-review-v2-file-nav-button"
disabled={!canCycle()}
onClick={() => {
const file = next()
if (!file) return
props.onSelectFile(file)
}}
aria-label={i18n.t("ui.sessionReviewV2.nextFile")}
/>
</TooltipV2>
</div>
</div>
<div data-slot="session-review-v2-toolbar-group" class="session-review-v2-toolbar-group--segments">
<SegmentedControlV2
value={props.expandMode}
onChange={(value) => {
if (value !== "expand" && value !== "collapse") return
props.onExpandModeChange(value)
}}
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
aria-label={i18n.t("ui.sessionReviewV2.expandMode")}
>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.showAllLines")}>
<SegmentedControlItemV2 value="expand" aria-label={i18n.t("ui.sessionReviewV2.showAllLines")}>
<Icon name="expand" />
</SegmentedControlItemV2>
</TooltipV2>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<SegmentedControlItemV2 value="collapse" aria-label={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<Icon name="collapse" />
</SegmentedControlItemV2>
</TooltipV2>
</SegmentedControlV2>
<Show when={props.onDiffStyleChange}>
<SegmentedControlV2
value={props.diffStyle}
onChange={(value) => {
if (value !== "unified" && value !== "split") return
props.onDiffStyleChange?.(value)
}}
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
aria-label={i18n.t("ui.sessionReviewV2.diffView")}
>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<SegmentedControlItemV2 value="unified" aria-label={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<Icon name="unified" />
</SegmentedControlItemV2>
</TooltipV2>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.splitDiff")}>
<SegmentedControlItemV2 value="split" aria-label={i18n.t("ui.sessionReviewV2.splitDiff")}>
<Icon name="split" />
</SegmentedControlItemV2>
</TooltipV2>
</SegmentedControlV2>
</Show>
</div>
</div>
<Show when={props.activeFile} fallback={<div data-slot="session-review-v2-empty">{props.empty}</div>}>
{props.preview}
</Show>
const toolbarStart = () => (
<>
{props.sidebarToggle}
<Show when={showCollapsedMeta()}>
<div data-slot="session-review-v2-toolbar-collapsed-meta">
<Show when={title()}>
<div data-slot="session-review-v2-toolbar-title">{title()}</div>
</Show>
{stats()}
<Show when={canCycle()}>
<span data-slot="session-review-v2-file-position">
{fileIndex() + 1}/{props.files.length}
</span>
</Show>
</div>
</Show>
<div class="flex items-center">
<TooltipV2
openDelay={2000}
value={
<>
{i18n.t("ui.sessionReviewV2.previousFile")}
<KeybindV2 keys={["<"]} variant="neutral" />
</>
}
>
<IconButton
icon="arrow-left"
variant="ghost"
size="small"
class="session-review-v2-file-nav-button"
disabled={!canCycle()}
onClick={() => cycle(prev())}
aria-label={i18n.t("ui.sessionReviewV2.previousFile")}
/>
</TooltipV2>
<TooltipV2
openDelay={2000}
value={
<>
{i18n.t("ui.sessionReviewV2.nextFile")}
<KeybindV2 keys={[">"]} variant="neutral" />
</>
}
>
<IconButton
icon="arrow-right"
variant="ghost"
size="small"
class="session-review-v2-file-nav-button"
disabled={!canCycle()}
onClick={() => cycle(next())}
aria-label={i18n.t("ui.sessionReviewV2.nextFile")}
/>
</TooltipV2>
</div>
</div>
</>
)
const toolbarEnd = () => (
<>
<SegmentedControlV2
value={props.expandMode}
onChange={(value) => {
if (value !== "expand" && value !== "collapse") return
props.onExpandModeChange(value)
}}
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
aria-label={i18n.t("ui.sessionReviewV2.expandMode")}
>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.showAllLines")}>
<SegmentedControlItemV2 value="expand" aria-label={i18n.t("ui.sessionReviewV2.showAllLines")}>
<Icon name="expand" />
</SegmentedControlItemV2>
</TooltipV2>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<SegmentedControlItemV2 value="collapse" aria-label={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<Icon name="collapse" />
</SegmentedControlItemV2>
</TooltipV2>
</SegmentedControlV2>
<Show when={props.onDiffStyleChange}>
<SegmentedControlV2
value={props.diffStyle}
onChange={(value) => {
if (value !== "unified" && value !== "split") return
props.onDiffStyleChange?.(value)
}}
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
aria-label={i18n.t("ui.sessionReviewV2.diffView")}
>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<SegmentedControlItemV2 value="unified" aria-label={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<Icon name="unified" />
</SegmentedControlItemV2>
</TooltipV2>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.splitDiff")}>
<SegmentedControlItemV2 value="split" aria-label={i18n.t("ui.sessionReviewV2.splitDiff")}>
<Icon name="split" />
</SegmentedControlItemV2>
</TooltipV2>
</SegmentedControlV2>
</Show>
</>
)
return (
<SessionFilePanelV2
sidebar={props.sidebar}
toolbar={props.hasDiffs}
toolbarStart={toolbarStart()}
toolbarEnd={toolbarEnd()}
>
<Show when={props.hasDiffs} fallback={props.empty}>
<Show when={props.activeFile} fallback={<SessionFilePanelV2Empty>{props.empty}</SessionFilePanelV2Empty>}>
{props.preview}
</Show>
</Show>
</SessionFilePanelV2>
)
}