chore: merge dev into v2 (#36770)

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: Jay <air@live.ca>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: James Long <jlongster@users.noreply.github.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
Co-authored-by: Victor Navarro <vn4varro@gmail.com>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Nabs <nabil@instafork.com>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com>
Co-authored-by: Mark <geraint0923@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot] 2026-07-14 09:58:18 -05:00 committed by GitHub
commit 634386fe0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
116 changed files with 3764 additions and 1016 deletions

View file

@ -698,3 +698,104 @@ body[data-new-layout] #terminal-panel [data-component="tabs"][data-variant="norm
}
}
}
body[data-new-layout] #review-panel [data-component="tabs"][data-variant="normal"][data-orientation="horizontal"] {
[data-slot="tabs-list"] {
height: 52px;
padding-right: 12px;
gap: 8px;
--tabs-review-fade: 8px;
}
[data-slot="tabs-trigger-wrapper"] {
height: 28px;
padding-inline: 10px;
border: 0;
background-color: transparent;
box-shadow: none;
&::after {
display: none;
}
[data-slot="tabs-trigger"] {
padding: 0 !important;
}
&:has([data-slot="tabs-trigger-close-button"]) {
padding-right: 10px;
}
&:has([data-selected]) {
background-color: var(--v2-background-bg-layer-02);
box-shadow: none;
}
}
[data-slot="tabs-trigger"] {
font-family: Inter, sans-serif;
font-style: normal;
font-weight: 440;
font-size: 13px;
line-height: 16px;
letter-spacing: -0.04px;
color: var(--v2-text-text-muted);
font-variation-settings: "slnt" 0;
font-variant-numeric: tabular-nums;
&[data-selected] {
color: var(--v2-text-text-base);
}
.tab-fileicon-color,
.tab-fileicon-mono,
[data-slot="icon-svg"] {
margin-left: -2px;
}
}
/* Preview/temporary tabs: beat trigger + .text-14-medium upright defaults */
[data-slot="tabs-trigger"] .italic {
font-style: italic !important;
font-variation-settings: "slnt" -10 !important;
}
}
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-tabs-bar {
width: 100%;
min-width: 0;
gap: 8px;
box-sizing: border-box;
border-bottom: 1px solid var(--border-weaker-base, var(--v2-border-border-weak));
background-color: var(--v2-background-bg-base);
}
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-tabs-bar [data-slot="tabs-list"],
body[data-new-layout]
#review-panel
[data-component="tabs"][data-variant="normal"][data-orientation="horizontal"]
.session-review-v2-tabs-bar
[data-slot="tabs-list"] {
flex: 1 1 0;
min-width: 0;
width: auto;
border-bottom: none;
}
body[data-new-layout]
#review-panel
[data-component="tabs"]
.session-review-v2-tabs-bar
[data-slot="tabs-list"]
> .sticky {
padding-right: 0;
}
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-open-in-app-slot {
flex-shrink: 0;
margin-left: auto;
}
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-open-in-app {
flex-shrink: 0;
}

View file

@ -0,0 +1,13 @@
import { expect, test } from "bun:test"
import { Marked } from "marked"
import { markedCodeSpanBoundary } from "./marked-code-span"
test("preserves code spans adjacent to tildes", async () => {
const marked = new Marked(markedCodeSpanBoundary)
expect(await marked.parse("~`0.1576` to measurement-window-only `0.00092`")).toBe(
"<p>~<code>0.1576</code> to measurement-window-only <code>0.00092</code></p>\n",
)
expect(await marked.parse("`before`~`after`")).toBe("<p><code>before</code>~<code>after</code></p>\n")
expect(await marked.parse("~~`deleted code`~~")).toBe("<p><del><code>deleted code</code></del></p>\n")
})

View file

