11// Entry and exit splash banners for direct interactive mode scrollback.
22//
3- // Renders the opencode ASCII logo with half-block shadow characters, the
4- // session title, and contextual hints (entry: "/exit to finish", exit:
5- // "opencode -s <id>" to resume). These are scrollback snapshots, so they
6- // become immutable terminal history once committed.
3+ // Renders the full opencode entry logo and a compact [O] exit badge, plus
4+ // session metadata and the resume command. These are scrollback snapshots, so
5+ // they become immutable terminal history once committed.
76//
8- // The logo uses a cell-based renderer. cells() classifies each character
9- // in the logo template as text, full-block, half-block-mix, or
7+ // Both variants use a cell-based renderer. cells() classifies each character
8+ // in the source template as text, full-block, half-block-mix, or
109// half-block-top, and draw() renders it with foreground/background shadow
1110// colors from the theme.
1211import {
@@ -20,7 +19,7 @@ import {
2019 type ScrollbackWriter ,
2120} from "@opentui/core"
2221import * as Locale from "@/util/locale"
23- import { logo } from "@/cli/logo"
22+ import { go , logo } from "@/cli/logo"
2423import type { RunSplashTheme } from "./theme"
2524
2625export const SPLASH_TITLE_LIMIT = 50
@@ -77,11 +76,27 @@ function title(text: string | undefined): string {
7776 return SPLASH_TITLE_FALLBACK
7877 }
7978
80- if ( ! text . trim ( ) ) {
79+ let value = ""
80+ let gap = false
81+ for ( const char of text . trim ( ) ) {
82+ if ( char === " " || char === "\n" || char === "\r" || char === "\t" ) {
83+ gap = true
84+ continue
85+ }
86+
87+ if ( gap && value . length > 0 ) {
88+ value += " "
89+ }
90+
91+ value += char
92+ gap = false
93+ }
94+
95+ if ( ! value ) {
8196 return SPLASH_TITLE_FALLBACK
8297 }
8398
84- return Locale . truncate ( text . trim ( ) , SPLASH_TITLE_LIMIT )
99+ return Locale . truncate ( value , SPLASH_TITLE_LIMIT )
85100}
86101
87102function write (
@@ -188,58 +203,66 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback
188203 const left = color ( input . theme . left , fallback ( 81 , "#38bdf8" ) )
189204 const right = color ( input . theme . right , RGBA . defaultForeground ( RGBA . fromHex ( "#f8fafc" ) ) )
190205 const leftShadow = color ( input . theme . leftShadow , fallback ( 238 , "#334155" ) )
191- const rightShadow = color ( input . theme . rightShadow , fallback ( 240 , "#475569" ) )
192- let y = 0
193-
194- for ( let i = 0 ; i < logo . left . length ; i += 1 ) {
195- const leftText = logo . left [ i ] ?? ""
196- const rightText = logo . right [ i ] ?? ""
197-
198- draw ( lines , leftText , {
199- left : 0 ,
200- top : y ,
201- fg : left ,
202- shadow : leftShadow ,
203- } )
204- draw ( lines , rightText , {
205- left : leftText . length + 1 ,
206- top : y ,
207- fg : right ,
208- shadow : rightShadow ,
209- } )
210- y += 1
211- }
206+ let height = 1
212207
213- y += 1
208+ if ( kind === "entry" ) {
209+ const rightShadow = color ( input . theme . rightShadow , fallback ( 240 , "#475569" ) )
210+
211+ for ( let i = 0 ; i < logo . left . length ; i += 1 ) {
212+ const leftText = logo . left [ i ] ?? ""
213+ const rightText = logo . right [ i ] ?? ""
214+
215+ draw ( lines , leftText , {
216+ left : 0 ,
217+ top : i ,
218+ fg : left ,
219+ shadow : leftShadow ,
220+ } )
221+ draw ( lines , rightText , {
222+ left : leftText . length + 1 ,
223+ top : i ,
224+ fg : right ,
225+ shadow : rightShadow ,
226+ } )
227+ }
214228
215- if ( input . showSession !== false ) {
216- const label = "Session" . padEnd ( 10 , " " )
217- push ( lines , 0 , y , label , input . theme . left , undefined , TextAttributes . DIM )
218- push ( lines , label . length , y , meta . title , input . theme . right , undefined , TextAttributes . BOLD )
219- y += 1
220- }
229+ height = logo . left . length + 1
221230
222- if ( kind === "entry" ) {
223- push ( lines , 0 , y , "Type /exit to finish." , input . theme . left , undefined , undefined )
224- y += 1
231+ if ( input . showSession !== false ) {
232+ const top = logo . left . length + 1
233+ const label = "Session" . padEnd ( 10 , " " )
234+ push ( lines , 0 , top , label , left , undefined , TextAttributes . DIM )
235+ push ( lines , label . length , top , meta . title , right , undefined , TextAttributes . BOLD )
236+ height = top + 1
237+ }
225238 }
226239
227240 if ( kind === "exit" ) {
228- const next = "Continue" . padEnd ( 10 , " " )
229- push ( lines , 0 , y , next , input . theme . left , undefined , TextAttributes . DIM )
230- push (
231- lines ,
232- next . length ,
233- y ,
234- `opencode -s ${ meta . session_id } ` ,
235- input . theme . right ,
236- undefined ,
237- TextAttributes . BOLD ,
238- )
239- y += 1
241+ const mark = go . right . slice ( 1 )
242+ const top = 1
243+ const body_left = ( mark [ 0 ] ?. length ?? 0 ) + 2
244+ const session = "Session "
245+ const label = "Continue "
246+
247+ for ( let i = 0 ; i < mark . length ; i += 1 ) {
248+ draw ( lines , mark [ i ] ?? "" , {
249+ left : 0 ,
250+ top : top + i ,
251+ fg : left ,
252+ shadow : leftShadow ,
253+ } )
254+ }
255+
256+ if ( input . showSession !== false ) {
257+ push ( lines , body_left , top , session , left , undefined , TextAttributes . DIM )
258+ push ( lines , body_left + session . length , top , meta . title , right , undefined , TextAttributes . BOLD )
259+ }
260+
261+ push ( lines , body_left , top + 1 , label , left , undefined , TextAttributes . DIM )
262+ push ( lines , body_left + label . length , top + 1 , `opencode run -i -s ${ meta . session_id } ` , right , undefined , TextAttributes . BOLD )
263+ height = top + mark . length
240264 }
241265
242- const height = Math . max ( 1 , y )
243266 const root = new BoxRenderable ( ctx . renderContext , {
244267 id : `run-direct-splash-${ kind } -${ id ++ } ` ,
245268 position : "absolute" ,
0 commit comments