Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 39 additions & 2 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1302,7 +1302,9 @@ export function Prompt(props: PromptProps) {
flexDirection="row"
gap={1}
flexGrow={1}
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
justifyContent={
status().type === "retry" || status().type === "reconnecting" ? "space-between" : "flex-start"
}
>
<box flexShrink={0} flexDirection="row" gap={1}>
<box marginLeft={1}>
Expand Down Expand Up @@ -1367,6 +1369,41 @@ export function Prompt(props: PromptProps) {
</Show>
)
})()}
{(() => {
const reconnecting = createMemo(() => {
const s = status()
if (s.type !== "reconnecting") return
return s
})
const [visible, setVisible] = createSignal(false)
let timer: ReturnType<typeof setTimeout> | undefined
createEffect(() => {
const r = reconnecting()
if (r) {
timer = setTimeout(() => setVisible(true), 1000)
} else {
clearTimeout(timer)
setVisible(false)
}
})
onCleanup(() => clearTimeout(timer))
const msg = createMemo(() => {
const r = reconnecting()
if (!r) return
if (r.message.length > 80) return r.message.slice(0, 80) + "..."
return r.message
})

return (
<Show when={visible() && reconnecting()}>
<box>
<text fg={theme.warning}>
{msg()} [reconnecting attempt #{reconnecting()?.attempt}]
</text>
</box>
</Show>
)
})()}
</box>
</box>
<text fg={store.interrupt > 0 ? theme.primary : theme.text}>
Expand All @@ -1377,7 +1414,7 @@ export function Prompt(props: PromptProps) {
</text>
</box>
</Show>
<Show when={status().type !== "retry"}>
<Show when={status().type !== "retry" && status().type !== "reconnecting"}>
<box gap={2} flexDirection="row">
<Show when={editorFileLabelDisplay()}>{(file) => <text fg={theme.secondary}>{file()}</text>}</Show>
<Switch>
Expand Down
100 changes: 100 additions & 0 deletions packages/opencode/src/cli/cmd/tui/context/status-colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Status Color Convention
*
* Based on ISO 3864 safety colors and WCAG accessibility standards.
* Each state includes: color + icon + text for accessibility.
*
* @see https://www.iso.org/standard/51000.html (ISO 3864)
* @see https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html (WCAG 1.4.1)
*/

import { RGBA } from "@opentui/core"

export const STATUS_COLORS = {
running: {
color: "#3B82F6", // Blue
bg: "rgba(59, 130, 246, 0.15)",
icon: "◐",
text: "Ejecutando...",
description: "Task is currently executing",
},
waiting: {
color: "#F59E0B", // Yellow
bg: "rgba(245, 158, 11, 0.15)",
icon: "⏳",
text: "Esperando respuesta",
description: "Waiting for subagent response",
},
attention: {
color: "#D4652F", // Orange
bg: "rgba(212, 101, 47, 0.15)",
icon: "⚠",
text: "Requiere atención",
description: "Requires user attention",
},
error: {
color: "#EF4444", // Red
bg: "rgba(239, 68, 68, 0.15)",
icon: "✗",
text: "Error",
description: "An error occurred",
},
done: {
color: "#22C55E", // Green
bg: "rgba(34, 197, 94, 0.15)",
icon: "✓",
text: "Completado",
description: "Task completed successfully",
},
idle: {
color: "#6B7280", // Gray
bg: "rgba(107, 114, 128, 0.1)",
icon: "○",
text: "Inactivo",
description: "No activity",
},
} as const

export type StatusType = keyof typeof STATUS_COLORS

/**
* Get RGBA from hex color for theme integration
*/
export function statusColorToRgba(hex: string, alpha: number = 1): RGBA {
return RGBA.fromHex(hex).withAlpha(alpha)
}

/**
* Get RGBA background color for a status
*/
export function statusBackground(status: StatusType): RGBA {
const config = STATUS_COLORS[status]
const rgba = RGBA.fromHex(config.color)
return rgba.withAlpha(0.15)
}

/**
* Check if a status is "active" (not idle or done)
*/
export function isActiveStatus(status: StatusType): boolean {
return status !== "idle" && status !== "done"
}

/**
* Get toast variant mapping for existing toast system
*/
export function statusToToastVariant(status: StatusType): "error" | "warning" | "info" | "success" {
switch (status) {
case "error":
return "error"
case "attention":
case "waiting":
return "warning"
case "done":
return "success"
case "running":
case "idle":
default:
return "info"
}
}
1 change: 1 addition & 0 deletions packages/opencode/src/cli/cmd/tui/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const TuiEvent = {
Schema.Struct({
title: Schema.optional(Schema.String),
message: Schema.String,
projectName: Schema.optional(Schema.String).annotate({ description: "Project name for multi-project context" }),
variant: Schema.Literals(["info", "success", "warning", "error"]),
duration: Schema.optional(Schema.Number).annotate({ description: "Duration in milliseconds" }),
}),
Expand Down
181 changes: 181 additions & 0 deletions packages/opencode/src/cli/cmd/tui/ui/status-indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { type ParentProps, Show } from "solid-js"
import { useTheme } from "@tui/context/theme"
import { TextAttributes } from "@opentui/core"
import { STATUS_COLORS, type StatusType, statusColorToRgba } from "../context/status-colors"

/**
* Status Indicator Component
*
* Displays a colored indicator with icon and text for task states.
* Follows WCAG 1.4.1 accessibility guidelines - color + icon + text.
*
* Usage:
* ```tsx
* <StatusIndicator status="running" showLabel />
* <StatusIndicator status="error" />
* ```
*/
export function StatusIndicator(props: ParentProps<{
status: StatusType
showLabel?: boolean
showIcon?: boolean
size?: "small" | "medium" | "large"
}>) {
const { theme } = useTheme()

const config = () => STATUS_COLORS[props.status]
const size = () => props.size ?? "medium"

const padding = () => {
switch (size()) {
case "small":
return 0
case "large":
return 2
default:
return 1
}
}

const iconSize = () => {
switch (size()) {
case "small":
return 12
case "large":
return 16
default:
return 14
}
}

const textSize = () => {
switch (size()) {
case "small":
return 10
case "large":
return 14
default:
return 12
}
}

return (
<box
flexDirection="row"
alignItems="center"
gap={1}
paddingLeft={padding()}
paddingRight={padding()}
paddingTop={padding() / 2}
paddingBottom={padding() / 2}
backgroundColor={statusColorToRgba(config().color, 0.15)}
borderColor={statusColorToRgba(config().color, 0.5)}
border={["left"]}
>
<Show when={props.showIcon !== false}>
<text
fg={statusColorToRgba(config().color)}
fontSize={iconSize()}
>
{config().icon}
</text>
</Show>

<Show when={props.showLabel !== false}>
<text
fg={statusColorToRgba(config().color)}
attributes={TextAttributes.BOLD}
fontSize={textSize()}
>
{config().text}
</text>
</Show>

{props.children}
</box>
)
}

/**
* Project Status Badge
*
* Shows project name with status indicator.
* Useful for multi-project views.
*/
export function ProjectStatusBadge(props: {
projectName: string
status: StatusType
onClick?: () => void
}) {
const { theme } = useTheme()
const config = () => STATUS_COLORS[props.status]

return (
<box
flexDirection="row"
alignItems="center"
gap={1}
paddingLeft={1}
paddingRight={2}
paddingTop={1}
paddingBottom={1}
backgroundColor={statusColorToRgba(config().color, 0.2)}
borderColor={statusColorToRgba(config().color, 0.6)}
border={["left"]}
>
<text
fg={statusColorToRgba(config().color)}
fontSize={14}
>
{config().icon}
</text>

<text
fg={statusColorToRgba(config().color)}
attributes={TextAttributes.BOLD}
fontSize={12}
>
[{props.projectName}]
</text>

<text fg={theme.textMuted} fontSize={12}>
{config().text}
</text>
</box>
)
}

/**
* Session State Banner
*
* Full-width banner for session state changes.
* Appears at top of session view to indicate current state.
*/
export function SessionStateBanner(props: {
status: StatusType
projectName?: string
}) {
const { theme } = useTheme()
const config = () => STATUS_COLORS[props.status]

return (
<box
width="100%"
justifyContent="center"
alignItems="center"
paddingTop={1}
paddingBottom={1}
backgroundColor={statusColorToRgba(config().color, 0.1)}
borderColor={statusColorToRgba(config().color, 0.3)}
border={["bottom"]}
>
<text
fg={statusColorToRgba(config().color)}
attributes={TextAttributes.BOLD}
fontSize={14}
>
{config().icon} {props.projectName ? `[${props.projectName}] ` : ""}{config().text}
</text>
</box>
)
}
Loading
Loading