refactor(tui): migrate workflow views to V2 themes (#37526)
This commit is contained in:
parent
f2a4011371
commit
c7a7900ff2
15 changed files with 243 additions and 138 deletions
|
|
@ -420,7 +420,7 @@ function App(props: { pair?: DialogPairCredentials; started: number }) {
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const themeState = useTheme()
|
const themeState = useTheme()
|
||||||
const { theme, mode, setMode, locked, lock, unlock } = themeState
|
const { themeV2, mode, setMode, locked, lock, unlock } = themeState
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const exit = useExit()
|
const exit = useExit()
|
||||||
|
|
@ -1087,7 +1087,7 @@ function App(props: { pair?: DialogPairCredentials; started: number }) {
|
||||||
width={dimensions().width}
|
width={dimensions().width}
|
||||||
height={dimensions().height}
|
height={dimensions().height}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
backgroundColor={theme.background}
|
backgroundColor={themeV2.background()}
|
||||||
onMouseDown={(evt) => {
|
onMouseDown={(evt) => {
|
||||||
if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
|
if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
|
||||||
if (evt.button !== MouseButton.RIGHT) return
|
if (evt.button !== MouseButton.RIGHT) return
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import { For } from "solid-js"
|
import { createSignal, For } from "solid-js"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { DevTools } from "../devtools"
|
import { DevTools } from "../devtools"
|
||||||
|
|
||||||
export function DevToolsSidebar() {
|
export function DevToolsSidebar() {
|
||||||
const { themeV2 } = useTheme().contextual("elevated")
|
const { themeV2, mode, setMode } = useTheme().contextual("elevated")
|
||||||
|
const [modeHovered, setModeHovered] = createSignal(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
|
|
@ -16,6 +17,27 @@ export function DevToolsSidebar() {
|
||||||
paddingRight={2}
|
paddingRight={2}
|
||||||
backgroundColor={themeV2.background()}
|
backgroundColor={themeV2.background()}
|
||||||
>
|
>
|
||||||
|
<box flexShrink={0} marginBottom={1}>
|
||||||
|
<box marginBottom={1}>
|
||||||
|
<text fg={themeV2.text.action()} attributes={TextAttributes.BOLD}>
|
||||||
|
Theme
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<box flexDirection="row">
|
||||||
|
<text fg={themeV2.text.subdued()}>Mode</text>
|
||||||
|
<box flexGrow={1} />
|
||||||
|
<box
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
backgroundColor={modeHovered() ? themeV2.background.action("hovered") : undefined}
|
||||||
|
onMouseOver={() => setModeHovered(true)}
|
||||||
|
onMouseOut={() => setModeHovered(false)}
|
||||||
|
onMouseUp={() => setMode(mode() === "dark" ? "light" : "dark")}
|
||||||
|
>
|
||||||
|
<text fg={themeV2.text()}>{mode()}</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
<For each={DevTools.data()}>
|
<For each={DevTools.data()}>
|
||||||
{(group) => (
|
{(group) => (
|
||||||
<box flexShrink={0} marginBottom={1}>
|
<box flexShrink={0} marginBottom={1}>
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export function connectionSummary(integration: IntegrationInfo) {
|
||||||
export function DialogIntegration(props: { onConnected?: OnIntegrationConnected } = {}) {
|
export function DialogIntegration(props: { onConnected?: OnIntegrationConnected } = {}) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const options = createMemo(() =>
|
const options = createMemo(() =>
|
||||||
integrationOptions(data.location.integration.list() ?? []).map((integration) => {
|
integrationOptions(data.location.integration.list() ?? []).map((integration) => {
|
||||||
const methods = connectMethods(integration)
|
const methods = connectMethods(integration)
|
||||||
|
|
@ -74,7 +74,7 @@ export function DialogIntegration(props: { onConnected?: OnIntegrationConnected
|
||||||
footer: connectionSummary(integration) || undefined,
|
footer: connectionSummary(integration) || undefined,
|
||||||
category: integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services",
|
category: integration.id in INTEGRATION_PRIORITY ? "Popular" : "Services",
|
||||||
disabled: methods.length === 0,
|
disabled: methods.length === 0,
|
||||||
gutter: connected ? () => <text fg={theme.success}>✓</text> : undefined,
|
gutter: connected ? () => <text fg={themeV2.text.feedback.success()}>✓</text> : undefined,
|
||||||
onSelect: () =>
|
onSelect: () =>
|
||||||
credentialConnections(integration).length
|
credentialConnections(integration).length
|
||||||
? manageConnections(integration, methods, dialog, props.onConnected)
|
? manageConnections(integration, methods, dialog, props.onConnected)
|
||||||
|
|
@ -89,12 +89,12 @@ export function DialogIntegration(props: { onConnected?: OnIntegrationConnected
|
||||||
options={options()}
|
options={options()}
|
||||||
emptyView={
|
emptyView={
|
||||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
||||||
<text fg={theme.textMuted}>No integrations available</text>
|
<text fg={themeV2.text.subdued()}>No integrations available</text>
|
||||||
</box>
|
</box>
|
||||||
}
|
}
|
||||||
noMatchView={
|
noMatchView={
|
||||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
||||||
<text fg={theme.textMuted}>No integrations found</text>
|
<text fg={themeV2.text.subdued()}>No integrations found</text>
|
||||||
</box>
|
</box>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -289,23 +289,30 @@ function CommandPending(props: {
|
||||||
|
|
||||||
function CommandView(props: { title: string; output: string; message: string }) {
|
function CommandView(props: { title: string; output: string; message: string }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
|
const { themeV2: overlayTheme } = useTheme().contextual("overlay")
|
||||||
onMount(() => dialog.setSize("large"))
|
onMount(() => dialog.setSize("large"))
|
||||||
return (
|
return (
|
||||||
<box gap={1} paddingBottom={1}>
|
<box gap={1} paddingBottom={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
|
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
|
||||||
{props.title}
|
{props.title}
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
|
||||||
esc close
|
esc close
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<box backgroundColor={theme.backgroundElement} paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
|
<box
|
||||||
<text fg={theme.text}>{props.output.trim()}</text>
|
backgroundColor={overlayTheme.background()}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
>
|
||||||
|
<text fg={overlayTheme.text()}>{props.output.trim()}</text>
|
||||||
</box>
|
</box>
|
||||||
<box paddingLeft={2} paddingRight={2}>
|
<box paddingLeft={2} paddingRight={2}>
|
||||||
<text fg={theme.textMuted}>{props.message}</text>
|
<text fg={themeV2.text.subdued()}>{props.message}</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
|
|
@ -320,7 +327,7 @@ function KeyMethod(props: {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const [error, setError] = createSignal<string>()
|
const [error, setError] = createSignal<string>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -338,7 +345,9 @@ function KeyMethod(props: {
|
||||||
.then(() => connected(props.integration, data, dialog, toast, props.onConnected))
|
.then(() => connected(props.integration, data, dialog, toast, props.onConnected))
|
||||||
.catch((cause) => setError(message(cause)))
|
.catch((cause) => setError(message(cause)))
|
||||||
}}
|
}}
|
||||||
description={() => <Show when={error()}>{(value) => <text fg={theme.error}>{value()}</text>}</Show>}
|
description={() => (
|
||||||
|
<Show when={error()}>{(value) => <text fg={themeV2.text.feedback.error()}>{value()}</text>}</Show>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -493,7 +502,7 @@ function OAuthCode(props: {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const [error, setError] = createSignal<string>()
|
const [error, setError] = createSignal<string>()
|
||||||
let settled = false
|
let settled = false
|
||||||
|
|
||||||
|
|
@ -527,9 +536,9 @@ function OAuthCode(props: {
|
||||||
}}
|
}}
|
||||||
description={() => (
|
description={() => (
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<text fg={theme.textMuted}>{props.attempt.instructions}</text>
|
<text fg={themeV2.text.subdued()}>{props.attempt.instructions}</text>
|
||||||
<Link href={props.attempt.url} fg={theme.primary} />
|
<Link href={props.attempt.url} fg={themeV2.markdown.link()} />
|
||||||
<Show when={error()}>{(value) => <text fg={theme.error}>{value()}</text>}</Show>
|
<Show when={error()}>{(value) => <text fg={themeV2.text.feedback.error()}>{value()}</text>}</Show>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -538,31 +547,31 @@ function OAuthCode(props: {
|
||||||
|
|
||||||
function OAuthView(props: { title: string; url?: string; instructions?: string; message: string; copy?: boolean }) {
|
function OAuthView(props: { title: string; url?: string; instructions?: string; message: string; copy?: boolean }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
return (
|
return (
|
||||||
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
|
||||||
{props.title}
|
{props.title}
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
|
||||||
esc
|
esc
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<Show when={props.url}>
|
<Show when={props.url}>
|
||||||
{(url) => (
|
{(url) => (
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<Link href={url()} fg={theme.primary} />
|
<Link href={url()} fg={themeV2.markdown.link()} />
|
||||||
<Show when={props.instructions}>
|
<Show when={props.instructions}>
|
||||||
{(instructions) => <text fg={theme.textMuted}>{instructions()}</text>}
|
{(instructions) => <text fg={themeV2.text.subdued()}>{instructions()}</text>}
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<text fg={theme.textMuted}>{props.message}</text>
|
<text fg={themeV2.text.subdued()}>{props.message}</text>
|
||||||
<Show when={props.copy}>
|
<Show when={props.copy}>
|
||||||
<text fg={theme.text}>
|
<text fg={themeV2.text()}>
|
||||||
c <span style={{ fg: theme.textMuted }}>copy</span>
|
c <span style={{ fg: themeV2.text.subdued() }}>copy</span>
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Keymap } from "../context/keymap"
|
||||||
import { pipe, sortBy } from "remeda"
|
import { pipe, sortBy } from "remeda"
|
||||||
import { DialogSelect } from "../ui/dialog-select"
|
import { DialogSelect } from "../ui/dialog-select"
|
||||||
import { useDialog } from "../ui/dialog"
|
import { useDialog } from "../ui/dialog"
|
||||||
import { useTheme, type Theme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
||||||
import type { McpServer } from "@opencode-ai/client"
|
import type { McpServer } from "@opencode-ai/client"
|
||||||
import { useClipboard } from "../context/clipboard"
|
import { useClipboard } from "../context/clipboard"
|
||||||
|
|
@ -12,30 +12,59 @@ import { useToast } from "../ui/toast"
|
||||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
||||||
import { useConfig } from "../config"
|
import { useConfig } from "../config"
|
||||||
import { getScrollAcceleration } from "../util/scroll"
|
import { getScrollAcceleration } from "../util/scroll"
|
||||||
|
import type { ComponentTheme } from "../theme/v2/component"
|
||||||
|
|
||||||
// Sort by how much attention a server needs: auth prompts first, then failures,
|
// Sort by how much attention a server needs: auth prompts first, then failures,
|
||||||
// then healthy servers, and intentionally-off servers last.
|
// then healthy servers, and intentionally-off servers last.
|
||||||
function statusMeta(status: McpServer["status"], theme: Theme) {
|
function statusMeta(status: McpServer["status"], themeV2: ComponentTheme) {
|
||||||
switch (status.status) {
|
switch (status.status) {
|
||||||
case "needs_auth":
|
case "needs_auth":
|
||||||
return { rank: 0, icon: "!", label: "Needs authentication", color: theme.warning, error: undefined, bold: false }
|
return {
|
||||||
|
rank: 0,
|
||||||
|
icon: "!",
|
||||||
|
label: "Needs authentication",
|
||||||
|
color: themeV2.text.feedback.warning(),
|
||||||
|
error: undefined,
|
||||||
|
bold: false,
|
||||||
|
}
|
||||||
case "needs_client_registration":
|
case "needs_client_registration":
|
||||||
return { rank: 1, icon: "✗", label: "Needs registration", color: theme.error, error: status.error, bold: false }
|
return {
|
||||||
|
rank: 1,
|
||||||
|
icon: "✗",
|
||||||
|
label: "Needs registration",
|
||||||
|
color: themeV2.text.feedback.error(),
|
||||||
|
error: status.error,
|
||||||
|
bold: false,
|
||||||
|
}
|
||||||
case "failed":
|
case "failed":
|
||||||
return { rank: 2, icon: "✗", label: "Failed", color: theme.error, error: status.error, bold: false }
|
return {
|
||||||
|
rank: 2,
|
||||||
|
icon: "✗",
|
||||||
|
label: "Failed",
|
||||||
|
color: themeV2.text.feedback.error(),
|
||||||
|
error: status.error,
|
||||||
|
bold: false,
|
||||||
|
}
|
||||||
case "connected":
|
case "connected":
|
||||||
return { rank: 3, icon: "✓", label: "Connected", color: theme.success, error: undefined, bold: true }
|
return {
|
||||||
|
rank: 3,
|
||||||
|
icon: "✓",
|
||||||
|
label: "Connected",
|
||||||
|
color: themeV2.text.feedback.success(),
|
||||||
|
error: undefined,
|
||||||
|
bold: true,
|
||||||
|
}
|
||||||
case "pending":
|
case "pending":
|
||||||
return { rank: 4, icon: "◌", label: "Pending", color: theme.textMuted, error: undefined, bold: false }
|
return { rank: 4, icon: "◌", label: "Pending", color: themeV2.text.subdued(), error: undefined, bold: false }
|
||||||
default:
|
default:
|
||||||
return { rank: 5, icon: "○", label: "Disabled", color: theme.textMuted, error: undefined, bold: false }
|
return { rank: 5, icon: "○", label: "Disabled", color: themeV2.text.subdued(), error: undefined, bold: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DialogMcp() {
|
export function DialogMcp() {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const [focused, setFocused] = createSignal<string>()
|
const [focused, setFocused] = createSignal<string>()
|
||||||
const [detail, setDetail] = createSignal<McpServer>()
|
const [detail, setDetail] = createSignal<McpServer>()
|
||||||
|
|
||||||
|
|
@ -47,7 +76,7 @@ export function DialogMcp() {
|
||||||
pipe(
|
pipe(
|
||||||
data.location.mcp.server.list() ?? [],
|
data.location.mcp.server.list() ?? [],
|
||||||
sortBy(
|
sortBy(
|
||||||
(server) => statusMeta(server.status, theme).rank,
|
(server) => statusMeta(server.status, themeV2).rank,
|
||||||
(server) => server.name,
|
(server) => server.name,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -61,7 +90,7 @@ export function DialogMcp() {
|
||||||
|
|
||||||
const options = createMemo(() =>
|
const options = createMemo(() =>
|
||||||
servers().map((server) => {
|
servers().map((server) => {
|
||||||
const meta = statusMeta(server.status, theme)
|
const meta = statusMeta(server.status, themeV2)
|
||||||
return {
|
return {
|
||||||
value: server.name,
|
value: server.name,
|
||||||
title: server.name,
|
title: server.name,
|
||||||
|
|
@ -77,12 +106,12 @@ export function DialogMcp() {
|
||||||
const focusedError = createMemo(() => {
|
const focusedError = createMemo(() => {
|
||||||
const name = focused()
|
const name = focused()
|
||||||
const server = servers().find((entry) => entry.name === name)
|
const server = servers().find((entry) => entry.name === name)
|
||||||
return server ? statusMeta(server.status, theme).error : undefined
|
return server ? statusMeta(server.status, themeV2).error : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const open = (name: string | undefined) => {
|
const open = (name: string | undefined) => {
|
||||||
const server = servers().find((entry) => entry.name === name)
|
const server = servers().find((entry) => entry.name === name)
|
||||||
if (!server || !statusMeta(server.status, theme).error) return
|
if (!server || !statusMeta(server.status, themeV2).error) return
|
||||||
setDetail(server)
|
setDetail(server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,7 +129,7 @@ export function DialogMcp() {
|
||||||
onSelect={(option) => open(option.value as string)}
|
onSelect={(option) => open(option.value as string)}
|
||||||
footer={
|
footer={
|
||||||
<Show when={focusedError()}>
|
<Show when={focusedError()}>
|
||||||
<text fg={theme.textMuted}>enter to view error</text>
|
<text fg={themeV2.text.subdued()}>enter to view error</text>
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -116,11 +145,12 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const clipboard = useClipboard()
|
const clipboard = useClipboard()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
|
const { themeV2: overlayTheme } = useTheme().contextual("overlay")
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const config = useConfig().data
|
const config = useConfig().data
|
||||||
const [copied, setCopied] = createSignal(false)
|
const [copied, setCopied] = createSignal(false)
|
||||||
const error = () => statusMeta(props.server.status, theme).error ?? "Unknown MCP connection error"
|
const error = () => statusMeta(props.server.status, themeV2).error ?? "Unknown MCP connection error"
|
||||||
const height = createMemo(() => Math.max(3, Math.floor(dimensions().height / 2) - 5))
|
const height = createMemo(() => Math.max(3, Math.floor(dimensions().height / 2) - 5))
|
||||||
let scroll: ScrollBoxRenderable | undefined
|
let scroll: ScrollBoxRenderable | undefined
|
||||||
|
|
||||||
|
|
@ -152,29 +182,35 @@ function DialogMcpError(props: { server: McpServer; onBack: () => void }) {
|
||||||
return (
|
return (
|
||||||
<box paddingLeft={4} paddingRight={4} paddingBottom={1} gap={1}>
|
<box paddingLeft={4} paddingRight={4} paddingBottom={1} gap={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
|
||||||
MCP server: {props.server.name}
|
MCP server: {props.server.name}
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted} onMouseUp={props.onBack}>
|
<text fg={themeV2.text.subdued()} onMouseUp={props.onBack}>
|
||||||
esc back
|
esc back
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg={theme.error}>✗ Failed</text>
|
<text fg={themeV2.text.feedback.error()}>✗ Failed</text>
|
||||||
<box backgroundColor={theme.backgroundElement} paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
|
<box
|
||||||
|
backgroundColor={overlayTheme.background()}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
>
|
||||||
<scrollbox
|
<scrollbox
|
||||||
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
||||||
height={height()}
|
height={height()}
|
||||||
scrollbarOptions={{ visible: false }}
|
scrollbarOptions={{ visible: false }}
|
||||||
scrollAcceleration={getScrollAcceleration(config)}
|
scrollAcceleration={getScrollAcceleration(config)}
|
||||||
>
|
>
|
||||||
<text fg={theme.text} wrapMode="word">
|
<text fg={overlayTheme.text()} wrapMode="word">
|
||||||
{error()}
|
{error()}
|
||||||
</text>
|
</text>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text fg={theme.textMuted}>↑↓ scroll</text>
|
<text fg={themeV2.text.subdued()}>↑↓ scroll</text>
|
||||||
<text fg={theme.textMuted} onMouseUp={copy}>
|
<text fg={themeV2.text.subdued()} onMouseUp={copy}>
|
||||||
{copied() ? "✓ copied" : "c copy details"}
|
{copied() ? "✓ copied" : "c copy details"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const sessionData = useData()
|
const sessionData = useData()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
@ -172,16 +172,18 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
titleView: isRemoving ? (
|
titleView: isRemoving ? (
|
||||||
<span style={{ fg: theme.error }}>Deleting {item.location}</span>
|
<span style={{ fg: themeV2.text.feedback.error() }}>Deleting {item.location}</span>
|
||||||
) : deleting ? (
|
) : deleting ? (
|
||||||
<span style={{ fg: theme.text }}>Press {shortcuts.get("dialog.move_session.delete")} again to confirm</span>
|
<span style={{ fg: themeV2.text.action.destructive() }}>
|
||||||
|
Press {shortcuts.get("dialog.move_session.delete")} again to confirm
|
||||||
|
</span>
|
||||||
) : suffix ? (
|
) : suffix ? (
|
||||||
<>
|
<>
|
||||||
{visible.slice(0, split)}
|
{visible.slice(0, split)}
|
||||||
<span style={{ fg: theme.textMuted }}>{visible.slice(split)}</span>
|
<span style={{ fg: themeV2.text.subdued() }}>{visible.slice(split)}</span>
|
||||||
</>
|
</>
|
||||||
) : undefined,
|
) : undefined,
|
||||||
bg: deleting ? theme.error : undefined,
|
bg: deleting ? themeV2.background.action.destructive() : undefined,
|
||||||
value: {
|
value: {
|
||||||
type: "directory",
|
type: "directory",
|
||||||
directory: item.location,
|
directory: item.location,
|
||||||
|
|
@ -314,7 +316,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||||
title="Move session"
|
title="Move session"
|
||||||
titleView={
|
titleView={
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
<text fg={themeV2.text()} attributes={TextAttributes.BOLD}>
|
||||||
Move session
|
Move session
|
||||||
</text>
|
</text>
|
||||||
<Show when={working() || directories.loading || loadedProject.loading}>
|
<Show when={working() || directories.loading || loadedProject.loading}>
|
||||||
|
|
@ -327,25 +329,25 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||||
emptyView={
|
emptyView={
|
||||||
showError() ? (
|
showError() ? (
|
||||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
||||||
<text fg={theme.error} attributes={TextAttributes.BOLD}>
|
<text fg={themeV2.text.feedback.error()} attributes={TextAttributes.BOLD}>
|
||||||
Could not load project directories
|
Could not load project directories
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted}>{errorMessage(loadError())}</text>
|
<text fg={themeV2.text.subdued()}>{errorMessage(loadError())}</text>
|
||||||
<text fg={theme.textMuted}>Close and reopen Move session to try again.</text>
|
<text fg={themeV2.text.subdued()}>Close and reopen Move session to try again.</text>
|
||||||
</box>
|
</box>
|
||||||
) : directories.loading || loadedProject.loading ? (
|
) : directories.loading || loadedProject.loading ? (
|
||||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
||||||
<text fg={theme.textMuted}>Loading project directories…</text>
|
<text fg={themeV2.text.subdued()}>Loading project directories…</text>
|
||||||
</box>
|
</box>
|
||||||
) : (
|
) : (
|
||||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
||||||
<text fg={theme.textMuted}>No project directories available</text>
|
<text fg={themeV2.text.subdued()}>No project directories available</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
noMatchView={
|
noMatchView={
|
||||||
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
<box paddingLeft={4} paddingRight={4} paddingTop={1}>
|
||||||
<text fg={theme.textMuted}>No project directories found</text>
|
<text fg={themeV2.text.subdued()}>No project directories found</text>
|
||||||
</box>
|
</box>
|
||||||
}
|
}
|
||||||
locked={showError() || directories.loading || loadedProject.loading || Boolean(removing())}
|
locked={showError() || directories.loading || loadedProject.loading || Boolean(removing())}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { RGBA, TextAttributes } from "@opentui/core"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { Keymap } from "../context/keymap"
|
import { Keymap } from "../context/keymap"
|
||||||
import { selectedForeground, useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useDialog, type DialogContext } from "../ui/dialog"
|
import { useDialog, type DialogContext } from "../ui/dialog"
|
||||||
import { Link } from "../ui/link"
|
import { Link } from "../ui/link"
|
||||||
import { BgPulse } from "./bg-pulse"
|
import { BgPulse } from "./bg-pulse"
|
||||||
|
|
@ -38,10 +38,9 @@ function panelOverlay(color: RGBA) {
|
||||||
|
|
||||||
export function DialogRetryAction(props: DialogRetryActionProps) {
|
export function DialogRetryAction(props: DialogRetryActionProps) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const fg = selectedForeground(theme)
|
|
||||||
const showGoTreatment = () => props.link === GO_URL
|
const showGoTreatment = () => props.link === GO_URL
|
||||||
const textBg = () => (showGoTreatment() ? panelOverlay(theme.backgroundPanel) : undefined)
|
const textBg = () => (showGoTreatment() ? panelOverlay(themeV2.background()) : undefined)
|
||||||
const [selected, setSelected] = createSignal<"dismiss" | "action">("action")
|
const [selected, setSelected] = createSignal<"dismiss" | "action">("action")
|
||||||
|
|
||||||
Keymap.createLayer(() => ({
|
Keymap.createLayer(() => ({
|
||||||
|
|
@ -86,26 +85,26 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
|
||||||
) : null}
|
) : null}
|
||||||
<box zIndex={1} paddingLeft={PAD_X} paddingRight={PAD_X} paddingBottom={1} gap={1}>
|
<box zIndex={1} paddingLeft={PAD_X} paddingRight={PAD_X} paddingBottom={1} gap={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text} bg={textBg()}>
|
<text attributes={TextAttributes.BOLD} fg={themeV2.text()} bg={textBg()}>
|
||||||
{props.title}
|
{props.title}
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted} bg={textBg()} onMouseUp={() => dialog.clear()}>
|
<text fg={themeV2.text.subdued()} bg={textBg()} onMouseUp={() => dialog.clear()}>
|
||||||
esc
|
esc
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<box gap={0}>
|
<box gap={0}>
|
||||||
<text fg={theme.textMuted} bg={textBg()}>
|
<text fg={themeV2.text.subdued()} bg={textBg()}>
|
||||||
{props.message}
|
{props.message}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
{props.link ? (
|
{props.link ? (
|
||||||
showGoTreatment() ? (
|
showGoTreatment() ? (
|
||||||
<box alignItems="center" justifyContent="flex-end" height={7} paddingBottom={1}>
|
<box alignItems="center" justifyContent="flex-end" height={7} paddingBottom={1}>
|
||||||
<Link href={props.link} fg={theme.primary} bg={textBg()} wrapMode="none" />
|
<Link href={props.link} fg={themeV2.markdown.link()} bg={textBg()} wrapMode="none" />
|
||||||
</box>
|
</box>
|
||||||
) : (
|
) : (
|
||||||
<box width="100%" flexDirection="row" justifyContent="center" paddingBottom={1}>
|
<box width="100%" flexDirection="row" justifyContent="center" paddingBottom={1}>
|
||||||
<Link href={props.link} fg={theme.primary} wrapMode="none" />
|
<Link href={props.link} fg={themeV2.markdown.link()} wrapMode="none" />
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -115,12 +114,14 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
|
||||||
<box
|
<box
|
||||||
paddingLeft={2}
|
paddingLeft={2}
|
||||||
paddingRight={2}
|
paddingRight={2}
|
||||||
backgroundColor={selected() === "dismiss" ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
backgroundColor={
|
||||||
|
selected() === "dismiss" ? themeV2.background.action("focused") : RGBA.fromInts(0, 0, 0, 0)
|
||||||
|
}
|
||||||
onMouseOver={() => setSelected("dismiss")}
|
onMouseOver={() => setSelected("dismiss")}
|
||||||
onMouseUp={() => dismiss(props, dialog)}
|
onMouseUp={() => dismiss(props, dialog)}
|
||||||
>
|
>
|
||||||
<text
|
<text
|
||||||
fg={selected() === "dismiss" ? fg : theme.textMuted}
|
fg={selected() === "dismiss" ? themeV2.text.action("focused") : themeV2.text.subdued()}
|
||||||
bg={selected() === "dismiss" ? undefined : textBg()}
|
bg={selected() === "dismiss" ? undefined : textBg()}
|
||||||
attributes={selected() === "dismiss" ? TextAttributes.BOLD : undefined}
|
attributes={selected() === "dismiss" ? TextAttributes.BOLD : undefined}
|
||||||
>
|
>
|
||||||
|
|
@ -130,12 +131,12 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
|
||||||
<box
|
<box
|
||||||
paddingLeft={2}
|
paddingLeft={2}
|
||||||
paddingRight={2}
|
paddingRight={2}
|
||||||
backgroundColor={selected() === "action" ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
backgroundColor={selected() === "action" ? themeV2.background.action("focused") : RGBA.fromInts(0, 0, 0, 0)}
|
||||||
onMouseOver={() => setSelected("action")}
|
onMouseOver={() => setSelected("action")}
|
||||||
onMouseUp={() => runAction(props, dialog)}
|
onMouseUp={() => runAction(props, dialog)}
|
||||||
>
|
>
|
||||||
<text
|
<text
|
||||||
fg={selected() === "action" ? fg : theme.text}
|
fg={selected() === "action" ? themeV2.text.action("focused") : themeV2.text()}
|
||||||
bg={selected() === "action" ? undefined : textBg()}
|
bg={selected() === "action" ? undefined : textBg()}
|
||||||
attributes={selected() === "action" ? TextAttributes.BOLD : undefined}
|
attributes={selected() === "action" ? TextAttributes.BOLD : undefined}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export function DialogSessionDeleteFailed(props: {
|
||||||
onDone?: () => void
|
onDone?: () => void
|
||||||
}) {
|
}) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
active: "delete" as "delete" | "restore",
|
active: "delete" as "delete" | "restore",
|
||||||
})
|
})
|
||||||
|
|
@ -64,17 +64,17 @@ export function DialogSessionDeleteFailed(props: {
|
||||||
return (
|
return (
|
||||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
|
||||||
Failed to Delete Session
|
Failed to Delete Session
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
|
||||||
esc
|
esc
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg={theme.textMuted} wrapMode="word">
|
<text fg={themeV2.text.subdued()} wrapMode="word">
|
||||||
{`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
|
{`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted} wrapMode="word">
|
<text fg={themeV2.text.subdued()} wrapMode="word">
|
||||||
Choose how you want to recover this broken workspace session.
|
Choose how you want to recover this broken workspace session.
|
||||||
</text>
|
</text>
|
||||||
<box flexDirection="column" paddingBottom={1} gap={1}>
|
<box flexDirection="column" paddingBottom={1} gap={1}>
|
||||||
|
|
@ -86,7 +86,7 @@ export function DialogSessionDeleteFailed(props: {
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
paddingTop={1}
|
paddingTop={1}
|
||||||
paddingBottom={1}
|
paddingBottom={1}
|
||||||
backgroundColor={item.id === store.active ? theme.primary : undefined}
|
backgroundColor={item.id === store.active ? themeV2.background.action("focused") : undefined}
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
setStore("active", item.id)
|
setStore("active", item.id)
|
||||||
void confirm()
|
void confirm()
|
||||||
|
|
@ -94,11 +94,14 @@ export function DialogSessionDeleteFailed(props: {
|
||||||
>
|
>
|
||||||
<text
|
<text
|
||||||
attributes={TextAttributes.BOLD}
|
attributes={TextAttributes.BOLD}
|
||||||
fg={item.id === store.active ? theme.selectedListItemText : theme.text}
|
fg={item.id === store.active ? themeV2.text.action("focused") : themeV2.text()}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</text>
|
</text>
|
||||||
<text fg={item.id === store.active ? theme.selectedListItemText : theme.textMuted} wrapMode="word">
|
<text
|
||||||
|
fg={item.id === store.active ? themeV2.text.action("focused") : themeV2.text.subdued()}
|
||||||
|
wrapMode="word"
|
||||||
|
>
|
||||||
{item.description}
|
{item.description}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,15 @@ export function DialogWorkspaceFileChanges(props: {
|
||||||
message?: string
|
message?: string
|
||||||
}) {
|
}) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
|
const { themeV2: overlayTheme } = useTheme().contextual("overlay")
|
||||||
const config = useConfig().data
|
const config = useConfig().data
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
|
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
|
||||||
const [store, setStore] = createStore({ active: "yes" as WorkspaceFileChangesChoice })
|
const [store, setStore] = createStore({ active: "yes" as WorkspaceFileChangesChoice })
|
||||||
const height = createMemo(() => Math.min(props.files.length, 8))
|
const height = createMemo(() => Math.min(props.files.length, 8))
|
||||||
const fileNameWidth = createMemo(
|
const fileNameWidth = createMemo(() =>
|
||||||
() => Math.max(2, Math.min(60, dimensions().width - 2) - 6 - Math.max(7, ...props.files.map(changeCountWidth))),
|
Math.max(2, Math.min(60, dimensions().width - 2) - 6 - Math.max(7, ...props.files.map(changeCountWidth))),
|
||||||
)
|
)
|
||||||
|
|
||||||
function confirm() {
|
function confirm() {
|
||||||
|
|
@ -71,21 +72,21 @@ export function DialogWorkspaceFileChanges(props: {
|
||||||
return (
|
return (
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
|
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
|
||||||
{props.title ?? "File Changes Found"}
|
{props.title ?? "File Changes Found"}
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
|
||||||
esc
|
esc
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<box paddingLeft={2} paddingRight={2}>
|
<box paddingLeft={2} paddingRight={2}>
|
||||||
<text fg={theme.textMuted} wrapMode="word">
|
<text fg={themeV2.text.subdued()} wrapMode="word">
|
||||||
{props.message ?? "Do you want to move these changes with the session?"}
|
{props.message ?? "Do you want to move these changes with the session?"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<scrollbox
|
<scrollbox
|
||||||
height={height()}
|
height={height()}
|
||||||
backgroundColor={theme.backgroundElement}
|
backgroundColor={overlayTheme.background()}
|
||||||
scrollbarOptions={{ visible: false }}
|
scrollbarOptions={{ visible: false }}
|
||||||
scrollAcceleration={scrollAcceleration()}
|
scrollAcceleration={scrollAcceleration()}
|
||||||
>
|
>
|
||||||
|
|
@ -94,15 +95,19 @@ export function DialogWorkspaceFileChanges(props: {
|
||||||
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
|
<box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
|
||||||
<box flexDirection="row" minWidth={0} flexShrink={1}>
|
<box flexDirection="row" minWidth={0} flexShrink={1}>
|
||||||
<box width={2} flexShrink={0}>
|
<box width={2} flexShrink={0}>
|
||||||
<text fg={theme.textMuted}>{statusLabel(item.status)}</text>
|
<text fg={overlayTheme.text.subdued()}>{statusLabel(item.status)}</text>
|
||||||
</box>
|
</box>
|
||||||
<FilePath value={item.file} maxWidth={fileNameWidth()} fg={theme.textMuted} />
|
<FilePath value={item.file} maxWidth={fileNameWidth()} fg={overlayTheme.text.subdued()} />
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" gap={1} minWidth={7} flexShrink={0} justifyContent="flex-end">
|
<box flexDirection="row" gap={1} minWidth={7} flexShrink={0} justifyContent="flex-end">
|
||||||
<text>
|
<text>
|
||||||
{" "}
|
{" "}
|
||||||
{item.additions ? <span style={{ fg: theme.diffAdded }}>+{item.additions}</span> : null}
|
{item.additions ? (
|
||||||
{item.deletions ? <span style={{ fg: theme.diffRemoved }}> -{item.deletions}</span> : null}
|
<span style={{ fg: overlayTheme.diff.text.added() }}>+{item.additions}</span>
|
||||||
|
) : null}
|
||||||
|
{item.deletions ? (
|
||||||
|
<span style={{ fg: overlayTheme.diff.text.removed() }}> -{item.deletions}</span>
|
||||||
|
) : null}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
@ -115,14 +120,14 @@ export function DialogWorkspaceFileChanges(props: {
|
||||||
<box
|
<box
|
||||||
paddingLeft={2}
|
paddingLeft={2}
|
||||||
paddingRight={2}
|
paddingRight={2}
|
||||||
backgroundColor={item === store.active ? theme.primary : undefined}
|
backgroundColor={item === store.active ? themeV2.background.action("focused") : undefined}
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
setStore("active", item)
|
setStore("active", item)
|
||||||
props.onSelect(item)
|
props.onSelect(item)
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<text fg={item === store.active ? theme.selectedListItemText : theme.textMuted}>{item}</text>
|
<text fg={item === store.active ? themeV2.text.action("focused") : themeV2.text.subdued()}>{item}</text>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ function Commands(props: { context: Plugin.Context }) {
|
||||||
|
|
||||||
function Scrap(props: { context: Plugin.Context }) {
|
function Scrap(props: { context: Plugin.Context }) {
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme()
|
||||||
|
const { themeV2: elevatedTheme } = useTheme().contextual("elevated")
|
||||||
|
|
||||||
Keymap.createLayer(() => ({
|
Keymap.createLayer(() => ({
|
||||||
commands: [
|
commands: [
|
||||||
|
|
@ -42,19 +43,19 @@ function Scrap(props: { context: Plugin.Context }) {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box width={dimensions().width} height={dimensions().height} backgroundColor={theme.background}>
|
<box width={dimensions().width} height={dimensions().height} backgroundColor={themeV2.background()}>
|
||||||
<box flexGrow={1} />
|
<box flexGrow={1} />
|
||||||
<box
|
<box
|
||||||
height={1}
|
height={1}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
backgroundColor={theme.backgroundPanel}
|
backgroundColor={elevatedTheme.background()}
|
||||||
paddingLeft={1}
|
paddingLeft={1}
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
>
|
>
|
||||||
<text fg={theme.textMuted}>~/code/anomalyco/opencode</text>
|
<text fg={elevatedTheme.text.subdued()}>~/code/anomalyco/opencode</text>
|
||||||
<box flexGrow={1} />
|
<box flexGrow={1} />
|
||||||
<text fg={theme.textMuted}>esc home</text>
|
<text fg={elevatedTheme.text.subdued()}>esc home</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -267,7 +267,7 @@ function neutralAnchors(theme: Theme, mode: "light" | "dark") {
|
||||||
const light: { step: HueStep; color: RGBA }[] = [
|
const light: { step: HueStep; color: RGBA }[] = [
|
||||||
{ step: 100, color: theme.background },
|
{ step: 100, color: theme.background },
|
||||||
{ step: 200, color: theme.backgroundPanel },
|
{ step: 200, color: theme.backgroundPanel },
|
||||||
{ step: 300, color: theme.backgroundMenu },
|
{ step: 300, color: theme.backgroundElement || theme.backgroundMenu },
|
||||||
{ step: 700, color: theme.textMuted },
|
{ step: 700, color: theme.textMuted },
|
||||||
{ step: 900, color: theme.text },
|
{ step: 900, color: theme.text },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ type Active = ExportFormat | "debug" | "thinking" | "copy" | "export"
|
||||||
|
|
||||||
export function DialogExportOptions(props: DialogExportOptionsProps) {
|
export function DialogExportOptions(props: DialogExportOptionsProps) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
|
const { themeV2: overlayTheme } = useTheme().contextual("overlay")
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
format: "markdown" as ExportFormat,
|
format: "markdown" as ExportFormat,
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|
@ -75,25 +76,33 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
|
||||||
return (
|
return (
|
||||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
|
||||||
Export session
|
Export session
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
|
||||||
esc
|
esc
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text fg={theme.text}>Export as:</text>
|
<text fg={themeV2.text()}>Export as:</text>
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<For each={["markdown", "json"] as const}>
|
<For each={["markdown", "json"] as const}>
|
||||||
{(format) => (
|
{(format) => (
|
||||||
<box
|
<box
|
||||||
paddingLeft={1}
|
paddingLeft={1}
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
backgroundColor={store.format === format ? theme.backgroundElement : undefined}
|
backgroundColor={themeV2.background.formfield({
|
||||||
|
focused: store.active === format,
|
||||||
|
selected: store.format === format,
|
||||||
|
})}
|
||||||
onMouseUp={() => selectFormat(format)}
|
onMouseUp={() => selectFormat(format)}
|
||||||
>
|
>
|
||||||
<text fg={store.format === format ? theme.text : theme.textMuted}>
|
<text
|
||||||
|
fg={themeV2.text.formfield({
|
||||||
|
focused: store.active === format,
|
||||||
|
selected: store.format === format,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{store.format === format ? "◉" : "○"} {format === "markdown" ? "Markdown" : "JSON"}
|
{store.format === format ? "◉" : "○"} {format === "markdown" ? "Markdown" : "JSON"}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
@ -105,43 +114,60 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
|
||||||
<box
|
<box
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
gap={1}
|
gap={1}
|
||||||
backgroundColor={store.active === "thinking" ? theme.backgroundElement : undefined}
|
backgroundColor={themeV2.background.formfield({
|
||||||
|
focused: store.active === "thinking",
|
||||||
|
selected: store.thinking,
|
||||||
|
})}
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
setStore("active", "thinking")
|
setStore("active", "thinking")
|
||||||
setStore("thinking", !store.thinking)
|
setStore("thinking", !store.thinking)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<text fg={store.active === "thinking" ? theme.primary : theme.textMuted}>
|
<text fg={themeV2.text.formfield({ focused: store.active === "thinking", selected: store.thinking })}>
|
||||||
{store.thinking ? "[x]" : "[ ]"}
|
{store.thinking ? "[x]" : "[ ]"}
|
||||||
</text>
|
</text>
|
||||||
<text fg={store.active === "thinking" ? theme.primary : theme.text}>Include thinking</text>
|
<text fg={themeV2.text.formfield({ focused: store.active === "thinking", selected: store.thinking })}>
|
||||||
|
Include thinking
|
||||||
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={store.format === "json"}>
|
<Show when={store.format === "json"}>
|
||||||
<box
|
<box
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
gap={1}
|
gap={1}
|
||||||
backgroundColor={store.active === "debug" ? theme.backgroundElement : undefined}
|
backgroundColor={themeV2.background.formfield({
|
||||||
|
focused: store.active === "debug",
|
||||||
|
selected: store.debug,
|
||||||
|
})}
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
setStore("active", "debug")
|
setStore("active", "debug")
|
||||||
setStore("debug", !store.debug)
|
setStore("debug", !store.debug)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<text fg={store.active === "debug" ? theme.primary : theme.textMuted}>{store.debug ? "[x]" : "[ ]"}</text>
|
<text fg={themeV2.text.formfield({ focused: store.active === "debug", selected: store.debug })}>
|
||||||
<text fg={store.active === "debug" ? theme.primary : theme.text}>Events (debug)</text>
|
{store.debug ? "[x]" : "[ ]"}
|
||||||
|
</text>
|
||||||
|
<text fg={themeV2.text.formfield({ focused: store.active === "debug", selected: store.debug })}>
|
||||||
|
Events (debug)
|
||||||
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
|
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
|
||||||
<box
|
<box
|
||||||
paddingLeft={4}
|
paddingLeft={4}
|
||||||
paddingRight={4}
|
paddingRight={4}
|
||||||
backgroundColor={theme.backgroundElement}
|
backgroundColor={overlayTheme.background()}
|
||||||
onMouseUp={() => confirm("copy")}
|
onMouseUp={() => confirm("copy")}
|
||||||
>
|
>
|
||||||
<text fg={theme.text}>Copy</text>
|
<text fg={overlayTheme.text()}>Copy</text>
|
||||||
</box>
|
</box>
|
||||||
<box paddingLeft={4} paddingRight={4} backgroundColor={theme.primary} onMouseUp={() => confirm("export")}>
|
<box
|
||||||
<text fg={theme.selectedListItemText}>Export</text>
|
paddingLeft={4}
|
||||||
|
paddingRight={4}
|
||||||
|
backgroundColor={themeV2.background.action({ focused: store.active === "export" })}
|
||||||
|
onMouseUp={() => confirm("export")}
|
||||||
|
>
|
||||||
|
<text fg={themeV2.text.action({ focused: store.active === "export" })}>Export</text>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export type DialogPromptProps = {
|
||||||
|
|
||||||
export function DialogPrompt(props: DialogPromptProps) {
|
export function DialogPrompt(props: DialogPromptProps) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const shortcuts = Keymap.useShortcuts()
|
const shortcuts = Keymap.useShortcuts()
|
||||||
const [textareaTarget, setTextareaTarget] = createSignal<TextareaRenderable>()
|
const [textareaTarget, setTextareaTarget] = createSignal<TextareaRenderable>()
|
||||||
let textarea: TextareaRenderable
|
let textarea: TextareaRenderable
|
||||||
|
|
@ -74,10 +74,10 @@ export function DialogPrompt(props: DialogPromptProps) {
|
||||||
return (
|
return (
|
||||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
<text attributes={TextAttributes.BOLD} fg={themeV2.text()}>
|
||||||
{props.title}
|
{props.title}
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
<text fg={themeV2.text.subdued()} onMouseUp={() => dialog.clear()}>
|
||||||
esc
|
esc
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
@ -91,20 +91,20 @@ export function DialogPrompt(props: DialogPromptProps) {
|
||||||
}}
|
}}
|
||||||
initialValue={props.value}
|
initialValue={props.value}
|
||||||
placeholder={props.placeholder ?? "Enter text"}
|
placeholder={props.placeholder ?? "Enter text"}
|
||||||
placeholderColor={theme.textMuted}
|
placeholderColor={themeV2.text.subdued()}
|
||||||
textColor={props.busy ? theme.textMuted : theme.text}
|
textColor={themeV2.text.formfield({ disabled: props.busy })}
|
||||||
focusedTextColor={props.busy ? theme.textMuted : theme.text}
|
focusedTextColor={themeV2.text.formfield({ disabled: props.busy })}
|
||||||
cursorColor={props.busy ? theme.backgroundElement : theme.text}
|
cursorColor={props.busy ? themeV2.background.formfield("disabled") : themeV2.text()}
|
||||||
/>
|
/>
|
||||||
<Show when={props.busy}>
|
<Show when={props.busy}>
|
||||||
<Spinner color={theme.textMuted}>{props.busyText ?? "Working..."}</Spinner>
|
<Spinner color={themeV2.text.subdued()}>{props.busyText ?? "Working..."}</Spinner>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
<box paddingBottom={1} gap={1} flexDirection="row">
|
<box paddingBottom={1} gap={1} flexDirection="row">
|
||||||
<Show when={!props.busy} fallback={<text fg={theme.textMuted}>processing...</text>}>
|
<Show when={!props.busy} fallback={<text fg={themeV2.text.subdued()}>processing...</text>}>
|
||||||
<Show when={shortcuts.get("dialog.prompt.submit")}>
|
<Show when={shortcuts.get("dialog.prompt.submit")}>
|
||||||
<text fg={theme.text}>
|
<text fg={themeV2.text()}>
|
||||||
{shortcuts.get("dialog.prompt.submit")} <span style={{ fg: theme.textMuted }}>submit</span>
|
{shortcuts.get("dialog.prompt.submit")} <span style={{ fg: themeV2.text.subdued() }}>submit</span>
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export function Dialog(
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const { theme } = useTheme()
|
const { themeV2 } = useTheme().contextual("elevated")
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
|
|
||||||
let dismiss = false
|
let dismiss = false
|
||||||
|
|
@ -59,7 +59,7 @@ export function Dialog(
|
||||||
}}
|
}}
|
||||||
width={width()}
|
width={width()}
|
||||||
maxWidth={dimensions().width - 2}
|
maxWidth={dimensions().width - 2}
|
||||||
backgroundColor={theme.backgroundPanel}
|
backgroundColor={themeV2.background()}
|
||||||
paddingTop={1}
|
paddingTop={1}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ type ToastInput = Omit<ToastOptions, "duration"> & { duration?: number }
|
||||||
|
|
||||||
export function Toast() {
|
export function Toast() {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { theme, themeV2 } = useTheme()
|
const { themeV2 } = useTheme().contextual("overlay")
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -31,8 +31,8 @@ export function Toast() {
|
||||||
paddingRight={2}
|
paddingRight={2}
|
||||||
paddingTop={1}
|
paddingTop={1}
|
||||||
paddingBottom={1}
|
paddingBottom={1}
|
||||||
backgroundColor={theme.backgroundPanel}
|
backgroundColor={themeV2.background()}
|
||||||
borderColor={theme[current().variant]}
|
borderColor={themeV2.text.feedback[current().variant]()}
|
||||||
border={["left", "right"]}
|
border={["left", "right"]}
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
customBorderChars={SplitBorder.customBorderChars}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ test("infers chromatic hues, anchors light and dark colors, and aliases ambiguou
|
||||||
expect(() => resolveThemeFile(migrated, "dark")).not.toThrow()
|
expect(() => resolveThemeFile(migrated, "dark")).not.toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("builds gray from V1 surfaces and text without using borders", () => {
|
test("builds gray from V1 surfaces and text without using menus or borders", () => {
|
||||||
const source = structuredClone(DEFAULT_THEMES.opencode)
|
const source = structuredClone(DEFAULT_THEMES.opencode)
|
||||||
source.theme.backgroundMenu = { light: "#ededed", dark: "#252525" }
|
source.theme.backgroundMenu = { light: "#ededed", dark: "#252525" }
|
||||||
const light = resolveV1(source, "light")
|
const light = resolveV1(source, "light")
|
||||||
|
|
@ -97,12 +97,12 @@ test("builds gray from V1 surfaces and text without using borders", () => {
|
||||||
|
|
||||||
expect(lightGray[100]).toBe(hex(light.background))
|
expect(lightGray[100]).toBe(hex(light.background))
|
||||||
expect(lightGray[200]).toBe(hex(light.backgroundPanel))
|
expect(lightGray[200]).toBe(hex(light.backgroundPanel))
|
||||||
expect(lightGray[300]).toBe(hex(light.backgroundMenu))
|
expect(lightGray[300]).toBe(hex(light.backgroundElement))
|
||||||
expect(lightGray[700]).toBe(hex(light.textMuted))
|
expect(lightGray[700]).toBe(hex(light.textMuted))
|
||||||
expect(lightGray[900]).toBe(hex(light.text))
|
expect(lightGray[900]).toBe(hex(light.text))
|
||||||
expect(darkGray[100]).toBe(hex(dark.text))
|
expect(darkGray[100]).toBe(hex(dark.text))
|
||||||
expect(darkGray[300]).toBe(hex(dark.textMuted))
|
expect(darkGray[300]).toBe(hex(dark.textMuted))
|
||||||
expect(darkGray[700]).toBe(hex(dark.backgroundMenu))
|
expect(darkGray[700]).toBe(hex(dark.backgroundElement))
|
||||||
expect(darkGray[800]).toBe(hex(dark.backgroundPanel))
|
expect(darkGray[800]).toBe(hex(dark.backgroundPanel))
|
||||||
expect(darkGray[900]).toBe(hex(dark.background))
|
expect(darkGray[900]).toBe(hex(dark.background))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue