11import {
22 batch ,
3- createContext ,
43 createEffect ,
54 createMemo ,
65 createSignal ,
@@ -12,7 +11,6 @@ import {
1211 onMount ,
1312 Show ,
1413 Switch ,
15- useContext ,
1614 type Accessor ,
1715} from "solid-js"
1816import path from "node:path"
@@ -29,7 +27,6 @@ import { createSyntaxStyleMemo, ThemeContextProvider, useTheme, useThemes } from
2927import { BoxRenderable , ScrollBoxRenderable , addDefaultParsers , TextAttributes , RGBA , MouseEvent } from "@opentui/core"
3028import { Prompt , type PromptRef } from "../../component/prompt"
3129import type {
32- ModelInfo ,
3330 SessionMessageInfo ,
3431 SessionMessageAssistant ,
3532 SessionMessageAssistantReasoning ,
@@ -111,6 +108,9 @@ import { createDelayedPresence } from "../../util/delayed-presence"
111108import { SessionLocationMissing } from "./location-missing"
112109import { isRecord } from "../../util/record"
113110import { createHistoryPrepend } from "./history"
111+ import { context , use , type PendingAction } from "./render-context"
112+ import { INLINE_TOOL_ICON_WIDTH , InlineToolRow , ReasoningPart , reasoningContent , TextPart } from "./message-parts"
113+ export { InlineToolRow } from "./message-parts"
114114
115115addDefaultParsers ( parsers . parsers )
116116
@@ -121,34 +121,6 @@ const BACKGROUND_TOOL_HINT_DELAY = 3_000
121121// The tail comfortably overfills a tall viewport; older rows mount as the reader approaches them.
122122const TRANSCRIPT_TAIL_ROWS = 40
123123const TRANSCRIPT_BACKFILL_CHUNK = 60
124- type PendingAction = "steer" | "queue" | "cancel"
125-
126- const context = createContext < {
127- /** Content width: terminal width minus vertical tabs, sidebar, and padding. */
128- width : number
129- /**
130- * Shared reactive terminal size. Transcript-row components must read this
131- * instead of calling useTerminalDimensions(), which registers one renderer
132- * resize listener per mounted component and grows with transcript length.
133- */
134- terminal : { width : number ; height : number }
135- sessionID : string
136- thinkingMode : ( ) => ThinkingMode
137- markdownMode : ( ) => "source" | "rendered"
138- groupExploration : ( ) => boolean
139- diffWrapMode : ( ) => "word" | "none"
140- models : ( ) => ModelInfo [ ]
141- messageIndex : ( messageID : string ) => number | undefined
142- config : ReturnType < typeof useConfig > [ "data" ]
143- mutatePending : ( action : PendingAction , inboxID : string ) => Promise < boolean >
144- pendingDelivery : ( inboxID : string ) => SessionInbox . Delivery | undefined
145- } > ( )
146-
147- function use ( ) {
148- const ctx = useContext ( context )
149- if ( ! ctx ) throw new Error ( "useContext must be used within a Session component" )
150- return ctx
151- }
152124
153125export function Session ( props : {
154126 scrollRef ?: ( scroll : ScrollBoxRenderable | undefined ) => void
@@ -2462,160 +2434,6 @@ function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
24622434 )
24632435}
24642436
2465- const INLINE_TOOL_ICON_WIDTH = 2
2466-
2467- function ReasoningPart ( props : {
2468- last : boolean
2469- part : SessionMessageAssistantReasoning
2470- message : SessionMessageAssistant
2471- } ) {
2472- const theme = useTheme ( )
2473- const { currentSyntax : syntax } = useThemes ( )
2474- const thinkingSyntax = createSyntaxStyleMemo ( ( ) => generateThinkingSyntax ( syntax ( ) , theme . text . subdued ) )
2475- const ctx = use ( )
2476- // Collapsed by default in hide mode: a single line throughout, so the
2477- // layout never shifts. Click to open the full markdown block, click to close.
2478- const [ expanded , setExpanded ] = createSignal ( false )
2479-
2480- const content = createMemo ( ( ) => reasoningContent ( props . part ) )
2481- const isDone = createMemo (
2482- ( ) => props . part . time ?. completed !== undefined || props . message . time . completed !== undefined ,
2483- )
2484- const inMinimal = createMemo ( ( ) => ctx . thinkingMode ( ) === "hide" )
2485- const duration = createMemo ( ( ) => {
2486- const end = props . part . time ?. completed ?? props . message . time . completed
2487- const start = props . part . time ?. created ?? props . message . time . created
2488- return end === undefined ? 0 : Math . max ( 0 , end - start )
2489- } )
2490- const summary = createMemo ( ( ) => reasoningSummary ( content ( ) ) )
2491- const toggle = ( ) => {
2492- if ( ! inMinimal ( ) ) return
2493- setExpanded ( ( prev ) => ! prev )
2494- }
2495-
2496- return (
2497- < Show when = { content ( ) } >
2498- < box paddingLeft = { 3 } flexDirection = "column" flexShrink = { 0 } >
2499- < box
2500- border = { ! inMinimal ( ) || expanded ( ) ? [ "left" ] : undefined }
2501- customBorderChars = { SplitBorder . customBorderChars }
2502- borderColor = { theme . raise ( theme . background . default ) }
2503- paddingLeft = { ! inMinimal ( ) || expanded ( ) ? 1 : 0 }
2504- >
2505- < box onMouseUp = { toggle } >
2506- < ReasoningHeader
2507- toggleable = { inMinimal ( ) }
2508- open = { ! inMinimal ( ) || expanded ( ) }
2509- done = { isDone ( ) }
2510- title = { inMinimal ( ) && ! expanded ( ) ? summary ( ) . title : null }
2511- duration = { isDone ( ) ? Locale . duration ( duration ( ) ) : undefined }
2512- />
2513- </ box >
2514- </ box >
2515- < Show when = { ! inMinimal ( ) || expanded ( ) } >
2516- < box marginTop = { 1 } >
2517- < box
2518- border = { [ "left" ] }
2519- customBorderChars = { SplitBorder . customBorderChars }
2520- borderColor = { theme . raise ( theme . background . default ) }
2521- paddingLeft = { inMinimal ( ) ? 3 : 1 }
2522- >
2523- < code
2524- filetype = "markdown"
2525- drawUnstyledText = { false }
2526- streaming = { true }
2527- syntaxStyle = { thinkingSyntax ( ) }
2528- content = { content ( ) }
2529- conceal = { ctx . markdownMode ( ) === "rendered" }
2530- fg = { theme . text . subdued }
2531- />
2532- </ box >
2533- </ box >
2534- </ Show >
2535- </ box >
2536- </ Show >
2537- )
2538- }
2539-
2540- function reasoningContent ( part : SessionMessageAssistantReasoning ) {
2541- // OpenRouter encrypts some reasoning blocks; drop the placeholder.
2542- return part . text . replace ( "[REDACTED]" , "" ) . trim ( )
2543- }
2544-
2545- function ReasoningHeader ( props : {
2546- toggleable : boolean
2547- open : boolean
2548- done : boolean
2549- title : string | null
2550- duration ?: string
2551- } ) {
2552- const theme = useTheme ( )
2553- const fg = ( ) =>
2554- props . open
2555- ? RGBA . fromValues (
2556- theme . text . feedback . warning . default . r ,
2557- theme . text . feedback . warning . default . g ,
2558- theme . text . feedback . warning . default . b ,
2559- 0.6 ,
2560- )
2561- : theme . text . feedback . warning . default
2562-
2563- return (
2564- < Switch >
2565- < Match when = { ! props . done } >
2566- < box flexDirection = "row" >
2567- < Spinner color = { fg ( ) } > { props . title ? "Thinking: " + props . title : "Thinking" } </ Spinner >
2568- </ box >
2569- </ Match >
2570- < Match when = { true } >
2571- < text fg = { fg ( ) } wrapMode = "none" >
2572- < Show when = { props . toggleable } >
2573- < span > { props . open ? "- " : "+ " } </ span >
2574- </ Show >
2575- < span > Thought</ span >
2576- < Show when = { props . title || props . duration } >
2577- < span > : </ span >
2578- </ Show >
2579- < Show when = { props . title } >
2580- < span > { props . title } </ span >
2581- </ Show >
2582- < Show when = { props . duration } >
2583- < span >
2584- { props . title ? " · " : "" }
2585- { props . duration }
2586- </ span >
2587- </ Show >
2588- </ text >
2589- </ Match >
2590- </ Switch >
2591- )
2592- }
2593-
2594- function TextPart ( props : { last : boolean ; part : SessionMessageAssistantText ; message : SessionMessageAssistant } ) {
2595- const ctx = use ( )
2596- const theme = useTheme ( )
2597- const { currentSyntax : syntax } = useThemes ( )
2598- const plugins = usePlugin ( )
2599- return (
2600- < Show when = { props . part . text . trim ( ) } >
2601- < box paddingLeft = { 3 } flexShrink = { 0 } >
2602- { /* Configure custom nodes before parsing; apply content before streaming so completion keeps the final tokens. */ }
2603- < markdown
2604- syntaxStyle = { syntax ( ) }
2605- renderNode = { plugins . markdown ( ) }
2606- content = { props . part . text . trim ( ) }
2607- streaming = { props . message . time . completed === undefined }
2608- internalBlockMode = "top-level"
2609- tableOptions = { { style : "grid" , cellPaddingX : 1 } }
2610- conceal = { ctx . markdownMode ( ) === "rendered" }
2611- fg = { theme . markdown . text }
2612- bg = { theme . background . default }
2613- />
2614- </ box >
2615- </ Show >
2616- )
2617- }
2618-
26192437// Pending messages moved to individual tool pending functions
26202438
26212439function ToolPart ( props : { part : SessionMessageAssistantTool ; images ?: boolean } ) {
@@ -2918,101 +2736,6 @@ function InlineTool(props: {
29182736 )
29192737}
29202738
2921- export function InlineToolRow ( props : {
2922- icon : string
2923- iconColor ?: RGBA
2924- color ?: RGBA
2925- errorColor ?: RGBA
2926- failed ?: boolean
2927- denied ?: boolean
2928- error ?: string
2929- errorExpanded ?: boolean
2930- complete : unknown
2931- pending : string
2932- failure ?: string
2933- spinner ?: boolean
2934- status ?: JSX . Element
2935- children : JSX . Element
2936- onMouseOver ?: ( ) => void
2937- onMouseOut ?: ( ) => void
2938- onMouseUp ?: ( ) => void
2939- } ) {
2940- return (
2941- < box paddingLeft = { 3 } onMouseOver = { props . onMouseOver } onMouseOut = { props . onMouseOut } onMouseUp = { props . onMouseUp } >
2942- < Switch >
2943- < Match when = { props . spinner } >
2944- < Show when = { props . status } fallback = { < Spinner color = { props . color } children = { props . children } /> } >
2945- { ( status ) => (
2946- < box flexDirection = "row" gap = { 1 } >
2947- < Spinner color = { props . color } />
2948- < InlineToolLabel color = { props . color } status = { status ( ) } >
2949- { props . children }
2950- </ InlineToolLabel >
2951- </ box >
2952- ) }
2953- </ Show >
2954- </ Match >
2955- < Match when = { true } >
2956- < Show fallback = { < Spinner color = { props . color } > { props . pending } </ Spinner > } when = { props . complete || props . failed } >
2957- < box flexDirection = "row" >
2958- < text
2959- width = { INLINE_TOOL_ICON_WIDTH }
2960- fg = { props . failed ? props . errorColor : ( props . iconColor ?? props . color ) }
2961- attributes = { props . denied ? TextAttributes . STRIKETHROUGH : undefined }
2962- >
2963- { props . icon }
2964- </ text >
2965- < Show
2966- when = { props . status }
2967- fallback = {
2968- < text
2969- flexGrow = { 1 }
2970- fg = { props . failed ? props . errorColor : props . color }
2971- attributes = { props . denied ? TextAttributes . STRIKETHROUGH : undefined }
2972- >
2973- { props . failed && ! props . complete ? ( props . failure ?? props . children ) : props . children }
2974- </ text >
2975- }
2976- >
2977- { ( status ) => (
2978- < InlineToolLabel
2979- color = { props . failed ? props . errorColor : props . color }
2980- denied = { props . denied }
2981- status = { status ( ) }
2982- >
2983- { props . failed && ! props . complete ? ( props . failure ?? props . children ) : props . children }
2984- </ InlineToolLabel >
2985- ) }
2986- </ Show >
2987- </ box >
2988- </ Show >
2989- </ Match >
2990- </ Switch >
2991- < Show when = { props . failed && props . errorExpanded } >
2992- < box paddingLeft = { INLINE_TOOL_ICON_WIDTH } >
2993- < text fg = { props . errorColor } > { props . error } </ text >
2994- </ box >
2995- </ Show >
2996- </ box >
2997- )
2998- }
2999-
3000- function InlineToolLabel ( props : { color ?: RGBA ; denied ?: boolean ; status : JSX . Element ; children : JSX . Element } ) {
3001- return (
3002- < box flexDirection = "row" flexWrap = "wrap" columnGap = { 1 } flexGrow = { 1 } >
3003- < text
3004- maxWidth = "100%"
3005- flexShrink = { 0 }
3006- fg = { props . color }
3007- attributes = { props . denied ? TextAttributes . STRIKETHROUGH : undefined }
3008- >
3009- { props . children }
3010- </ text >
3011- { props . status }
3012- </ box >
3013- )
3014- }
3015-
30162739function StatusBadge ( props : { children : string } ) {
30172740 const theme = useTheme ( )
30182741 return (
0 commit comments