Skip to content

Commit 5f79395

Browse files
committed
fix(desktop): page history and preserve cancel context
1 parent 4bea752 commit 5f79395

20 files changed

Lines changed: 919 additions & 90 deletions

desktop/app.go

Lines changed: 330 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
22912341
func (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.
31563219
func (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+
31603320
func (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+
31923391
func historyMessages(msgs []provider.Message, resolveUserContent func(string) string) []HistoryMessage {
31933392
return historyMessagesWithPlannerDisplays(msgs, resolveUserContent, nil, nil)
31943393
}
31953394

31963395
func 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+
32633567
func 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+
36083937
type previewEventRecord struct {
36093938
Kind string `json:"kind"`
36103939
Type string `json:"type"`

0 commit comments

Comments
 (0)