@@ -69,6 +69,7 @@ const pasteSessionPerLineGuard = 8 * time.Millisecond
6969const inlineLogMarker = "[[neo-log]] "
7070const sessionWorkdirMissingWarning = "Session workspace not found, keeping current workspace."
7171const localLogViewerPersistDir = "log-viewer"
72+ const transcriptBlockRenderCacheMax = 2048
7273
7374type sessionLogPersistenceRuntime interface {
7475 LoadSessionLogEntries (ctx context.Context , sessionID string ) ([]tuiservices.SessionLogEntry , error )
@@ -5591,20 +5592,63 @@ func (a *App) normalizeComposerHeight() {
55915592}
55925593
55935594func (a * App ) rebuildTranscript () {
5595+ a .rebuildTranscriptInternal (false )
5596+ }
5597+
5598+ func (a * App ) rebuildTranscriptForFoldToggle () {
5599+ a .rebuildTranscriptInternal (true )
5600+ }
5601+
5602+ func (a * App ) rebuildTranscriptInternal (foldToggleOnly bool ) {
55945603 width := max (24 , a .transcript .Width )
55955604 if len (a .activeMessages ) == 0 {
55965605 queued := a .renderQueuedInterventionBlock (width )
55975606 if strings .TrimSpace (queued ) == "" {
55985607 a .setTranscriptContent (a .styles .empty .Width (width ).Render (emptyConversationText ))
55995608 a .transcript .GotoTop ()
5609+ if foldToggleOnly {
5610+ a .transcriptScrollbarDrag = false
5611+ a .clearTextSelection ()
5612+ }
56005613 return
56015614 }
56025615 a .setTranscriptContent (queued )
56035616 a .transcript .GotoTop ()
5617+ if foldToggleOnly {
5618+ a .transcriptScrollbarDrag = false
5619+ a .clearTextSelection ()
5620+ }
56045621 return
56055622 }
56065623
56075624 atBottom := a .transcript .AtBottom ()
5625+ content , hasBlock := a .composeTranscriptContent (width )
5626+ if ! hasBlock {
5627+ a .setTranscriptContent (a .styles .empty .Width (width ).Render (emptyConversationText ))
5628+ a .transcript .GotoTop ()
5629+ if foldToggleOnly {
5630+ a .transcriptScrollbarDrag = false
5631+ a .clearTextSelection ()
5632+ }
5633+ return
5634+ }
5635+
5636+ a .setTranscriptContent (content )
5637+ if atBottom {
5638+ a .transcript .GotoBottom ()
5639+ }
5640+
5641+ if foldToggleOnly {
5642+ a .transcriptScrollbarDrag = false
5643+ a .clearTextSelection ()
5644+ maxOffset := a .transcriptMaxOffset ()
5645+ if a .transcript .YOffset > maxOffset {
5646+ a .transcript .SetYOffset (maxOffset )
5647+ }
5648+ }
5649+ }
5650+
5651+ func (a * App ) composeTranscriptContent (width int ) (string , bool ) {
56085652 foldSegments := findTranscriptProcessFoldSegments (a .activeMessages )
56095653 foldExists := len (foldSegments ) > 0
56105654 a .transcriptProcessFoldAvailable = foldExists
@@ -5667,7 +5711,7 @@ func (a *App) rebuildTranscript() {
56675711 if inlineLog && lastRenderedRole == roleAssistant {
56685712 continuation = true
56695713 }
5670- rendered , _ := a .renderMessageBlockWithCopy (message , width , 0 , ! continuation )
5714+ rendered := a .renderMessageBlockForTranscript (message , width , ! continuation )
56715715 if rendered == "" {
56725716 continue
56735717 }
@@ -5695,15 +5739,36 @@ func (a *App) rebuildTranscript() {
56955739 }
56965740
56975741 if ! hasBlock {
5698- a .setTranscriptContent (a .styles .empty .Width (width ).Render (emptyConversationText ))
5699- a .transcript .GotoTop ()
5700- return
5742+ return "" , false
57015743 }
57025744
5703- a .setTranscriptContent (builder .String ())
5704- if atBottom {
5705- a .transcript .GotoBottom ()
5745+ return builder .String (), true
5746+ }
5747+
5748+ func (a * App ) renderMessageBlockForTranscript (message providertypes.Message , width int , includeTag bool ) string {
5749+ if a .transcriptBlockRenderCache == nil {
5750+ a .transcriptBlockRenderCache = make (map [string ]string )
5751+ }
5752+ key := transcriptBlockRenderCacheKey (message , width , includeTag )
5753+ if cached , ok := a .transcriptBlockRenderCache [key ]; ok {
5754+ return cached
5755+ }
5756+
5757+ rendered , _ := a .renderMessageBlockWithCopy (message , width , 0 , includeTag )
5758+ if rendered == "" {
5759+ return ""
57065760 }
5761+ if len (a .transcriptBlockRenderCache ) >= transcriptBlockRenderCacheMax {
5762+ clear (a .transcriptBlockRenderCache )
5763+ }
5764+ a .transcriptBlockRenderCache [key ] = rendered
5765+ return rendered
5766+ }
5767+
5768+ func transcriptBlockRenderCacheKey (message providertypes.Message , width int , includeTag bool ) string {
5769+ content := renderMessagePartsForDisplay (message .Parts )
5770+ sum := sha256 .Sum256 ([]byte (content ))
5771+ return fmt .Sprintf ("%s|%t|%d|%t|%x" , message .Role , message .IsError , width , includeTag , sum [:8 ])
57075772}
57085773
57095774func (a * App ) toggleTranscriptProcessExpansion () bool {
@@ -5736,7 +5801,7 @@ func (a *App) toggleTranscriptProcessExpansionWithAnchor(anchorViewportRow int,
57365801 } else {
57375802 a .state .StatusText = "Process output collapsed"
57385803 }
5739- a .rebuildTranscript ()
5804+ a .rebuildTranscriptForFoldToggle ()
57405805 if anchorViewportRow >= 0 {
57415806 a .pinTranscriptProcessControlRow (anchorViewportRow , controlOrdinal )
57425807 }
@@ -6024,6 +6089,8 @@ func (a *App) handleImmediateSlashCommand(input string) (bool, tea.Cmd) {
60246089 return true , a .handleSkillCommand (rest )
60256090 case slashCommandCheckpoint :
60266091 return true , a .handleCheckpointCommand (rest )
6092+ case slashCommandWeb :
6093+ return true , a .handleWebCommand (rest )
60276094 case slashCommandSession :
60286095 if err := a .ensureSessionSwitchAllowed ("" ); err != nil {
60296096 a .state .ExecutionError = err .Error ()
@@ -6101,6 +6168,7 @@ func (a *App) startDraftSession() {
61016168 a .startupScreenLocked = false
61026169 a .state .ActiveSessionTitle = draftSessionTitle
61036170 a .activeMessages = nil
6171+ clear (a .transcriptBlockRenderCache )
61046172 a .transcriptProcessFoldAvailable = false
61056173 a .transcriptProcessExpanded = false
61066174 a .clearActivities ()
0 commit comments