From bf58fae51f6c4c2112c1da4a27b1ba2bd3a6a5bb Mon Sep 17 00:00:00 2001 From: usrnk1 <7547651+usrnk1@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:52:40 +0200 Subject: [PATCH 1/5] feat(desktop): add recently closed projects to home (#34926) --- packages/app/src/context/global.tsx | 12 +- packages/app/src/context/layout.tsx | 13 +- packages/app/src/context/server.test.ts | 84 ++++++++++++- packages/app/src/context/server.tsx | 45 ++++++- packages/app/src/i18n/en.ts | 1 + packages/app/src/pages/home.tsx | 111 +++++++++++++++++- .../src/v2/components/project-avatar-v2.css | 7 ++ .../components/project-avatar-v2.stories.tsx | 10 +- .../src/v2/components/project-avatar-v2.tsx | 5 +- 9 files changed, 271 insertions(+), 17 deletions(-) diff --git a/packages/app/src/context/global.tsx b/packages/app/src/context/global.tsx index 138b17c4fc..83438898b5 100644 --- a/packages/app/src/context/global.tsx +++ b/packages/app/src/context/global.tsx @@ -1,7 +1,8 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { createEffect, createMemo, createRoot } from "solid-js" import { createStore } from "solid-js/store" -import { createServerProjects, ServerConnection, useServer } from "./server" +import { createServerProjects, RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServer } from "./server" +import { pathKey } from "@/utils/path-key" import { useServerHealth } from "@/utils/server-health" import { createServerSdkContext } from "./server-sdk" import { createServerSyncContext } from "./server-sync" @@ -127,6 +128,14 @@ function createServerCtx( } const projectsList = createMemo(() => projects.list().map(enrich)) + const recentlyClosedList = createMemo(() => { + const known = new Set(sync.data.project.map((project) => pathKey(project.worktree))) + return projects + .recentlyClosed() + .filter((worktree) => known.has(pathKey(worktree))) + .slice(0, RECENTLY_CLOSED_DISPLAY_LIMIT) + .map((worktree) => enrich({ worktree, expanded: false })) + }) const isLocal = (conn?.type === "sidecar" && conn.variant === "base") || (conn?.type === "http" && isLocalHost(conn.http.url)) @@ -139,6 +148,7 @@ function createServerCtx( projects: { ...projects, list: projectsList, + recentlyClosed: recentlyClosedList, }, } } diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 1487676e54..b09a77eb33 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -5,10 +5,11 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { makeEventListener } from "@solid-primitives/event-listener" import { useServerSync } from "./server-sync" import { useServerSDK } from "./server-sdk" -import { ServerConnection, useServer } from "./server" +import { RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServer } from "./server" import { usePlatform } from "./platform" import { Project } from "@opencode-ai/sdk/v2" import { Persist, persisted, removePersisted } from "@/utils/persist" +import { pathKey } from "@/utils/path-key" import { decode64 } from "@/utils/base64" import { same } from "@/utils/same" import { createScrollPersistence, type SessionScroll } from "./layout-scroll" @@ -493,7 +494,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const root = rootFor(project.worktree) if (root === project.worktree) continue - server.projects.close(project.worktree) + server.projects.remove(project.worktree) if (!seen.has(root)) { server.projects.open(root) @@ -613,6 +614,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, projects: { list, + recentlyClosed: createMemo(() => { + const known = new Set(serverSync().data.project.map((project) => pathKey(project.worktree))) + return server.projects + .recentlyClosed() + .filter((worktree) => known.has(pathKey(worktree))) + .slice(0, RECENTLY_CLOSED_DISPLAY_LIMIT) + .map((worktree) => enrich({ worktree, expanded: false })) + }), open(directory: string) { const root = rootFor(directory) if (server.projects.list().find((x) => x.worktree === root)) return diff --git a/packages/app/src/context/server.test.ts b/packages/app/src/context/server.test.ts index 72352fc5d5..d78880ecf1 100644 --- a/packages/app/src/context/server.test.ts +++ b/packages/app/src/context/server.test.ts @@ -99,7 +99,7 @@ describe("createServerProjects", () => { test("keeps active and explicit server buckets in one reactive store", () => { createRoot((dispose) => { const [scope] = createSignal(ServerScope.local) - const [store, setStore] = createStore({ projects: {}, lastProject: {} }) + const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} }) const active = createServerProjects({ scope, store, setStore }) const remote = createServerProjects({ scope: () => "https://debian.example" as ServerScope, store, setStore }) @@ -115,6 +115,88 @@ describe("createServerProjects", () => { dispose() }) }) + + test("tracks recently closed projects and drops them when reopened", () => { + createRoot((dispose) => { + const [scope] = createSignal(ServerScope.local) + const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} }) + const projects = createServerProjects({ scope, store, setStore }) + + projects.open("/a") + projects.open("/b") + projects.close("/a") + expect(projects.recentlyClosed()).toEqual(["/a"]) + + projects.close("/b") + expect(projects.recentlyClosed()).toEqual(["/b", "/a"]) + + projects.open("/a") + expect(projects.recentlyClosed()).toEqual(["/b"]) + expect(projects.list()).toEqual([{ worktree: "/a", expanded: true }]) + dispose() + }) + }) + + test("remove drops a project without recording it as recently closed", () => { + createRoot((dispose) => { + const [scope] = createSignal(ServerScope.local) + const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} }) + const projects = createServerProjects({ scope, store, setStore }) + + projects.open("/repo/subdir") + projects.remove("/repo/subdir") + expect(projects.list()).toEqual([]) + expect(projects.recentlyClosed()).toEqual([]) + dispose() + }) + }) + + test("retains recently closed history beyond the visible display limit", () => { + createRoot((dispose) => { + const [scope] = createSignal(ServerScope.local) + const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} }) + const projects = createServerProjects({ scope, store, setStore }) + + // Closing 6 projects keeps all 6 in the store even though only 5 are displayed; + // this prevents display-filtered entries from evicting still-visible ones. + for (const dir of ["/1", "/2", "/3", "/4", "/5", "/6"]) { + projects.open(dir) + projects.close(dir) + } + expect(projects.recentlyClosed()).toEqual(["/6", "/5", "/4", "/3", "/2", "/1"]) + dispose() + }) + }) + + test("caps recently closed history at the store limit", () => { + createRoot((dispose) => { + const [scope] = createSignal(ServerScope.local) + const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} }) + const projects = createServerProjects({ scope, store, setStore }) + + for (let i = 1; i <= 20; i++) { + projects.open(`/p${i}`) + projects.close(`/p${i}`) + } + expect(projects.recentlyClosed()).toHaveLength(16) + expect(projects.recentlyClosed()[0]).toBe("/p20") + expect(projects.recentlyClosed().at(-1)).toBe("/p5") + dispose() + }) + }) + + test("dedupes recently closed entries by normalized path", () => { + createRoot((dispose) => { + const [scope] = createSignal(ServerScope.local) + const [store, setStore] = createStore({ projects: {}, lastProject: {}, recentlyClosed: {} }) + const projects = createServerProjects({ scope, store, setStore }) + + projects.close("/repo") + projects.close("/repo/") + expect(projects.recentlyClosed()).toEqual(["/repo/"]) + dispose() + }) + }) }) describe("migrateCanonicalLocalServerState", () => { diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index c7957bab8e..95c6940d70 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -2,12 +2,23 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { type Accessor, batch, createMemo } from "solid-js" import { createStore, type SetStoreFunction, type Store } from "solid-js/store" import { Persist, persisted } from "@/utils/persist" +import { pathKey } from "@/utils/path-key" import { ServerScope } from "@/utils/server-scope" type StoredProject = { worktree: string; expanded: boolean } type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http -type ServerProjectState = { projects: Record; lastProject: Record } +type ServerProjectState = { + projects: Record + lastProject: Record + recentlyClosed: Record +} const HEALTH_POLL_INTERVAL_MS = 10_000 +// The store retains more history than is displayed. Consumers filter recently closed entries +// against the live project list (dropping deleted projects) and then cap the visible count via +// RECENTLY_CLOSED_DISPLAY_LIMIT. Retaining extra history ensures entries that are temporarily +// filtered out do not evict still-visible ones from the persisted store. +const RECENTLY_CLOSED_HISTORY_LIMIT = 16 +export const RECENTLY_CLOSED_DISPLAY_LIMIT = 5 export function normalizeServerUrl(input: string) { const trimmed = input.trim() @@ -72,19 +83,42 @@ export function createServerProjects(input: { }) { const setStore = input.setStore as unknown as SetStoreFunction const current = () => input.store.projects[input.scope()] ?? [] + const currentClosed = () => input.store.recentlyClosed?.[input.scope()] ?? [] + const remove = (directory: string) => { + setStore( + "projects", + input.scope(), + current().filter((project) => project.worktree !== directory), + ) + } return { list: current, + recentlyClosed: currentClosed, + remove, open(directory: string) { const scope = input.scope() + const key = pathKey(directory) + const closed = currentClosed() + if (closed.some((worktree) => pathKey(worktree) === key)) { + setStore( + "recentlyClosed", + scope, + closed.filter((worktree) => pathKey(worktree) !== key), + ) + } if (current().some((project) => project.worktree === directory)) return setStore("projects", scope, [{ worktree: directory, expanded: true }, ...current()]) }, + // User-initiated close: removes the project and records it in recently closed. + // Internal, non-user removals (e.g. sandbox/worktree normalization) should use remove(). close(directory: string) { - setStore( - "projects", - input.scope(), - current().filter((project) => project.worktree !== directory), + remove(directory) + const key = pathKey(directory) + const closed = [directory, ...currentClosed().filter((worktree) => pathKey(worktree) !== key)].slice( + 0, + RECENTLY_CLOSED_HISTORY_LIMIT, ) + setStore("recentlyClosed", input.scope(), closed) }, expand(directory: string) { const index = current().findIndex((project) => project.worktree === directory) @@ -235,6 +269,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( list: [] as StoredServer[], projects: {} as Record, lastProject: {} as Record, + recentlyClosed: {} as Record, }), ) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 0124fcf6ed..e5255df542 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -604,6 +604,7 @@ export const dict = { "home.title": "Home", "home.projects": "Projects", "home.project.add": "Add project", + "home.recentlyClosed": "Recently closed", "home.server.collapse": "Collapse server projects", "home.server.expand": "Expand server projects", "home.sessions.search.placeholder": "Search sessions", diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 7990495260..6222817ab6 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -270,6 +270,10 @@ export function NewHome() { }) const focusedSync = () => focusedServerCtx()?.sync ?? sync() const projects = createMemo(() => focusedServerCtx()?.projects.list() ?? layout.projects.list()) + const recentlyClosed = createMemo( + () => focusedServerCtx()?.projects.recentlyClosed() ?? layout.projects.recentlyClosed(), + ) + const homedir = createMemo(() => focusedSync().data.path.home ?? "") const selectedProject = createMemo(() => projects().find((project) => project.worktree === selection().directory)) const newSessionProject = createMemo( () => @@ -518,10 +522,13 @@ export function NewHome() {
addProjects(conn, [directory])} chooseProject={(conn) => void chooseProject(conn)} editProject={editProject} closeProject={(conn, directory) => { @@ -638,10 +645,13 @@ export function NewHome() { function HomeProjectColumn(props: { projects: LocalProject[] + recentlyClosed: LocalProject[] + homedir: string selected: HomeProjectSelection focusServer: (server: ServerConnection.Any) => void selectProject: (server: ServerConnection.Any, directory: string) => void openNewSession: (server: ServerConnection.Any, directory: string) => void + openRecentProject: (server: ServerConnection.Any, directory: string) => void chooseProject: (server: ServerConnection.Any) => void editProject: (server: ServerConnection.Any, project: LocalProject) => void closeProject: (server: ServerConnection.Any, directory: string) => void @@ -670,8 +680,10 @@ function HomeProjectColumn(props: { aria-label={props.language.t("home.projects")} >
- - +
{props.language.t("home.projects")}
+ 0)} + > 1} fallback={
- + 0} + fallback={ + + } + > + +
} > @@ -897,6 +923,79 @@ function HomeProjectList(props: { ) } +function HomeProjectEmpty(props: { + server: ServerConnection.Any + recentlyClosed: LocalProject[] + homedir: string + chooseProject: (server: ServerConnection.Any) => void + openRecentProject: (server: ServerConnection.Any, directory: string) => void + language: ReturnType +}) { + const global = useGlobal() + const unreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false + return ( +
+ + 0}> +
+
{props.language.t("home.recentlyClosed")}
+
+ + {(project) => ( + + )} + +
+
+ ) +} + +function HomeRecentlyClosedRow(props: { + project: LocalProject + server: ServerConnection.Any + homedir: string + openRecentProject: (server: ServerConnection.Any, directory: string) => void + language: ReturnType +}) { + const global = useGlobal() + const unreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false + const path = () => { + const home = props.homedir + const worktree = props.project.worktree + if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}` + return worktree + } + return ( + + + + ) +} + function HomeProjectRow(props: { project: LocalProject server: ServerConnection.Any @@ -979,13 +1078,13 @@ function HomeProjectRow(props: { ) } -function HomeProjectAvatar(props: { project: LocalProject }) { +function HomeProjectAvatar(props: { project: LocalProject; outline?: boolean }) { const name = createMemo(() => displayName(props.project)) return ( ) } diff --git a/packages/ui/src/v2/components/project-avatar-v2.css b/packages/ui/src/v2/components/project-avatar-v2.css index c724744654..e8c2149b5d 100644 --- a/packages/ui/src/v2/components/project-avatar-v2.css +++ b/packages/ui/src/v2/components/project-avatar-v2.css @@ -81,6 +81,13 @@ --project-avatar-border: var(--v2-avatar-border-gray); } +[data-slot="project-avatar-surface"][data-variant="outline"] { + --project-avatar-bg: var(--v2-background-bg-base); + --project-avatar-border: var(--v2-border-border-strong); + color: var(--v2-text-text-muted); + text-shadow: none; +} + [data-slot="project-avatar-surface"][data-has-image] { background: var(--project-avatar-bg); } diff --git a/packages/ui/src/v2/components/project-avatar-v2.stories.tsx b/packages/ui/src/v2/components/project-avatar-v2.stories.tsx index a677384627..3a2babcd9e 100644 --- a/packages/ui/src/v2/components/project-avatar-v2.stories.tsx +++ b/packages/ui/src/v2/components/project-avatar-v2.stories.tsx @@ -11,6 +11,7 @@ Saturated 16px project avatar with color variants and optional unread dot. ### Variants - Color: orange, yellow, cyan, green, red, pink, blue, purple, gray. +- Outline: neutral, muted style for de-emphasized projects (e.g. recently closed). - Image vs initial content state. - Unread dot with corner mask when \`unread\` is set. @@ -33,7 +34,7 @@ export default { argTypes: { variant: { control: "select", - options: [...PROJECT_AVATAR_VARIANTS], + options: [...PROJECT_AVATAR_VARIANTS, "outline"], }, }, args: { @@ -62,6 +63,13 @@ export const AllVariants = { ), } +export const Outline = { + args: { + fallback: "O", + variant: "outline", + }, +} + export const Unread = { args: { fallback: "O", diff --git a/packages/ui/src/v2/components/project-avatar-v2.tsx b/packages/ui/src/v2/components/project-avatar-v2.tsx index e0110d3cb4..22da7f6b72 100644 --- a/packages/ui/src/v2/components/project-avatar-v2.tsx +++ b/packages/ui/src/v2/components/project-avatar-v2.tsx @@ -26,10 +26,13 @@ export const PROJECT_AVATAR_VARIANTS = [ export type ProjectAvatarVariant = (typeof PROJECT_AVATAR_VARIANTS)[number] +// "outline" is a neutral, muted style (e.g. recently closed projects) and is not part of the color rotation. +export type ProjectAvatarStyle = ProjectAvatarVariant | "outline" + export interface ProjectAvatarProps extends ComponentProps<"div"> { fallback: string src?: string - variant?: ProjectAvatarVariant + variant?: ProjectAvatarStyle unread?: boolean } From 4c6e2a92ce7153cac26f637cfe31fc75c0741a3f Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:23:31 +0530 Subject: [PATCH 2/5] feat(app): align subagent UI with v2 (#34931) Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> --- .../session-ui/src/components/basic-tool.css | 52 ++++++++++++ .../src/components/message-part.tsx | 79 +++++++++++++++++-- packages/ui/src/styles/theme.css | 18 +++++ packages/ui/src/theme/v2/mapping.ts | 26 ++++++ packages/ui/src/v2/styles/theme.css | 40 ++++++++++ 5 files changed, 208 insertions(+), 7 deletions(-) diff --git a/packages/session-ui/src/components/basic-tool.css b/packages/session-ui/src/components/basic-tool.css index 7df483afe6..bda3069351 100644 --- a/packages/session-ui/src/components/basic-tool.css +++ b/packages/session-ui/src/components/basic-tool.css @@ -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"] { diff --git a/packages/session-ui/src/components/message-part.tsx b/packages/session-ui/src/components/message-part.tsx index fc4ca35999..89b4d34d36 100644 --- a/packages/session-ui/src/components/message-part.tsx +++ b/packages/session-ui/src/components/message-part.tsx @@ -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 { const body = typeof document === "undefined" ? undefined : document.body @@ -370,6 +371,44 @@ const agentTones: Record = { plan: "var(--icon-agent-plan-base)", } +const v2AgentTones: Record = { + 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 = { + 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 = { + 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) { + 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" @@ -1882,6 +1936,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 @@ -1921,17 +1978,25 @@ ToolRegistry.register({ } const trigger = () => ( -
+
- + }> + + - - {title()} - + {title()} {subtitle()} diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 751036598d..894f65e45d 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -281,6 +281,15 @@ --icon-agent-docs-base: #fcb239; --icon-agent-ask-base: #2090f5; --icon-agent-build-base: #034cff; + --v2-agent-plan-solid: var(--v2-pink-900); + --v2-agent-plan-border: rgba(200, 61, 139, 0.5); + --v2-agent-plan-background: rgba(253, 236, 243, 0.33); + --v2-agent-build-solid: var(--v2-blue-900); + --v2-agent-build-border: rgba(59, 92, 246, 0.5); + --v2-agent-build-background: rgba(236, 241, 254, 0.33); + --v2-agent-explore-solid: var(--v2-yellow-900); + --v2-agent-explore-border: rgba(142, 114, 49, 0.5); + --v2-agent-explore-background: rgba(254, 250, 236, 0.33); --icon-on-success-base: rgba(18, 201, 5, 0.9); --icon-on-success-hover: rgba(45, 186, 38, 0.9); --icon-on-success-selected: rgba(7, 137, 1, 0.9); @@ -541,6 +550,15 @@ --icon-agent-docs-base: #fbb73c; --icon-agent-ask-base: #2090f5; --icon-agent-build-base: #9dbefe; + --v2-agent-plan-solid: var(--v2-pink-400); + --v2-agent-plan-border: rgba(250, 188, 216, 0.5); + --v2-agent-plan-background: rgba(247, 213, 228, 0.1); + --v2-agent-build-solid: var(--v2-blue-400); + --v2-agent-build-border: rgba(215, 226, 252, 0.5); + --v2-agent-build-background: rgba(215, 226, 252, 0.1); + --v2-agent-explore-solid: var(--v2-yellow-400); + --v2-agent-explore-border: rgba(247, 229, 181, 0.5); + --v2-agent-explore-background: rgba(252, 239, 208, 0.1); --icon-on-success-base: rgba(18, 201, 5, 0.9); --icon-on-success-hover: rgba(53, 192, 45, 0.9); --icon-on-success-selected: rgba(77, 225, 68, 0.9); diff --git a/packages/ui/src/theme/v2/mapping.ts b/packages/ui/src/theme/v2/mapping.ts index 5f4be5ea1c..9bb29fbfa9 100644 --- a/packages/ui/src/theme/v2/mapping.ts +++ b/packages/ui/src/theme/v2/mapping.ts @@ -3,6 +3,30 @@ import { V2_AVATAR_DARK, V2_AVATAR_LIGHT } from "./avatar" const ref = (name: string): V2ColorValue => `var(--${name})` +const lightAgentTokens: Record = { + "v2-agent-plan-solid": ref("v2-pink-900"), + "v2-agent-plan-border": "rgba(200, 61, 139, 0.5)", + "v2-agent-plan-background": "rgba(253, 236, 243, 0.33)", + "v2-agent-build-solid": ref("v2-blue-900"), + "v2-agent-build-border": "rgba(59, 92, 246, 0.5)", + "v2-agent-build-background": "rgba(236, 241, 254, 0.33)", + "v2-agent-explore-solid": ref("v2-yellow-900"), + "v2-agent-explore-border": "rgba(142, 114, 49, 0.5)", + "v2-agent-explore-background": "rgba(254, 250, 236, 0.33)", +} + +const darkAgentTokens: Record = { + "v2-agent-plan-solid": ref("v2-pink-400"), + "v2-agent-plan-border": "rgba(250, 188, 216, 0.5)", + "v2-agent-plan-background": "rgba(247, 213, 228, 0.1)", + "v2-agent-build-solid": ref("v2-blue-400"), + "v2-agent-build-border": "rgba(215, 226, 252, 0.5)", + "v2-agent-build-background": "rgba(215, 226, 252, 0.1)", + "v2-agent-explore-solid": ref("v2-yellow-400"), + "v2-agent-explore-border": "rgba(247, 229, 181, 0.5)", + "v2-agent-explore-background": "rgba(252, 239, 208, 0.1)", +} + const light: Record = { "v2-background-bg-base": ref("v2-grey-100"), "v2-background-bg-deep": ref("v2-grey-200"), @@ -46,6 +70,7 @@ const light: Record = { "v2-state-bg-info": ref("v2-blue-100"), "v2-state-fg-info": ref("v2-blue-800"), "v2-state-border-info": ref("v2-blue-300"), + ...lightAgentTokens, ...V2_AVATAR_LIGHT, "v2-elevation-raised": "0px 2px 4px 0px var(--v2-alpha-dark-4), 0px 1px 2px -1px var(--v2-alpha-dark-8), 0px 0px 0px 0.5px var(--v2-alpha-dark-12), 0px 0px 0px 0px var(--v2-alpha-dark-0)", @@ -110,6 +135,7 @@ const dark: Record = { "v2-state-bg-info": ref("v2-blue-1200"), "v2-state-fg-info": ref("v2-blue-500"), "v2-state-border-info": ref("v2-blue-900"), + ...darkAgentTokens, ...V2_AVATAR_DARK, "v2-elevation-raised": "0px 2px 4px 0px var(--v2-alpha-dark-30), 0px 1px 2px 0px var(--v2-alpha-dark-30), 0px 0px 0px 0.5px var(--v2-alpha-light-16), 0px -0.5px 0px 0px var(--v2-alpha-light-6)", diff --git a/packages/ui/src/v2/styles/theme.css b/packages/ui/src/v2/styles/theme.css index 387ee94cbb..5ed135b7e9 100644 --- a/packages/ui/src/v2/styles/theme.css +++ b/packages/ui/src/v2/styles/theme.css @@ -77,6 +77,16 @@ --v2-state-fg-info: var(--v2-blue-800); --v2-state-border-info: var(--v2-blue-300); + --v2-agent-plan-solid: var(--v2-pink-900); + --v2-agent-plan-border: rgba(200, 61, 139, 0.5); + --v2-agent-plan-background: rgba(253, 236, 243, 0.33); + --v2-agent-build-solid: var(--v2-blue-900); + --v2-agent-build-border: rgba(59, 92, 246, 0.5); + --v2-agent-build-background: rgba(236, 241, 254, 0.33); + --v2-agent-explore-solid: var(--v2-yellow-900); + --v2-agent-explore-border: rgba(142, 114, 49, 0.5); + --v2-agent-explore-background: rgba(254, 250, 236, 0.33); + /* ── Project avatar (fixed; theme-independent) ── */ --v2-avatar-fg: #ffffffff; --v2-avatar-bg-orange: #ee7330ff; @@ -193,6 +203,16 @@ --v2-state-fg-info: var(--v2-blue-500); --v2-state-border-info: var(--v2-blue-900); + --v2-agent-plan-solid: var(--v2-pink-400); + --v2-agent-plan-border: rgba(250, 188, 216, 0.5); + --v2-agent-plan-background: rgba(247, 213, 228, 0.1); + --v2-agent-build-solid: var(--v2-blue-400); + --v2-agent-build-border: rgba(215, 226, 252, 0.5); + --v2-agent-build-background: rgba(215, 226, 252, 0.1); + --v2-agent-explore-solid: var(--v2-yellow-400); + --v2-agent-explore-border: rgba(247, 229, 181, 0.5); + --v2-agent-explore-background: rgba(252, 239, 208, 0.1); + --v2-elevation-raised: 0px 2px 4px 0px var(--v2-alpha-dark-30), 0px 1px 2px 0px var(--v2-alpha-dark-30), @@ -296,6 +316,16 @@ --v2-state-fg-info: var(--v2-blue-800); --v2-state-border-info: var(--v2-blue-300); + --v2-agent-plan-solid: var(--v2-pink-900); + --v2-agent-plan-border: rgba(200, 61, 139, 0.5); + --v2-agent-plan-background: rgba(253, 236, 243, 0.33); + --v2-agent-build-solid: var(--v2-blue-900); + --v2-agent-build-border: rgba(59, 92, 246, 0.5); + --v2-agent-build-background: rgba(236, 241, 254, 0.33); + --v2-agent-explore-solid: var(--v2-yellow-900); + --v2-agent-explore-border: rgba(142, 114, 49, 0.5); + --v2-agent-explore-background: rgba(254, 250, 236, 0.33); + --v2-elevation-raised: 0px 2px 4px 0px var(--v2-alpha-dark-4), 0px 1px 2px -1px var(--v2-alpha-dark-8), 0px 0px 0px 0.5px var(--v2-alpha-dark-12), 0px 0px 0px 0px var(--v2-alpha-dark-0); @@ -406,6 +436,16 @@ --v2-state-fg-info: var(--v2-blue-500); --v2-state-border-info: var(--v2-blue-900); + --v2-agent-plan-solid: var(--v2-pink-400); + --v2-agent-plan-border: rgba(250, 188, 216, 0.5); + --v2-agent-plan-background: rgba(247, 213, 228, 0.1); + --v2-agent-build-solid: var(--v2-blue-400); + --v2-agent-build-border: rgba(215, 226, 252, 0.5); + --v2-agent-build-background: rgba(215, 226, 252, 0.1); + --v2-agent-explore-solid: var(--v2-yellow-400); + --v2-agent-explore-border: rgba(247, 229, 181, 0.5); + --v2-agent-explore-background: rgba(252, 239, 208, 0.1); + --v2-avatar-fg: #ffffffff; --v2-avatar-bg-orange: #723d22ff; --v2-avatar-border-orange: #ff8648ff; From caedf36844be580b931cb37db30faf025831b42b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 3 Jul 2026 07:54:40 +0000 Subject: [PATCH 3/5] chore: generate --- packages/session-ui/src/components/message-part.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/session-ui/src/components/message-part.tsx b/packages/session-ui/src/components/message-part.tsx index 89b4d34d36..098ecdbd62 100644 --- a/packages/session-ui/src/components/message-part.tsx +++ b/packages/session-ui/src/components/message-part.tsx @@ -1992,7 +1992,9 @@ ToolRegistry.register({ }> - + From d46c02ba73ac533a1746ceb87b7f23ad520c8ebf Mon Sep 17 00:00:00 2001 From: usrnk1 <7547651+usrnk1@users.noreply.github.com> Date: Fri, 3 Jul 2026 10:04:59 +0200 Subject: [PATCH 4/5] feat(desktop): papercut fixes (#34939) --- packages/app/src/components/prompt-input.tsx | 4 +++- .../src/components/settings-v2/settings-v2.css | 2 +- .../app/src/components/titlebar-tab-popover.tsx | 2 +- .../pages/session/timeline/message-timeline.tsx | 4 ++-- .../src/pages/session/use-composer-commands.tsx | 17 ++++++++++++++++- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index c72df7e512..75ec3b7be5 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -218,6 +218,7 @@ export const PromptInput: Component = (props) => { let scrollRef!: HTMLDivElement let slashPopoverRef!: HTMLDivElement let restoreEndOnFocus = true + let savedCursor: number | null = null const mirror = { input: false } const inset = 56 @@ -590,7 +591,7 @@ export const PromptInput: Component = (props) => { const restoreFocus = () => { requestAnimationFrame(() => { - const cursor = prompt.cursor() ?? promptLength(prompt.current()) + const cursor = savedCursor ?? prompt.cursor() ?? promptLength(prompt.current()) editorRef.focus() setCursorPosition(editorRef, cursor) queueScroll() @@ -627,6 +628,7 @@ export const PromptInput: Component = (props) => { const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229 const handleBlur = () => { + savedCursor = currentCursor() closePopover() setComposing(false) } diff --git a/packages/app/src/components/settings-v2/settings-v2.css b/packages/app/src/components/settings-v2/settings-v2.css index de59bbc6fe..baff66c46f 100644 --- a/packages/app/src/components/settings-v2/settings-v2.css +++ b/packages/app/src/components/settings-v2/settings-v2.css @@ -399,7 +399,7 @@ overflow: hidden; font-size: 13px; font-weight: 440; - line-height: 1; + line-height: 16px; text-overflow: ellipsis; white-space: nowrap; } diff --git a/packages/app/src/components/titlebar-tab-popover.tsx b/packages/app/src/components/titlebar-tab-popover.tsx index 2eb07200c5..305b243786 100644 --- a/packages/app/src/components/titlebar-tab-popover.tsx +++ b/packages/app/src/components/titlebar-tab-popover.tsx @@ -3,7 +3,7 @@ import { createSignal, Show, type JSXElement } from "solid-js" import "./titlebar-tab-popover.css" // Initial hover delay before the preview appears, per design. -const OPEN_DELAY = 200 +const OPEN_DELAY = 400 // Mouse-out delay: begin closing immediately (a brief exit animation plays). const CLOSE_DELAY = 0 // After a preview closes, hovering a neighbouring tab within this window skips diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 2f3421a750..9d8d82bcc9 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -1393,14 +1393,14 @@ export function MessageTimeline(props: {