@@ -2236,6 +2236,33 @@ func (a *App) ResumeSession(path string) ([]HistoryMessage, error) {
22362236 return a .ResumeSessionForTab ("" , path )
22372237}
22382238
2239+ func (a * App ) ResumeSessionPage (path string , limit int ) (HistoryPage , error ) {
2240+ return a .ResumeSessionPageForTab ("" , path , limit )
2241+ }
2242+
2243+ func (a * App ) ResumeSessionPageForTab (tabID , path string , limit int ) (HistoryPage , error ) {
2244+ tab := a .tabByID (tabID )
2245+ if tab == nil || tab .Ctrl == nil {
2246+ return HistoryPage {}, fmt .Errorf ("tab is not ready" )
2247+ }
2248+ ctrl := tab .Ctrl
2249+ sessionPath , _ , err := validateSessionPath (controllerSessionDir (ctrl ), path )
2250+ if err != nil {
2251+ return HistoryPage {}, err
2252+ }
2253+ loaded , err := loadResumableSession (sessionPath )
2254+ if err != nil {
2255+ return HistoryPage {}, err
2256+ }
2257+ if sessionRuntimeKey (tab .currentSessionPath ()) != sessionRuntimeKey (sessionPath ) {
2258+ if err := a .rebindTabToLoadedSessionPath (tab , sessionPath , loaded ); err != nil {
2259+ return HistoryPage {}, err
2260+ }
2261+ }
2262+ a .setTabReadOnly (tab .ID , false )
2263+ return a .HistoryPageForTab (tab .ID , 0 , limit ), nil
2264+ }
2265+
22392266// ResumeSessionForTab is the tab-scoped form of ResumeSession. A saved session
22402267// path is a runtime identity, so changing to a different path must replace the
22412268// tab's controller binding rather than mutating the current controller in place.
@@ -2288,6 +2315,29 @@ func (a *App) OpenChannelSessionForTab(tabID, path string) ([]HistoryMessage, er
22882315 return a .HistoryForTab (tab .ID ), nil
22892316}
22902317
2318+ func (a * App ) OpenChannelSessionPageForTab (tabID , path string , limit int ) (HistoryPage , error ) {
2319+ tab := a .tabByID (tabID )
2320+ if tab == nil || tab .Ctrl == nil {
2321+ return HistoryPage {}, fmt .Errorf ("tab is not ready" )
2322+ }
2323+ ctrl := tab .Ctrl
2324+ sessionPath , _ , err := validateSessionPath (controllerSessionDir (ctrl ), path )
2325+ if err != nil {
2326+ return HistoryPage {}, err
2327+ }
2328+ loaded , err := loadResumableSession (sessionPath )
2329+ if err != nil {
2330+ return HistoryPage {}, err
2331+ }
2332+ if sessionRuntimeKey (tab .currentSessionPath ()) != sessionRuntimeKey (sessionPath ) {
2333+ if err := a .rebindTabToLoadedSessionPath (tab , sessionPath , loaded ); err != nil {
2334+ return HistoryPage {}, err
2335+ }
2336+ }
2337+ a .setTabReadOnly (tab .ID , true )
2338+ return a .HistoryPageForTab (tab .ID , 0 , limit ), nil
2339+ }
2340+
22912341func (a * App ) setTabReadOnly (tabID string , readOnly bool ) {
22922342 a .mu .Lock ()
22932343 if tab := a .tabs [tabID ]; tab != nil && tab .ReadOnly != readOnly {
@@ -3152,11 +3202,121 @@ type HistoryToolCall struct {
31523202 ArgumentsArchived bool `json:"argumentsArchived,omitempty"`
31533203}
31543204
3205+ const (
3206+ defaultHistoryPageTurns = 60
3207+ maxHistoryPageTurns = 200
3208+ )
3209+
3210+ type HistoryPage struct {
3211+ Messages []HistoryMessage `json:"messages"`
3212+ StartTurn int `json:"startTurn"`
3213+ EndTurn int `json:"endTurn"`
3214+ TotalTurns int `json:"totalTurns"`
3215+ HasOlder bool `json:"hasOlder"`
3216+ }
3217+
31553218// History returns the session's message log.
31563219func (a * App ) History () []HistoryMessage {
31573220 return a .HistoryForTab ("" )
31583221}
31593222
3223+ func (a * App ) HistoryPage (beforeTurn , limit int ) HistoryPage {
3224+ return a .HistoryPageForTab ("" , beforeTurn , limit )
3225+ }
3226+
3227+ func (a * App ) HistoryPageForTab (tabID string , beforeTurn , limit int ) HistoryPage {
3228+ a .mu .RLock ()
3229+ tab := a .tabByIDLocked (tabID )
3230+ var ctrl control.SessionAPI
3231+ var sessionDir , sessionPath string
3232+ if tab != nil {
3233+ ctrl = tab .Ctrl
3234+ sessionDir = tabSessionDir (tab )
3235+ sessionPath = tab .currentSessionPath ()
3236+ }
3237+ a .mu .RUnlock ()
3238+ if ctrl == nil {
3239+ if strings .TrimSpace (sessionPath ) == "" {
3240+ return HistoryPage {Messages : []HistoryMessage {}}
3241+ }
3242+ page , err := previewSessionPage (sessionDir , sessionPath , beforeTurn , limit )
3243+ if err != nil {
3244+ return HistoryPage {Messages : []HistoryMessage {}}
3245+ }
3246+ return page
3247+ }
3248+ msgs := ctrl .History ()
3249+ dir := controllerSessionDir (ctrl )
3250+ path := ctrl .SessionPath ()
3251+ return historyPageFromProviderMessages (
3252+ msgs ,
3253+ sessionDisplayResolver (dir , path ),
3254+ sessionPlannerDisplayTurns (dir , path ),
3255+ ctrl .CheckpointTurnsByMessageIndex (),
3256+ beforeTurn ,
3257+ limit ,
3258+ )
3259+ }
3260+
3261+ func normalizeHistoryPageLimit (limit int ) int {
3262+ if limit <= 0 {
3263+ return defaultHistoryPageTurns
3264+ }
3265+ if limit > maxHistoryPageTurns {
3266+ return maxHistoryPageTurns
3267+ }
3268+ return limit
3269+ }
3270+
3271+ func historyPageFromMessages (messages []HistoryMessage , beforeTurn , limit int ) HistoryPage {
3272+ limit = normalizeHistoryPageLimit (limit )
3273+ totalTurns := 0
3274+ for _ , msg := range messages {
3275+ if msg .Role == "user" {
3276+ totalTurns ++
3277+ }
3278+ }
3279+ if beforeTurn <= 0 || beforeTurn > totalTurns {
3280+ beforeTurn = totalTurns
3281+ }
3282+ startTurn := beforeTurn - limit
3283+ if startTurn < 0 {
3284+ startTurn = 0
3285+ }
3286+ page := HistoryPage {
3287+ StartTurn : startTurn ,
3288+ EndTurn : beforeTurn ,
3289+ TotalTurns : totalTurns ,
3290+ HasOlder : startTurn > 0 ,
3291+ }
3292+ if len (messages ) == 0 || startTurn >= beforeTurn {
3293+ page .Messages = []HistoryMessage {}
3294+ return page
3295+ }
3296+ page .Messages = historyMessagesForTurnRange (messages , startTurn , beforeTurn )
3297+ return page
3298+ }
3299+
3300+ func historyMessagesForTurnRange (messages []HistoryMessage , startTurn , endTurn int ) []HistoryMessage {
3301+ out := make ([]HistoryMessage , 0 , len (messages ))
3302+ turn := - 1
3303+ for _ , msg := range messages {
3304+ if msg .Role == "user" {
3305+ turn ++
3306+ }
3307+ if turn < 0 {
3308+ if startTurn == 0 {
3309+ out = append (out , msg )
3310+ }
3311+ continue
3312+ }
3313+ if turn >= startTurn && turn < endTurn {
3314+ out = append (out , msg )
3315+ }
3316+ }
3317+ return out
3318+ }
3319+
31603320func (a * App ) HistoryForTab (tabID string ) []HistoryMessage {
31613321 a .mu .RLock ()
31623322 tab := a .tabByIDLocked (tabID )
@@ -3189,14 +3349,64 @@ func (a *App) HistoryForTab(tabID string) []HistoryMessage {
31893349 )
31903350}
31913351
3352+ func (a * App ) HistoryCheckpointTurnsForTab (tabID string ) []int {
3353+ a .mu .RLock ()
3354+ tab := a .tabByIDLocked (tabID )
3355+ var ctrl control.SessionAPI
3356+ if tab != nil {
3357+ ctrl = tab .Ctrl
3358+ }
3359+ a .mu .RUnlock ()
3360+ if ctrl == nil {
3361+ return []int {}
3362+ }
3363+ return historyCheckpointTurns (
3364+ ctrl .History (),
3365+ sessionDisplayResolver (controllerSessionDir (ctrl ), ctrl .SessionPath ()),
3366+ ctrl .CheckpointTurnsByMessageIndex (),
3367+ )
3368+ }
3369+
3370+ func historyCheckpointTurns (msgs []provider.Message , resolveUserContent func (string ) string , checkpointTurns map [int ]int ) []int {
3371+ out := make ([]int , 0 )
3372+ for index , msg := range msgs {
3373+ if msg .Role != provider .RoleUser {
3374+ continue
3375+ }
3376+ if _ , isSteer := agent .SteerText (msg .Content ); isSteer {
3377+ continue
3378+ }
3379+ if control .IsSyntheticUserMessage (resolveUserContent (msg .Content )) {
3380+ continue
3381+ }
3382+ turn , ok := checkpointTurns [index ]
3383+ if ! ok {
3384+ turn = - 1
3385+ }
3386+ out = append (out , turn )
3387+ }
3388+ return out
3389+ }
3390+
31923391func historyMessages (msgs []provider.Message , resolveUserContent func (string ) string ) []HistoryMessage {
31933392 return historyMessagesWithPlannerDisplays (msgs , resolveUserContent , nil , nil )
31943393}
31953394
31963395func historyMessagesWithPlannerDisplays (msgs []provider.Message , resolveUserContent func (string ) string , plannerTurns []plannerDisplayTurn , checkpointTurns map [int ]int ) []HistoryMessage {
3197- out := make ([]HistoryMessage , 0 , len (msgs ))
31983396 replayedTodoArgs := historyTodoArgsWithCompleteSteps (msgs )
31993397 toolResults := historyToolResultsByID (msgs )
3398+ return historyMessagesWithPlannerDisplaysAndLookups (msgs , resolveUserContent , plannerTurns , checkpointTurns , replayedTodoArgs , toolResults )
3399+ }
3400+
3401+ func historyMessagesWithPlannerDisplaysAndLookups (
3402+ msgs []provider.Message ,
3403+ resolveUserContent func (string ) string ,
3404+ plannerTurns []plannerDisplayTurn ,
3405+ checkpointTurns map [int ]int ,
3406+ replayedTodoArgs map [string ]string ,
3407+ toolResults map [string ]provider.Message ,
3408+ ) []HistoryMessage {
3409+ out := make ([]HistoryMessage , 0 , len (msgs ))
32003410 plannerByUserHash := plannerTurnsByUserHash (plannerTurns )
32013411 for index , m := range msgs {
32023412 content := m .Content
@@ -3260,6 +3470,100 @@ func historyMessagesWithPlannerDisplays(msgs []provider.Message, resolveUserCont
32603470 return out
32613471}
32623472
3473+ func historyPageFromProviderMessages (
3474+ msgs []provider.Message ,
3475+ resolveUserContent func (string ) string ,
3476+ plannerTurns []plannerDisplayTurn ,
3477+ checkpointTurns map [int ]int ,
3478+ beforeTurn , limit int ,
3479+ ) HistoryPage {
3480+ limit = normalizeHistoryPageLimit (limit )
3481+ totalTurns := visibleHistoryUserTurns (msgs , resolveUserContent )
3482+ if beforeTurn <= 0 || beforeTurn > totalTurns {
3483+ beforeTurn = totalTurns
3484+ }
3485+ startTurn := beforeTurn - limit
3486+ if startTurn < 0 {
3487+ startTurn = 0
3488+ }
3489+ page := HistoryPage {
3490+ StartTurn : startTurn ,
3491+ EndTurn : beforeTurn ,
3492+ TotalTurns : totalTurns ,
3493+ HasOlder : startTurn > 0 ,
3494+ }
3495+ if len (msgs ) == 0 || startTurn >= beforeTurn {
3496+ page .Messages = []HistoryMessage {}
3497+ return page
3498+ }
3499+ pageMessages , originalIndexes := providerMessagesForVisibleTurnRange (msgs , resolveUserContent , startTurn , beforeTurn )
3500+ page .Messages = historyMessagesWithPlannerDisplaysAndLookups (
3501+ pageMessages ,
3502+ resolveUserContent ,
3503+ plannerTurns ,
3504+ checkpointTurnsForProviderWindow (checkpointTurns , originalIndexes ),
3505+ historyTodoArgsWithCompleteSteps (msgs ),
3506+ historyToolResultsByID (msgs ),
3507+ )
3508+ return page
3509+ }
3510+
3511+ func visibleHistoryUserTurns (msgs []provider.Message , resolveUserContent func (string ) string ) int {
3512+ total := 0
3513+ for _ , msg := range msgs {
3514+ if isVisibleHistoryUser (msg , resolveUserContent ) {
3515+ total ++
3516+ }
3517+ }
3518+ return total
3519+ }
3520+
3521+ func isVisibleHistoryUser (msg provider.Message , resolveUserContent func (string ) string ) bool {
3522+ if msg .Role != provider .RoleUser {
3523+ return false
3524+ }
3525+ if _ , isSteer := agent .SteerText (msg .Content ); isSteer {
3526+ return false
3527+ }
3528+ return ! control .IsSyntheticUserMessage (resolveUserContent (msg .Content ))
3529+ }
3530+
3531+ func providerMessagesForVisibleTurnRange (msgs []provider.Message , resolveUserContent func (string ) string , startTurn , endTurn int ) ([]provider.Message , []int ) {
3532+ out := make ([]provider.Message , 0 , len (msgs ))
3533+ indexes := make ([]int , 0 , len (msgs ))
3534+ turn := - 1
3535+ for index , msg := range msgs {
3536+ if isVisibleHistoryUser (msg , resolveUserContent ) {
3537+ turn ++
3538+ }
3539+ if turn < 0 {
3540+ if startTurn == 0 {
3541+ out = append (out , msg )
3542+ indexes = append (indexes , index )
3543+ }
3544+ continue
3545+ }
3546+ if turn >= startTurn && turn < endTurn {
3547+ out = append (out , msg )
3548+ indexes = append (indexes , index )
3549+ }
3550+ }
3551+ return out , indexes
3552+ }
3553+
3554+ func checkpointTurnsForProviderWindow (checkpointTurns map [int ]int , originalIndexes []int ) map [int ]int {
3555+ if len (checkpointTurns ) == 0 || len (originalIndexes ) == 0 {
3556+ return nil
3557+ }
3558+ out := map [int ]int {}
3559+ for pageIndex , originalIndex := range originalIndexes {
3560+ if turn , ok := checkpointTurns [originalIndex ]; ok {
3561+ out [pageIndex ] = turn
3562+ }
3563+ }
3564+ return out
3565+ }
3566+
32633567func plannerTurnsByUserHash (turns []plannerDisplayTurn ) map [string ][]plannerDisplayTurn {
32643568 out := map [string ][]plannerDisplayTurn {}
32653569 for _ , turn := range turns {
@@ -3605,6 +3909,31 @@ func previewSessionMessages(sessionDir, path string) ([]HistoryMessage, error) {
36053909 ), nil
36063910}
36073911
3912+ func previewSessionPage (sessionDir , path string , beforeTurn , limit int ) (HistoryPage , error ) {
3913+ sessionPath , _ , err := validateSessionPath (sessionDir , path )
3914+ if err != nil {
3915+ return HistoryPage {}, err
3916+ }
3917+ if out , ok , err := previewEventSessionMessages (sessionPath ); ok || err != nil {
3918+ if err != nil {
3919+ return HistoryPage {}, err
3920+ }
3921+ return historyPageFromMessages (out , beforeTurn , limit ), nil
3922+ }
3923+ loaded , err := agent .LoadSession (sessionPath )
3924+ if err != nil {
3925+ return HistoryPage {}, err
3926+ }
3927+ return historyPageFromProviderMessages (
3928+ loaded .Snapshot (),
3929+ sessionDisplayResolver (sessionDir , sessionPath ),
3930+ sessionPlannerDisplayTurns (sessionDir , sessionPath ),
3931+ nil ,
3932+ beforeTurn ,
3933+ limit ,
3934+ ), nil
3935+ }
3936+
36083937type previewEventRecord struct {
36093938 Kind string `json:"kind"`
36103939 Type string `json:"type"`
0 commit comments