@@ -5,7 +5,7 @@ import { Spinner } from "@tui/component/spinner"
55import { useTheme } from "@tui/context/theme"
66import { useLocal } from "@tui/context/local"
77import { useKeyboard , useRenderer , useTerminalDimensions , type JSX } from "@opentui/solid"
8- import type { SyntaxStyle } from "@opentui/core"
8+ import { TextAttributes , type BoxRenderable , type SyntaxStyle } from "@opentui/core"
99import { Locale } from "@/util/locale"
1010import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
1111import path from "path"
@@ -44,6 +44,10 @@ function View(props: { api: TuiPluginApi; sessionID: string }) {
4444 const messages = createMemo ( ( ) => sync . data . messages [ props . sessionID ] ?? [ ] )
4545 const renderedMessages = createMemo ( ( ) => messages ( ) . toReversed ( ) )
4646 const lastAssistant = createMemo ( ( ) => renderedMessages ( ) . findLast ( ( message ) => message . type === "assistant" ) )
47+ const lastUserCreated = ( index : number ) =>
48+ renderedMessages ( )
49+ . slice ( 0 , index )
50+ . findLast ( ( message ) => message . type === "user" ) ?. time . created
4751
4852 createEffect ( ( ) => {
4953 void sync . session . message . sync ( props . sessionID )
@@ -83,10 +87,11 @@ function View(props: { api: TuiPluginApi; sessionID: string }) {
8387 last = { lastAssistant ( ) ?. id === message . id }
8488 syntax = { syntax ( ) }
8589 subtleSyntax = { subtleSyntax ( ) }
90+ start = { lastUserCreated ( index ( ) ) }
8691 />
8792 </ Match >
8893 < Match when = { message . type === "synthetic" } >
89- < SyntheticMessage message = { message as SessionMessageSynthetic } index = { index ( ) } />
94+ < > < />
9095 </ Match >
9196 < Match when = { message . type === "shell" } >
9297 < ShellMessage message = { message as SessionMessageShell } />
@@ -146,63 +151,36 @@ function UserMessage(props: { message: SessionMessageUser; index: number }) {
146151 < box
147152 id = { props . message . id }
148153 border = { [ "left" ] }
149- borderColor = { theme . primary }
154+ borderColor = { theme . secondary }
150155 customBorderChars = { SplitBorder . customBorderChars }
151156 marginTop = { props . index === 0 ? 0 : 1 }
152157 flexShrink = { 0 }
153- >
154- < box paddingTop = { 1 } paddingBottom = { 1 } paddingLeft = { 2 } backgroundColor = { theme . backgroundPanel } >
155- < Show
156- when = { props . message . text . trim ( ) }
157- fallback = {
158- < MissingData label = "User message text" detail = { `Message ${ props . message . id } has no text field content.` } />
159- }
160- >
161- < text fg = { theme . text } > { props . message . text } </ text >
162- </ Show >
163- < Show when = { attachments ( ) . length } >
164- < box flexDirection = "row" paddingTop = { 1 } gap = { 1 } flexWrap = "wrap" >
165- < For each = { props . message . files ?? [ ] } >
166- { ( file ) => (
167- < text fg = { theme . text } >
168- < span style = { { bg : theme . secondary , fg : theme . background } } > { file . mime } </ span >
169- < span style = { { bg : theme . backgroundElement , fg : theme . textMuted } } > { file . name ?? file . uri } </ span >
170- </ text >
171- ) }
172- </ For >
173- < For each = { props . message . agents ?? [ ] } >
174- { ( agent ) => (
175- < text fg = { theme . text } >
176- < span style = { { bg : theme . accent , fg : theme . background } } > agent </ span >
177- < span style = { { bg : theme . backgroundElement , fg : theme . textMuted } } > { agent . name } </ span >
178- </ text >
179- ) }
180- </ For >
181- </ box >
182- </ Show >
183- < text fg = { theme . textMuted } > { Locale . todayTimeOrDateTime ( props . message . time . created ) } </ text >
184- </ box >
185- </ box >
186- )
187- }
188-
189- function SyntheticMessage ( props : { message : SessionMessageSynthetic ; index : number } ) {
190- const { theme } = useTheme ( )
191- return (
192- < box
193- id = { props . message . id }
194- border = { [ "left" ] }
195- borderColor = { theme . backgroundElement }
196- customBorderChars = { SplitBorder . customBorderChars }
197- marginTop = { props . index === 0 ? 0 : 1 }
198- paddingLeft = { 2 }
199158 paddingTop = { 1 }
200159 paddingBottom = { 1 }
160+ paddingLeft = { 2 }
201161 backgroundColor = { theme . backgroundPanel }
202- flexShrink = { 0 }
203162 >
204- < text fg = { theme . textMuted } > Synthetic</ text >
205163 < text fg = { theme . text } > { props . message . text } </ text >
164+ < Show when = { attachments ( ) . length } >
165+ < box flexDirection = "row" paddingTop = { 1 } gap = { 1 } flexWrap = "wrap" >
166+ < For each = { props . message . files ?? [ ] } >
167+ { ( file ) => (
168+ < text fg = { theme . text } >
169+ < span style = { { bg : theme . secondary , fg : theme . background } } > { file . mime } </ span >
170+ < span style = { { bg : theme . backgroundElement , fg : theme . textMuted } } > { file . name ?? file . uri } </ span >
171+ </ text >
172+ ) }
173+ </ For >
174+ < For each = { props . message . agents ?? [ ] } >
175+ { ( agent ) => (
176+ < text fg = { theme . text } >
177+ < span style = { { bg : theme . accent , fg : theme . background } } > agent </ span >
178+ < span style = { { bg : theme . backgroundElement , fg : theme . textMuted } } > { agent . name } </ span >
179+ </ text >
180+ ) }
181+ </ For >
182+ </ box >
183+ </ Show >
206184 </ box >
207185 )
208186}
@@ -237,7 +215,7 @@ function ShellMessage(props: { message: SessionMessageShell }) {
237215}
238216
239217function CompactionMessage ( props : { message : SessionMessageCompaction } ) {
240- const { theme } = useTheme ( )
218+ const { theme, syntax } = useTheme ( )
241219 return (
242220 < box
243221 marginTop = { 1 }
@@ -248,7 +226,19 @@ function CompactionMessage(props: { message: SessionMessageCompaction }) {
248226 flexShrink = { 0 }
249227 >
250228 < Show when = { props . message . summary } >
251- < text fg = { theme . textMuted } > { props . message . summary } </ text >
229+ { ( summary ) => (
230+ < box paddingLeft = { 3 } paddingTop = { 1 } >
231+ < code
232+ filetype = "markdown"
233+ drawUnstyledText = { false }
234+ streaming = { false }
235+ syntaxStyle = { syntax ( ) }
236+ content = { summary ( ) . trim ( ) }
237+ conceal = { true }
238+ fg = { theme . text }
239+ />
240+ </ box >
241+ ) }
252242 </ Show >
253243 </ box >
254244 )
@@ -294,12 +284,13 @@ function AssistantMessage(props: {
294284 last : boolean
295285 syntax : SyntaxStyle
296286 subtleSyntax : SyntaxStyle
287+ start ?: number
297288} ) {
298289 const { theme } = useTheme ( )
299290 const local = useLocal ( )
300291 const duration = createMemo ( ( ) => {
301292 if ( ! props . message . time . completed ) return 0
302- return props . message . time . completed - props . message . time . created
293+ return props . message . time . completed - ( props . start ?? props . message . time . created )
303294 } )
304295 const model = createMemo ( ( ) => {
305296 const variant = props . message . model . variant ? `/${ props . message . model . variant } ` : ""
@@ -361,7 +352,7 @@ function AssistantText(props: { part: SessionMessageAssistantText; syntax: Synta
361352 const { theme } = useTheme ( )
362353 return (
363354 < Show when = { props . part . text . trim ( ) } >
364- < box paddingLeft = { 3 } marginTop = { 1 } flexShrink = { 0 } >
355+ < box paddingLeft = { 3 } marginTop = { 1 } flexShrink = { 0 } id = "text" >
365356 < code
366357 filetype = "markdown"
367358 drawUnstyledText = { false }
@@ -521,33 +512,93 @@ function InlineTool(props: {
521512 part : SessionMessageAssistantTool
522513} ) {
523514 const { theme } = useTheme ( )
515+ const renderer = useRenderer ( )
516+ const [ margin , setMargin ] = createSignal ( 0 )
517+ const [ hover , setHover ] = createSignal ( false )
518+ const [ showError , setShowError ] = createSignal ( false )
524519 const error = createMemo ( ( ) => ( props . part . state . status === "error" ? props . part . state . error . message : undefined ) )
520+ const complete = createMemo ( ( ) => ! ! props . complete )
525521 const denied = createMemo ( ( ) => {
526522 const message = error ( )
527523 if ( ! message ) return false
528524 return (
529525 message . includes ( "QuestionRejectedError" ) ||
530526 message . includes ( "rejected permission" ) ||
527+ message . includes ( "specified a rule" ) ||
531528 message . includes ( "user dismissed" )
532529 )
533530 } )
531+ const fg = createMemo ( ( ) => {
532+ if ( error ( ) ) return theme . error
533+ if ( complete ( ) ) return theme . textMuted
534+ return theme . text
535+ } )
536+ const attributes = createMemo ( ( ) => ( denied ( ) ? TextAttributes . STRIKETHROUGH : undefined ) )
534537 return (
535- < box marginTop = { 1 } paddingLeft = { 3 } flexShrink = { 0 } >
536- < Switch >
537- < Match when = { props . spinner } >
538- < Spinner color = { theme . text } > { props . children } </ Spinner >
539- </ Match >
540- < Match when = { true } >
541- < text paddingLeft = { 3 } fg = { props . complete ? theme . textMuted : theme . text } >
542- < Show fallback = { < > ~ { props . pending } </ > } when = { props . complete } >
543- { props . icon } { props . children }
544- </ Show >
545- </ text >
546- </ Match >
547- </ Switch >
548- < Show when = { error ( ) && ! denied ( ) } >
549- < text fg = { theme . error } > { error ( ) } </ text >
550- </ Show >
538+ < box
539+ marginTop = { margin ( ) }
540+ paddingLeft = { 3 }
541+ flexShrink = { 0 }
542+ flexDirection = "row"
543+ gap = { 1 }
544+ backgroundColor = { hover ( ) && error ( ) ? theme . backgroundMenu : undefined }
545+ onMouseOver = { ( ) => error ( ) && setHover ( true ) }
546+ onMouseOut = { ( ) => setHover ( false ) }
547+ onMouseUp = { ( ) => {
548+ if ( ! error ( ) ) return
549+ if ( renderer . getSelection ( ) ?. getSelectedText ( ) ) return
550+ setShowError ( ( prev ) => ! prev )
551+ } }
552+ renderBefore = { function ( ) {
553+ const el = this as BoxRenderable
554+ const parent = el . parent
555+ if ( ! parent ) return
556+ const previous = parent . getChildren ( ) [ parent . getChildren ( ) . indexOf ( el ) - 1 ]
557+ if ( ! previous ) {
558+ setMargin ( 0 )
559+ return
560+ }
561+ if ( previous . id . startsWith ( "text" ) ) setMargin ( 1 )
562+ } }
563+ >
564+ < box flexShrink = { 0 } >
565+ < Switch >
566+ < Match when = { props . spinner } >
567+ < Spinner color = { theme . text } />
568+ </ Match >
569+ < Match when = { complete ( ) } >
570+ < text fg = { fg ( ) } attributes = { attributes ( ) } >
571+ { props . icon }
572+ </ text >
573+ </ Match >
574+ < Match when = { true } >
575+ < text fg = { fg ( ) } attributes = { attributes ( ) } >
576+ ~
577+ </ text >
578+ </ Match >
579+ </ Switch >
580+ </ box >
581+ < box flexGrow = { 1 } >
582+ < box >
583+ < Switch >
584+ < Match when = { complete ( ) } >
585+ < text fg = { fg ( ) } attributes = { attributes ( ) } >
586+ { props . children }
587+ </ text >
588+ </ Match >
589+ < Match when = { true } >
590+ < text fg = { fg ( ) } attributes = { attributes ( ) } >
591+ { props . pending }
592+ </ text >
593+ </ Match >
594+ </ Switch >
595+ </ box >
596+ < Show when = { showError ( ) && error ( ) } >
597+ < box >
598+ < text fg = { theme . error } > { error ( ) } </ text >
599+ </ box >
600+ </ Show >
601+ </ box >
551602 </ box >
552603 )
553604}
0 commit comments