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
co-authored by opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Aiden Cline usrnk1 Aarav Sareen Luke Parker Brendan Allan Kit Langton James Long Adam opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Jay Simon Klee Jay Jack David Hill Brendan Allan opencode Aiden Cline James Long Dustin Deus 冯基魁 Frank Aiden Cline Victor Navarro Dax Raad Dax Nabs Vladimir Glafirov AidenGeunGeun Mark
parent 26e27c7a7f
commit 634386fe0f
116 changed files with 3797 additions and 1049 deletions
+101
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;
}
@@ -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")
})
@@ -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
+2
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 }) {
+18 -1
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"] {
+8
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"/>`,
@@ -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;
}
@@ -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>
@@ -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>
@@ -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;
}
@@ -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}
/>
)
}