Repository navigation
feat(tui): crush-inspired gradient UI, session header, and prompt styling #89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
e89af16
6f830dc
ced99ed
670a60e
bd77e66
02b4a80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { TextAttributes } from "@opentui/core" | ||
| import { createMemo, For, Show } from "solid-js" | ||
| import { useSync } from "../../context/sync" | ||
| import { tint, useTheme } from "../../context/theme" | ||
| import { Locale } from "../../util/locale" | ||
|
|
||
| const BRAND = "BOLT CLI" | ||
|
|
||
| // Three-line session header: a gradient brand row with a thin rule, the | ||
| // current session line, and a subagent activity line. | ||
| export function SessionHeader(props: { sessionID: string; width: number }) { | ||
| const { theme } = useTheme() | ||
| const sync = useSync() | ||
|
|
||
| const session = createMemo(() => sync.session.get(props.sessionID)) | ||
| const sessions = createMemo(() => sync.data.session.filter((x) => !x.parentID)) | ||
| const subagents = createMemo(() => { | ||
| const parentID = session()?.parentID ?? props.sessionID | ||
| return sync.data.session.filter((x) => x.parentID === parentID).toSorted((a, b) => a.time.created - b.time.created) | ||
| }) | ||
| const working = (id: string) => (sync.data.session_status[id]?.type ?? "idle") !== "idle" | ||
|
|
||
| const gradient = (column: number) => tint(theme.accent, theme.primary, column / (BRAND.length - 1)) | ||
|
|
||
| const label = (title: string, max: number) => { | ||
| const name = title.match(/@(\w+) subagent/)?.[1] | ||
| const text = name ? Locale.titlecase(name) : title | ||
| return text.length > max ? `${text.slice(0, max - 1)}…` : text | ||
| } | ||
|
|
||
| return ( | ||
| <box flexShrink={0} paddingTop={1}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| <box flexDirection="row" gap={1}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| <text selectable={false}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| {Array.from(BRAND).map((char, column) => ( | ||
| <span style={{ fg: gradient(column), attributes: TextAttributes.BOLD }}>{char}</span> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ))} | ||
| </text> | ||
| <text fg={theme.border} selectable={false} wrapMode="none"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| {"─".repeat(Math.max(0, props.width - BRAND.length - 1))} | ||
| </text> | ||
| </box> | ||
| <box flexDirection="row" gap={1}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| <text fg={theme.primary}>●</text> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| <text fg={theme.text} wrapMode="none"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| {label(session()?.title ?? "untitled", Math.max(8, props.width - 20))} | ||
| </text> | ||
| <text fg={theme.textMuted} wrapMode="none"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| · {sessions().length} {sessions().length === 1 ? "session" : "sessions"} | ||
| </text> | ||
| </box> | ||
| <Show | ||
| when={subagents().length > 0} | ||
| fallback={ | ||
| <text fg={theme.textMuted} wrapMode="none"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| no subagent work | ||
| </text> | ||
| } | ||
| > | ||
| <box flexDirection="row" gap={2}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| <For each={subagents().slice(-3)}> | ||
| {(child) => ( | ||
| <box flexDirection="row" gap={1}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| <text fg={working(child.id) ? theme.success : theme.textMuted}>{working(child.id) ? "◉" : "○"}</text> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| <text fg={working(child.id) ? theme.text : theme.textMuted} wrapMode="none"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| {label(child.title, 24)} | ||
| </text> | ||
| </box> | ||
| )} | ||
| </For> | ||
| <Show when={subagents().length > 3}> | ||
| <text fg={theme.textMuted}>+{subagents().length - 3} more</text> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| </Show> | ||
| </box> | ||
| </Show> | ||
| </box> | ||
| ) | ||
| } | ||
|
Comment on lines
+11
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,7 @@ import { DialogTodos } from "../../component/dialog-todos" | |
|
|
||
| import { Sidebar } from "./sidebar" | ||
| import { SubagentFooter } from "./subagent-footer.tsx" | ||
| import { SessionHeader } from "./header.tsx" | ||
| import { filetype } from "../../util/filetype" | ||
| import parsers from "../../parsers-config" | ||
| import { errorMessage } from "../../util/error" | ||
|
|
@@ -1244,6 +1245,7 @@ export function Session() { | |
| <box flexDirection="row" flexGrow={1} minHeight={0}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| <box flexGrow={1} minHeight={0} paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| <Show when={session()}> | ||
| <SessionHeader sessionID={route.sessionID} width={contentWidth()} /> | ||
| <scrollbox | ||
| ref={(r) => (scroll = r)} | ||
| viewportOptions={{ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nesting JSX elements too deeply can confuse developers reading the code. To make maintenance and refactoring easier, DeepSource recommends limiting the maximum JSX tree depth to 4.