@ -0,0 +1,17 @@
import type { MarkedExtension } from "marked"
// Keep adjacent tilde and backtick runs separate until markedjs/marked#4011 is released.
export const markedCodeSpanBoundary = {
tokenizer: {
inlineText(src) {
const match = /^(`+(?=~)|~+(?=`))/.exec(src)
if (!match) return false
return {
type: "text",
raw: match[0],
text: match[0],
escaped: this.lexer.state.inRawBlock,
}
},
},
} satisfies MarkedExtension

View file

@ -3,6 +3,7 @@ import markedShiki from "marked-shiki"
import katex from "katex"
import { bundledLanguages, type BundledLanguage } from "shiki"
import { createSimpleContext } from "./helper"
import { markedCodeSpanBoundary } from "./marked-code-span"
import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/diffs"
export const OpenCodeTheme = {
@ -521,6 +522,7 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
name: "Marked",
init: (props: { nativeParser?: NativeMarkdownParser }) => {
const jsParser = marked.use(
markedCodeSpanBoundary,
{
renderer: {
link({ href, title, text }) {

View file

@ -14,12 +14,14 @@
justify-content: flex-start;
gap: 6px;
padding-right: 8px;
overflow: visible;
border: none;
border-radius: 6px;
background-color: transparent;
color: var(--v2-text-text-muted);
text-align: left;
cursor: pointer;
scroll-margin-block: 8px;
transition:
background-color 120ms ease,
color 120ms ease;
@ -42,6 +44,21 @@
background-color: var(--v2-overlay-simple-overlay-pressed);
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-guide"] {
position: absolute;
top: 0;
margin-top: -2px;
height: calc(100% + 2px);
width: 1px;
pointer-events: none;
background-color: var(--v2-border-border-muted);
opacity: 0;
}
[data-component="file-tree-v2"]:hover [data-slot="file-tree-v2-guide"] {
opacity: 0.5;
}
[data-component="file-tree-v2"] .filetree-icon--mono {
color: var(--v2-icon-icon-muted);
}
@ -108,7 +125,7 @@
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-change"][data-change="modified"] {
opacity: 0;
color: var(--v2-state-fg-info);
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-change"][data-change="added"] {

View file

@ -89,6 +89,10 @@ const icons = {
viewBox: "0 0 16 16",
body: `<path d="M4.25 11.75L11.75 4.25M11.75 11.75L4.25 4.25" stroke="currentColor"/>`,
},
"outline-xmark": {
viewBox: "0 0 16 16",
body: `<path fill-rule="evenodd" clip-rule="evenodd" d="M4.99487 5.70186L7.29297 7.99995L4.99487 10.2981L5.70198 11.0052L8.00008 8.70706L10.2982 11.0052L11.0053 10.2981L8.70718 7.99995L11.0053 5.70186L10.2982 4.99475L8.00008 7.29285L5.70198 4.99475L4.99487 5.70186Z" fill="currentColor"/>`,
},
"outline-chevron-down": {
viewBox: "0 0 16 16",
body: `<path d="M5 6.5L8 9.5L11 6.5" stroke="currentColor"/>`,
@ -129,6 +133,10 @@ const icons = {
viewBox: "0 0 16 16",
body: `<path d="M13.5555 6.66656V2.44434H9.33326M13.5555 2.44434L7.99993 7.99989M13.5555 9.33324V13.5555C13.5555 13.5555 12.7599 13.5555 11.7777 13.5555H2.44438C2.44438 13.5555 2.44438 12.7599 2.44438 11.7777V4.22213C2.44438 3.2399 2.44434 2.44435 2.44434 2.44435H6.66661" stroke="currentColor"/>`,
},
"outline-share": {
viewBox: "0 0 16 16",
body: `<path d="M13.5554 10.4445V13.5556C13.5554 13.5556 12.7599 13.5556 11.7777 13.5556H4.22211C3.23989 13.5556 2.44434 13.5556 2.44434 13.5556V10.4445M4.88878 5.55557L7.99989 2.44446L11.111 5.55557M7.99989 2.44446L7.99989 9.11112" stroke="currentColor"/>`,
},
reset: {
viewBox: "0 0 20 20",
body: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`,

View file

@ -203,3 +203,56 @@
gap: 8px;
flex-shrink: 0;
}
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-list"] {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 180px;
overflow: auto;
margin-top: 8px;
padding: 4px;
border: 0.5px solid var(--v2-border-border-muted);
border-radius: 6px;
background: var(--v2-background-bg-layer-02);
}
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-item"] {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
padding: 6px 8px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--v2-text-text-base);
text-align: left;
cursor: pointer;
}
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-item"][data-active] {
background: var(--v2-background-bg-layer-03);
}
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-path"] {
display: flex;
align-items: center;
min-width: 0;
font-size: 12px;
line-height: 16px;
}
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-dir"] {
min-width: 0;
color: var(--v2-text-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-file"] {
color: var(--v2-text-text-base);
white-space: nowrap;
}

View file

@ -1,4 +1,6 @@
import { type ComponentProps, type JSX, Show, onMount, splitProps } from "solid-js"
import { For, Show, createSignal, onMount, splitProps, type ComponentProps, type JSX } from "solid-js"
import { FileIcon } from "../../components/file-icon"
import { useFilteredList } from "../../hooks"
import { ButtonV2 } from "./button-v2"
import "./line-comment-v2.css"
@ -53,6 +55,10 @@ export function LineCommentV2(props: LineCommentV2Props) {
)
}
export type LineCommentEditorV2Mention = {
items: (query: string) => string[] | Promise<string[]>
}
export interface LineCommentEditorV2Props extends Omit<ComponentProps<"div">, "children" | "onInput" | "onSubmit"> {
/** Visible field label above the textarea (default: “Comment”). */
heading?: JSX.Element | string
@ -66,10 +72,22 @@ export interface LineCommentEditorV2Props extends Omit<ComponentProps<"div">, "c
cancelLabel?: string
submitLabel?: string
autofocus?: boolean
mention?: LineCommentEditorV2Mention
}
function pathFilename(path: string) {
const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
return index === -1 ? path : path.slice(index + 1)
}
function pathDirectory(path: string) {
const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
return index === -1 ? "" : path.slice(0, index + 1)
}
export function LineCommentEditorV2(props: LineCommentEditorV2Props) {
let textareaRef: HTMLTextAreaElement | undefined
const [mentionOpen, setMentionOpen] = createSignal(false)
const [local, rest] = splitProps(props, [
"heading",
@ -83,6 +101,7 @@ export function LineCommentEditorV2(props: LineCommentEditorV2Props) {
"cancelLabel",
"submitLabel",
"autofocus",
"mention",
"class",
"classList",
])
@ -90,6 +109,78 @@ export function LineCommentEditorV2(props: LineCommentEditorV2Props) {
const heading = () => local.heading ?? "Comment"
const canSubmit = () => local.value.trim().length > 0
const closeMention = () => {
setMentionOpen(false)
mention.clear()
}
const currentMention = () => {
const textarea = textareaRef
if (!textarea) return
if (!local.mention) return
if (textarea.selectionStart !== textarea.selectionEnd) return
const end = textarea.selectionStart
const match = textarea.value.slice(0, end).match(/@(\S*)$/)
if (!match) return
return {
query: match[1] ?? "",
start: end - match[0].length,
end,
}
}
function selectMention(item: { path: string } | undefined) {
if (!item) return
const textarea = textareaRef
const query = currentMention()
if (!textarea || !query) return
const value = `${textarea.value.slice(0, query.start)}@${item.path} ${textarea.value.slice(query.end)}`
const cursor = query.start + item.path.length + 2
local.onInput(value)
closeMention()
requestAnimationFrame(() => {
textarea.focus()
textarea.setSelectionRange(cursor, cursor)
})
}
const mention = useFilteredList<{ path: string }>({
items: async (query) => {
if (!local.mention) return []
if (!query.trim()) return []
const paths = await local.mention.items(query)
return paths.map((path) => ({ path }))
},
key: (item) => item.path,
filterKeys: ["path"],
skipFilter: () => true,
onSelect: selectMention,
})
const syncMention = () => {
const item = currentMention()
if (!item) {
closeMention()
return
}
setMentionOpen(true)
mention.onInput(item.query)
}
const selectActiveMention = () => {
const items = mention.flat()
if (items.length === 0) return
const active = mention.active()
selectMention(items.find((item) => item.path === active) ?? items[0])
}
const submit = () => {
const v = local.value.trim()
if (!v) return
@ -122,9 +213,39 @@ export function LineCommentEditorV2(props: LineCommentEditorV2Props) {
rows={local.rows ?? 3}
placeholder={local.placeholder ?? "Add context for this change"}
value={local.value}
onInput={(e) => local.onInput(e.currentTarget.value)}
onInput={(e) => {
local.onInput(e.currentTarget.value)
syncMention()
}}
onClick={() => syncMention()}
onSelect={() => syncMention()}
onKeyDown={(e) => {
e.stopPropagation()
if (e.isComposing || e.keyCode === 229) return
if (mentionOpen()) {
if (e.key === "Escape") {
e.preventDefault()
closeMention()
return
}
if (e.key === "Tab") {
if (mention.flat().length === 0) return
e.preventDefault()
selectActiveMention()
return
}
const nav = e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Enter"
const ctrlNav = e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && (e.key === "n" || e.key === "p")
if ((nav || ctrlNav) && mention.flat().length > 0) {
mention.onKeyDown(e)
e.preventDefault()
return
}
}
if (e.key === "Escape") {
e.preventDefault()
e.currentTarget.blur()
@ -137,6 +258,34 @@ export function LineCommentEditorV2(props: LineCommentEditorV2Props) {
}
}}
/>
<Show when={mentionOpen() && mention.flat().length > 0}>
<div data-slot="line-comment-v2-mention-list">
<For each={mention.flat().slice(0, 10)}>
{(item) => {
const directory = item.path.endsWith("/") ? item.path : pathDirectory(item.path)
const name = item.path.endsWith("/") ? "" : pathFilename(item.path)
return (
<button
type="button"
data-slot="line-comment-v2-mention-item"
data-active={mention.active() === item.path ? "" : undefined}
onMouseDown={(event) => event.preventDefault()}
onMouseEnter={() => mention.setActive(item.path)}
onClick={() => selectMention(item)}
>
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" />
<div data-slot="line-comment-v2-mention-path">
<span data-slot="line-comment-v2-mention-dir">{directory}</span>
<Show when={name}>
<span data-slot="line-comment-v2-mention-file">{name}</span>
</Show>
</div>
</button>
)
}}
</For>
</div>
</Show>
</div>
<div data-slot="line-comment-v2-footer">
<div data-slot="line-comment-v2-footer-meta">{local.selection}</div>

View file

@ -38,7 +38,6 @@ export interface ProjectAvatarProps extends ComponentProps<"div"> {
export function ProjectAvatar(props: ProjectAvatarProps) {
const [split, rest] = splitProps(props, ["fallback", "src", "variant", "unread", "class", "classList", "style"])
const src = split.src
return (
<div
{...rest}
@ -53,9 +52,9 @@ export function ProjectAvatar(props: ProjectAvatarProps) {
<div
data-slot="project-avatar-surface"
data-variant={split.variant ?? "gray"}
data-has-image={src ? "" : undefined}
data-has-image={split.src ? "" : undefined}
>
<Show when={src} fallback={first(split.fallback)}>
<Show when={split.src} fallback={first(split.fallback)}>
{(value) => <img src={value()} draggable={false} data-slot="project-avatar-image" />}
</Show>
</div>

View file

@ -0,0 +1,99 @@
[data-component="split-button-v2"] {
display: inline-flex;
align-items: stretch;
flex-shrink: 0;
height: 28px;
border-radius: 6px;
background-color: transparent;
box-shadow: none;
overflow: hidden;
}
[data-component="split-button-v2"]:is(:hover, :has([data-component="split-button-v2-menu-trigger"][data-expanded])) {
box-shadow: inset 0 0 0 1px var(--v2-border-border-muted);
}
[data-component="split-button-v2-action"],
[data-component="split-button-v2-menu-trigger"] {
display: inline-flex;
align-items: center;
justify-content: center;
height: 28px;
margin: 0;
padding: 0;
border: none;
background: transparent;
color: var(--v2-icon-icon-base);
cursor: default;
outline: none;
}
[data-component="split-button-v2-action"] {
width: 28px;
flex-shrink: 0;
}
[data-component="split-button-v2-action"]:hover:not(:disabled) {
background-color: var(--v2-overlay-simple-overlay-hover);
}
[data-component="split-button-v2-action"]:is(:active, [data-pressed]):not(:disabled) {
background-color: var(--v2-overlay-simple-overlay-pressed);
}
[data-component="split-button-v2-action"]:is(:focus-visible, [data-focus]) {
outline: 2px solid var(--v2-border-border-focus);
outline-offset: -2px;
z-index: 1;
}
[data-component="split-button-v2-action"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
[data-component="split-button-v2-menu-trigger"] {
width: 20px;
flex-shrink: 0;
}
[data-component="split-button-v2-menu-trigger"]:is(:hover, [data-expanded]):not(:disabled) {
background-color: var(--v2-overlay-simple-overlay-hover);
}
[data-component="split-button-v2-menu-trigger"]:is(:active, [data-pressed]):not(:disabled) {
background-color: var(--v2-overlay-simple-overlay-pressed);
}
[data-component="split-button-v2-menu-trigger"]:is(:focus-visible, [data-focus]) {
outline: 2px solid var(--v2-border-border-focus);
outline-offset: -2px;
z-index: 1;
}
[data-component="split-button-v2-menu-trigger"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
[data-component="split-button-v2-action"] [data-component="app-icon"] {
width: 16px;
height: 16px;
}
[data-component="menu-v2-content"].open-in-app-v2-menu [data-component="app-icon"] {
width: 16px;
height: 16px;
flex-shrink: 0;
}
[data-component="menu-v2-content"].open-in-app-v2-menu [data-component="icon"] {
width: 16px;
height: 16px;
flex-shrink: 0;
}
[data-component="menu-v2-content"].open-in-app-v2-menu [data-component="icon"] [data-slot="icon-svg"] {
width: 16px;
height: 16px;
}

View file

@ -0,0 +1,48 @@
import { splitProps, type ComponentProps, type ParentProps } from "solid-js"
import "./split-button-v2.css"
export function SplitButtonV2(props: ParentProps<ComponentProps<"div">>) {
const [split, rest] = splitProps(props, ["class", "classList", "children"])
return (
<div
data-component="split-button-v2"
classList={{
...split.classList,
[split.class ?? ""]: !!split.class,
}}
{...rest}
>
{split.children}
</div>
)
}
export function SplitButtonV2Action(props: ComponentProps<"button">) {
const [split, rest] = splitProps(props, ["class", "classList"])
return (
<button
type="button"
data-component="split-button-v2-action"
classList={{
...split.classList,
[split.class ?? ""]: !!split.class,
}}
{...rest}
/>
)
}
export function SplitButtonV2MenuTrigger(props: ComponentProps<"button">) {
const [split, rest] = splitProps(props, ["class", "classList"])
return (
<button
type="button"
data-component="split-button-v2-menu-trigger"
classList={{
...split.classList,
[split.class ?? ""]: !!split.class,
}}
{...rest}
/>
)
}