+
+
+
+
+
+
+
+
+
+
+ {(line) => (
+
+
-
+ )}
-
{(line) => {line()}
}
diff --git a/packages/session-ui/src/components/session-turn.css b/packages/session-ui/src/components/session-turn.css
index 25516f70d33..64140e661b6 100644
--- a/packages/session-ui/src/components/session-turn.css
+++ b/packages/session-ui/src/components/session-turn.css
@@ -83,6 +83,48 @@
overflow-y: auto;
}
+ [data-component="card"]:is([data-kind="session-retry-card"], [data-kind="session-error-card"]) {
+ font-size: 13px;
+ line-height: var(--line-height-compact);
+ letter-spacing: -0.04px;
+ }
+
+ [data-component="card"][data-kind="session-retry-card"] {
+ --card-pad-l: 0px;
+ --card-pad-r: 0px;
+
+ &::before {
+ display: none;
+ }
+
+ [data-slot="session-turn-retry-message"] {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ [data-slot="session-turn-retry-info"] {
+ margin-top: 4px;
+ color: var(--v2-text-text-faint);
+ font-family: var(--v2-font-family-sans);
+ font-size: 13px;
+ font-weight: 440;
+ line-height: var(--line-height-compact);
+ letter-spacing: -0.04px;
+ }
+ }
+
+ [data-component="card"][data-kind="session-error-card"] {
+ --card-pad-y: 0px;
+ --card-pad-l: 0px;
+ --card-pad-r: 0px;
+
+ &::before {
+ display: none;
+ }
+ }
+
[data-slot="session-turn-assistant-content"] {
width: 100%;
min-width: 0;
diff --git a/packages/session-ui/src/components/tool-error-card.css b/packages/session-ui/src/components/tool-error-card.css
index 495721cfc05..a22e54d993c 100644
--- a/packages/session-ui/src/components/tool-error-card.css
+++ b/packages/session-ui/src/components/tool-error-card.css
@@ -2,6 +2,7 @@
--card-pad-y: 0px;
--card-line-pad: 0px;
--card-pad-r: 0px;
+ --card-pad-l: 0px;
&::before {
display: none;
@@ -26,16 +27,6 @@
color: var(--v2-text-text-muted);
}
- [data-slot="tool-error-card-dot"] {
- flex-shrink: 0;
- font-family: var(--v2-font-family-sans);
- font-size: 11px;
- font-weight: 530;
- line-height: var(--line-height-tight);
- letter-spacing: 0.05px;
- color: var(--v2-text-text-muted);
- }
-
[data-slot="tool-error-card-summary"] {
font-family: var(--v2-font-family-sans);
font-size: 13px;
diff --git a/packages/session-ui/src/components/tool-error-card.tsx b/packages/session-ui/src/components/tool-error-card.tsx
index f27c267cee2..5f995bca263 100644
--- a/packages/session-ui/src/components/tool-error-card.tsx
+++ b/packages/session-ui/src/components/tool-error-card.tsx
@@ -97,7 +97,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
-
+
@@ -121,9 +121,6 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
-
- ·
-
{summary()}
diff --git a/packages/session-ui/src/message/attachment-card.css b/packages/session-ui/src/message/attachment-card.css
index 488c141183a..095ebf46485 100644
--- a/packages/session-ui/src/message/attachment-card.css
+++ b/packages/session-ui/src/message/attachment-card.css
@@ -68,3 +68,7 @@
}
}
}
+
+[data-color-scheme="dark"] [data-component="attachment-card"][data-surface="base"] {
+ background: var(--v2-overlay-simple-overlay-hover);
+}
diff --git a/packages/session-ui/src/timeline/session-timeline-row.tsx b/packages/session-ui/src/timeline/session-timeline-row.tsx
index e5f120ca525..8a3c266e795 100644
--- a/packages/session-ui/src/timeline/session-timeline-row.tsx
+++ b/packages/session-ui/src/timeline/session-timeline-row.tsx
@@ -4,7 +4,6 @@ import type {
SessionMessageUser,
SessionStatus,
} from "@opencode-ai/client/promise"
-import { Card } from "@opencode-ai/ui/card"
import { useI18n } from "@opencode-ai/ui/context/i18n"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { For, Show, createMemo, type Accessor, type JSX } from "solid-js"
@@ -22,6 +21,7 @@ import {
import { AssistantReasoningContent, SessionCompactionMessage } from "../message/message-content"
import type { ContextGroupPart } from "../tools/tool-renderer"
import { SessionRetry } from "../components/session-retry"
+import { SessionError } from "../components/session-error"
import { timelineCategory, type TimelineDetail } from "./detail"
import { currentToolFailed } from "../message/current-tool-state"
import {
@@ -293,8 +293,10 @@ export function createSessionTimelineRowRenderer(input: {
const notice = (message: SessionMessageInfo) => {
if (message.type === "agent-switched")
return {
- label: i18n.t("ui.tool.agent.default"),
- data: message.previous ? `${message.previous} → ${message.agent}` : message.agent,
+ label: i18n.t("ui.sessionTimeline.notice.agentChanged"),
+ data: message.previous
+ ? `${capitalizeAgent(message.previous)} → ${capitalizeAgent(message.agent)}`
+ : capitalizeAgent(message.agent),
}
if (message.type === "model-switched") return undefined
if (message.type === "skill") return { label: i18n.t("ui.tool.skill"), data: message.name }
@@ -412,14 +414,18 @@ export function createSessionTimelineRowRenderer(input: {
data-slot="session-timeline-notice"
class={`w-full truncate ${props.grouped ? "py-1" : "pt-3 pb-1"} text-13-regular leading-text-compact text-text-weak ${inset()}`}
>
-
+
{content().label}
{(data) => (
-
- {" "}
- · {data()}
+
+ {" · "}
+ {data()}
)}
@@ -683,9 +689,7 @@ export function createSessionTimelineRowRenderer(input: {
return (
-
- {current().text}
-
+
)
@@ -701,3 +705,7 @@ export function createSessionTimelineRowRenderer(input: {
return { Row }
}
+
+function capitalizeAgent(agent: string) {
+ return agent.slice(0, 1).toUpperCase() + agent.slice(1)
+}
diff --git a/packages/session-ui/src/tools/tool-renderer.tsx b/packages/session-ui/src/tools/tool-renderer.tsx
index 0946d736a5d..078c3a4eba5 100644
--- a/packages/session-ui/src/tools/tool-renderer.tsx
+++ b/packages/session-ui/src/tools/tool-renderer.tsx
@@ -1441,7 +1441,7 @@ ToolRegistry.register({
-
+
@@ -1468,7 +1468,7 @@ ToolRegistry.register({
>
diff --git a/packages/ui/src/actions/button/button.css b/packages/ui/src/actions/button/button.css
index 20a8855503f..7433bac33bc 100644
--- a/packages/ui/src/actions/button/button.css
+++ b/packages/ui/src/actions/button/button.css
@@ -260,3 +260,32 @@
opacity: 0.5;
cursor: not-allowed;
}
+
+/* Ghost faint */
+[data-component="button-v2"][data-variant="ghost-faint"] {
+ background-color: transparent;
+ color: var(--v2-text-text-faint);
+}
+
+[data-component="button-v2"][data-variant="ghost-faint"] [data-slot="icon-svg"] {
+ color: var(--v2-icon-icon-faint);
+}
+
+[data-component="button-v2"][data-variant="ghost-faint"]:is(:hover, [data-state="hover"]):not(:disabled):not(
+ [data-expanded]
+ ) {
+ background-color: var(--v2-overlay-simple-overlay-hover);
+}
+
+[data-component="button-v2"][data-variant="ghost-faint"]:is(:active, [data-state="pressed"]):not(:disabled) {
+ background-color: var(--v2-overlay-simple-overlay-pressed);
+}
+
+[data-component="button-v2"][data-variant="ghost-faint"]:where([data-expanded]):not(:disabled) {
+ background-color: var(--v2-overlay-simple-overlay-pressed);
+}
+
+[data-component="button-v2"][data-variant="ghost-faint"]:is(:disabled, [data-state="disabled"]) {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
diff --git a/packages/ui/src/actions/button/button.stories.tsx b/packages/ui/src/actions/button/button.stories.tsx
index 0e85bf7651b..e065f133015 100644
--- a/packages/ui/src/actions/button/button.stories.tsx
+++ b/packages/ui/src/actions/button/button.stories.tsx
@@ -5,7 +5,7 @@ const docs = `### Overview
Button v2 with visual variants and three sizes.
### API
-- \`variant\`: "neutral" | "danger" | "warning" | "contrast" | "ghost" | "ghost-muted" | "loading".
+- \`variant\`: "neutral" | "danger" | "warning" | "contrast" | "ghost" | "ghost-muted" | "ghost-faint" | "loading".
- \`size\`: "small" | "normal" | "large".
- \`icon\`: Optional icon name.
- Inherits Kobalte Button props and native button attributes.
@@ -40,7 +40,7 @@ export default {
},
variant: {
control: "select",
- options: ["neutral", "danger", "warning", "contrast", "ghost", "ghost-muted", "loading"],
+ options: ["neutral", "danger", "warning", "contrast", "ghost", "ghost-muted", "ghost-faint", "loading"],
},
size: {
control: "select",
@@ -69,6 +69,9 @@ export const Variants = {
+
),
@@ -119,7 +122,16 @@ export const Icon = {
export const AllStates = {
render: () => {
- const variants = ["neutral", "danger", "warning", "contrast", "ghost", "ghost-muted", "loading"] as const
+ const variants = [
+ "neutral",
+ "danger",
+ "warning",
+ "contrast",
+ "ghost",
+ "ghost-muted",
+ "ghost-faint",
+ "loading",
+ ] as const
const states = ["default", "hover", "pressed", "focus", "disabled"] as const
const toTitleCase = (value: string) => value.charAt(0).toUpperCase() + value.slice(1)
return (
diff --git a/packages/ui/src/actions/button/button.tsx b/packages/ui/src/actions/button/button.tsx
index 7442dc5d642..8e753f4477d 100644
--- a/packages/ui/src/actions/button/button.tsx
+++ b/packages/ui/src/actions/button/button.tsx
@@ -7,7 +7,16 @@ export interface ButtonProps
extends ComponentProps
,
Pick, "class" | "classList" | "children"> {
size?: "small" | "normal" | "large"
- variant?: "neutral" | "danger" | "warning" | "outline" | "contrast" | "ghost" | "ghost-muted" | "loading"
+ variant?:
+ | "neutral"
+ | "danger"
+ | "warning"
+ | "outline"
+ | "contrast"
+ | "ghost"
+ | "ghost-muted"
+ | "ghost-faint"
+ | "loading"
icon?: IconProps["name"]
}
diff --git a/packages/ui/src/forms/text-input/text-input.css b/packages/ui/src/forms/text-input/text-input.css
index 595a37098b1..d208a44df5a 100644
--- a/packages/ui/src/forms/text-input/text-input.css
+++ b/packages/ui/src/forms/text-input/text-input.css
@@ -13,7 +13,7 @@
outline: 1px solid transparent;
outline-offset: 0;
background:
- linear-gradient(180deg, var(--v2-alpha-light-2) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-base);
+ linear-gradient(180deg, var(--v2-alpha-light-6) 0%, var(--v2-alpha-light-2) 100%), var(--v2-background-bg-base);
box-shadow: var(--v2-elevation-button-neutral);
flex: none;
align-self: stretch;
@@ -30,7 +30,7 @@
[data-component="text-input-v2"]:where(:hover):not([data-disabled], [data-invalid]):not(:focus-within) {
background:
linear-gradient(0deg, var(--v2-overlay-simple-overlay-hover), var(--v2-overlay-simple-overlay-hover)),
- linear-gradient(180deg, var(--v2-alpha-light-2) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-base);
+ linear-gradient(180deg, var(--v2-alpha-light-6) 0%, var(--v2-alpha-light-2) 100%), var(--v2-background-bg-base);
}
[data-component="text-input-v2"]:where(:focus-within):not([data-disabled], [data-invalid]) {
diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts
index f26534b52b2..da2ff5b90a3 100644
--- a/packages/ui/src/i18n/en.ts
+++ b/packages/ui/src/i18n/en.ts
@@ -69,6 +69,8 @@ const source = {
"ui.sessionTurn.retry.inSeconds": "in {{seconds}}s",
"ui.sessionTurn.retry.attempt": "attempt #{{attempt}}",
"ui.sessionTurn.retry.attemptLine": "{{line}} - attempt #{{attempt}}",
+ "ui.sessionTurn.retry.attemptLabel": "Attempt {{attempt}}",
+ "ui.sessionTurn.retry.attemptRetrying": "Attempt {{attempt}} - {{line}}",
"ui.sessionTurn.retry.geminiHot": "gemini is way too hot right now",
"ui.sessionTurn.error.freeUsageExceeded": "Free usage exceeded",
"ui.sessionTurn.error.addCredits": "Add credits",
@@ -146,6 +148,7 @@ const source = {
"ui.promptInput.chooseVariant": "Choose model variant",
"ui.promptInput.send": "Send",
"ui.promptInput.stop": "Stop",
+ "ui.promptInput.exitShell": "Exit",
"ui.promptInput.steer": "Steer",
"ui.promptInput.queue": "Queue",
"ui.promptInput.steerHint": "Send without interrupting",
@@ -227,6 +230,7 @@ const source = {
"ui.message.interrupted": "Interrupted",
"ui.sessionTimeline.notice.model": "Model",
"ui.sessionTimeline.notice.modelSwitched": "Switched to {{model}}",
+ "ui.sessionTimeline.notice.agentChanged": "Agent changed",
"ui.sessionTimeline.notice.movedTo": "Moved to",
"ui.sessionTimeline.notice.movedTooltip": "Session working directory changed",
"ui.sessionTimeline.notice.failed": "{{actor}} failed",
diff --git a/packages/ui/src/icons/icon/icon.stories.tsx b/packages/ui/src/icons/icon/icon.stories.tsx
index bd06cf3dcc6..fd38c58d747 100644
--- a/packages/ui/src/icons/icon/icon.stories.tsx
+++ b/packages/ui/src/icons/icon/icon.stories.tsx
@@ -15,8 +15,10 @@ const names = [
"magnifying-glass",
"menu",
"monitor",
+ "outline-arrow-to-corner-top-right",
"outline-copy",
"outline-dots",
+ "outline-hexagonal-warning",
"plus",
"review",
"settings-gear",
diff --git a/packages/ui/src/icons/icon/icon.tsx b/packages/ui/src/icons/icon/icon.tsx
index 9e6941f45c3..43274d4f4ee 100644
--- a/packages/ui/src/icons/icon/icon.tsx
+++ b/packages/ui/src/icons/icon/icon.tsx
@@ -152,6 +152,14 @@ const icons = {
viewBox: "0 0 16 16",
body: ``,
},
+ "outline-arrow-to-corner-top-right": {
+ viewBox: "0 0 16 16",
+ body: ``,
+ },
+ "outline-hexagonal-warning": {
+ viewBox: "0 0 16 16",
+ body: ``,
+ },
"outline-share": {
viewBox: "0 0 16 16",
body: ``,
@@ -187,10 +195,18 @@ function getIcon(name: IconName) {
return { body, viewBox: additionalIconViewBox(name as keyof typeof additionalIcons) }
}
+function spriteContent() {
+ return Array.from(new Set([...Object.keys(additionalIcons), ...Object.keys(icons)] as IconName[]))
+ .map((name) => `${getIcon(name).body}`)
+ .join("")
+}
+
function ensureSprite() {
if (spriteInserted) return
if (typeof document === "undefined") return
- if (document.getElementById(spriteID)) {
+ const existing = document.getElementById(spriteID)
+ if (existing) {
+ existing.innerHTML = spriteContent()
spriteInserted = true
return
}
@@ -202,9 +218,7 @@ function ensureSprite() {
svg.setAttribute("height", "0")
svg.style.position = "absolute"
svg.style.overflow = "hidden"
- svg.innerHTML = Array.from(new Set([...Object.keys(additionalIcons), ...Object.keys(icons)] as IconName[]))
- .map((name) => `${getIcon(name).body}`)
- .join("")
+ svg.innerHTML = spriteContent()
document.body.insertBefore(svg, document.body.firstChild)
spriteInserted = true
}