Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 330 additions & 1 deletion desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2236,6 +2236,33 @@ func (a *App) ResumeSession(path string) ([]HistoryMessage, error) {
return a.ResumeSessionForTab("", path)
}

func (a *App) ResumeSessionPage(path string, limit int) (HistoryPage, error) {
return a.ResumeSessionPageForTab("", path, limit)
}

func (a *App) ResumeSessionPageForTab(tabID, path string, limit int) (HistoryPage, error) {
tab := a.tabByID(tabID)
if tab == nil || tab.Ctrl == nil {
return HistoryPage{}, fmt.Errorf("tab is not ready")
}
ctrl := tab.Ctrl
sessionPath, _, err := validateSessionPath(controllerSessionDir(ctrl), path)
if err != nil {
return HistoryPage{}, err
}
loaded, err := loadResumableSession(sessionPath)
if err != nil {
return HistoryPage{}, err
}
if sessionRuntimeKey(tab.currentSessionPath()) != sessionRuntimeKey(sessionPath) {
if err := a.rebindTabToLoadedSessionPath(tab, sessionPath, loaded); err != nil {
return HistoryPage{}, err
}
}
a.setTabReadOnly(tab.ID, false)
return a.HistoryPageForTab(tab.ID, 0, limit), nil
}

// ResumeSessionForTab is the tab-scoped form of ResumeSession. A saved session
// path is a runtime identity, so changing to a different path must replace the
// tab's controller binding rather than mutating the current controller in place.
Expand Down Expand Up @@ -2288,6 +2315,29 @@ func (a *App) OpenChannelSessionForTab(tabID, path string) ([]HistoryMessage, er
return a.HistoryForTab(tab.ID), nil
}

func (a *App) OpenChannelSessionPageForTab(tabID, path string, limit int) (HistoryPage, error) {
tab := a.tabByID(tabID)
if tab == nil || tab.Ctrl == nil {
return HistoryPage{}, fmt.Errorf("tab is not ready")
}
ctrl := tab.Ctrl
sessionPath, _, err := validateSessionPath(controllerSessionDir(ctrl), path)
if err != nil {
return HistoryPage{}, err
}
loaded, err := loadResumableSession(sessionPath)
if err != nil {
return HistoryPage{}, err
}
if sessionRuntimeKey(tab.currentSessionPath()) != sessionRuntimeKey(sessionPath) {
if err := a.rebindTabToLoadedSessionPath(tab, sessionPath, loaded); err != nil {
return HistoryPage{}, err
}
}
a.setTabReadOnly(tab.ID, true)
return a.HistoryPageForTab(tab.ID, 0, limit), nil
}

func (a *App) setTabReadOnly(tabID string, readOnly bool) {
a.mu.Lock()
if tab := a.tabs[tabID]; tab != nil && tab.ReadOnly != readOnly {
Expand Down Expand Up @@ -3152,11 +3202,121 @@ type HistoryToolCall struct {
ArgumentsArchived bool `json:"argumentsArchived,omitempty"`
}

const (
defaultHistoryPageTurns = 60
maxHistoryPageTurns = 200
)

type HistoryPage struct {
Messages []HistoryMessage `json:"messages"`
StartTurn int `json:"startTurn"`
EndTurn int `json:"endTurn"`
TotalTurns int `json:"totalTurns"`
HasOlder bool `json:"hasOlder"`
}

// History returns the session's message log.
func (a *App) History() []HistoryMessage {
return a.HistoryForTab("")
}

func (a *App) HistoryPage(beforeTurn, limit int) HistoryPage {
return a.HistoryPageForTab("", beforeTurn, limit)
}

func (a *App) HistoryPageForTab(tabID string, beforeTurn, limit int) HistoryPage {
a.mu.RLock()
tab := a.tabByIDLocked(tabID)
var ctrl control.SessionAPI
var sessionDir, sessionPath string
if tab != nil {
ctrl = tab.Ctrl
sessionDir = tabSessionDir(tab)
sessionPath = tab.currentSessionPath()
}
a.mu.RUnlock()
if ctrl == nil {
if strings.TrimSpace(sessionPath) == "" {
return HistoryPage{Messages: []HistoryMessage{}}
}
page, err := previewSessionPage(sessionDir, sessionPath, beforeTurn, limit)
if err != nil {
return HistoryPage{Messages: []HistoryMessage{}}
}
return page
}
msgs := ctrl.History()
dir := controllerSessionDir(ctrl)
path := ctrl.SessionPath()
return historyPageFromProviderMessages(
msgs,
sessionDisplayResolver(dir, path),
sessionPlannerDisplayTurns(dir, path),
ctrl.CheckpointTurnsByMessageIndex(),
beforeTurn,
limit,
)
}

func normalizeHistoryPageLimit(limit int) int {
if limit <= 0 {
return defaultHistoryPageTurns
}
if limit > maxHistoryPageTurns {
return maxHistoryPageTurns
}
return limit
}

func historyPageFromMessages(messages []HistoryMessage, beforeTurn, limit int) HistoryPage {
limit = normalizeHistoryPageLimit(limit)
totalTurns := 0
for _, msg := range messages {
if msg.Role == "user" {
totalTurns++
}
}
if beforeTurn <= 0 || beforeTurn > totalTurns {
beforeTurn = totalTurns
}
startTurn := beforeTurn - limit
if startTurn < 0 {
startTurn = 0
}
page := HistoryPage{
StartTurn: startTurn,
EndTurn: beforeTurn,
TotalTurns: totalTurns,
HasOlder: startTurn > 0,
}
if len(messages) == 0 || startTurn >= beforeTurn {
page.Messages = []HistoryMessage{}
return page
}
page.Messages = historyMessagesForTurnRange(messages, startTurn, beforeTurn)
return page
}

func historyMessagesForTurnRange(messages []HistoryMessage, startTurn, endTurn int) []HistoryMessage {
out := make([]HistoryMessage, 0, len(messages))
turn := -1
for _, msg := range messages {
if msg.Role == "user" {
turn++
}
if turn < 0 {
if startTurn == 0 {
out = append(out, msg)
}
continue
}
if turn >= startTurn && turn < endTurn {
out = append(out, msg)
}
}
return out
}

func (a *App) HistoryForTab(tabID string) []HistoryMessage {
a.mu.RLock()
tab := a.tabByIDLocked(tabID)
Expand Down Expand Up @@ -3189,14 +3349,64 @@ func (a *App) HistoryForTab(tabID string) []HistoryMessage {
)
}

func (a *App) HistoryCheckpointTurnsForTab(tabID string) []int {
a.mu.RLock()
tab := a.tabByIDLocked(tabID)
var ctrl control.SessionAPI
if tab != nil {
ctrl = tab.Ctrl
}
a.mu.RUnlock()
if ctrl == nil {
return []int{}
}
return historyCheckpointTurns(
ctrl.History(),
sessionDisplayResolver(controllerSessionDir(ctrl), ctrl.SessionPath()),
ctrl.CheckpointTurnsByMessageIndex(),
)
}

func historyCheckpointTurns(msgs []provider.Message, resolveUserContent func(string) string, checkpointTurns map[int]int) []int {
out := make([]int, 0)
for index, msg := range msgs {
if msg.Role != provider.RoleUser {
continue
}
if _, isSteer := agent.SteerText(msg.Content); isSteer {
continue
}
if control.IsSyntheticUserMessage(resolveUserContent(msg.Content)) {
continue
}
turn, ok := checkpointTurns[index]
if !ok {
turn = -1
}
out = append(out, turn)
}
return out
}

func historyMessages(msgs []provider.Message, resolveUserContent func(string) string) []HistoryMessage {
return historyMessagesWithPlannerDisplays(msgs, resolveUserContent, nil, nil)
}

func historyMessagesWithPlannerDisplays(msgs []provider.Message, resolveUserContent func(string) string, plannerTurns []plannerDisplayTurn, checkpointTurns map[int]int) []HistoryMessage {
out := make([]HistoryMessage, 0, len(msgs))
replayedTodoArgs := historyTodoArgsWithCompleteSteps(msgs)
toolResults := historyToolResultsByID(msgs)
return historyMessagesWithPlannerDisplaysAndLookups(msgs, resolveUserContent, plannerTurns, checkpointTurns, replayedTodoArgs, toolResults)
}

func historyMessagesWithPlannerDisplaysAndLookups(
msgs []provider.Message,
resolveUserContent func(string) string,
plannerTurns []plannerDisplayTurn,
checkpointTurns map[int]int,
replayedTodoArgs map[string]string,
toolResults map[string]provider.Message,
) []HistoryMessage {
out := make([]HistoryMessage, 0, len(msgs))
plannerByUserHash := plannerTurnsByUserHash(plannerTurns)
for index, m := range msgs {
content := m.Content
Expand Down Expand Up @@ -3260,6 +3470,100 @@ func historyMessagesWithPlannerDisplays(msgs []provider.Message, resolveUserCont
return out
}

func historyPageFromProviderMessages(
msgs []provider.Message,
resolveUserContent func(string) string,
plannerTurns []plannerDisplayTurn,
checkpointTurns map[int]int,
beforeTurn, limit int,
) HistoryPage {
limit = normalizeHistoryPageLimit(limit)
totalTurns := visibleHistoryUserTurns(msgs, resolveUserContent)
if beforeTurn <= 0 || beforeTurn > totalTurns {
beforeTurn = totalTurns
}
startTurn := beforeTurn - limit
if startTurn < 0 {
startTurn = 0
}
page := HistoryPage{
StartTurn: startTurn,
EndTurn: beforeTurn,
TotalTurns: totalTurns,
HasOlder: startTurn > 0,
}
if len(msgs) == 0 || startTurn >= beforeTurn {
page.Messages = []HistoryMessage{}
return page
}
pageMessages, originalIndexes := providerMessagesForVisibleTurnRange(msgs, resolveUserContent, startTurn, beforeTurn)
page.Messages = historyMessagesWithPlannerDisplaysAndLookups(
pageMessages,
resolveUserContent,
plannerTurns,
checkpointTurnsForProviderWindow(checkpointTurns, originalIndexes),
historyTodoArgsWithCompleteSteps(msgs),
historyToolResultsByID(msgs),
)
return page
}

func visibleHistoryUserTurns(msgs []provider.Message, resolveUserContent func(string) string) int {
total := 0
for _, msg := range msgs {
if isVisibleHistoryUser(msg, resolveUserContent) {
total++
}
}
return total
}

func isVisibleHistoryUser(msg provider.Message, resolveUserContent func(string) string) bool {
if msg.Role != provider.RoleUser {
return false
}
if _, isSteer := agent.SteerText(msg.Content); isSteer {
return false
}
return !control.IsSyntheticUserMessage(resolveUserContent(msg.Content))
}

func providerMessagesForVisibleTurnRange(msgs []provider.Message, resolveUserContent func(string) string, startTurn, endTurn int) ([]provider.Message, []int) {
out := make([]provider.Message, 0, len(msgs))
indexes := make([]int, 0, len(msgs))
turn := -1
for index, msg := range msgs {
if isVisibleHistoryUser(msg, resolveUserContent) {
turn++
}
if turn < 0 {
if startTurn == 0 {
out = append(out, msg)
indexes = append(indexes, index)
}
continue
}
if turn >= startTurn && turn < endTurn {
out = append(out, msg)
indexes = append(indexes, index)
}
}
return out, indexes
}

func checkpointTurnsForProviderWindow(checkpointTurns map[int]int, originalIndexes []int) map[int]int {
if len(checkpointTurns) == 0 || len(originalIndexes) == 0 {
return nil
}
out := map[int]int{}
for pageIndex, originalIndex := range originalIndexes {
if turn, ok := checkpointTurns[originalIndex]; ok {
out[pageIndex] = turn
}
}
return out
}

func plannerTurnsByUserHash(turns []plannerDisplayTurn) map[string][]plannerDisplayTurn {
out := map[string][]plannerDisplayTurn{}
for _, turn := range turns {
Expand Down Expand Up @@ -3605,6 +3909,31 @@ func previewSessionMessages(sessionDir, path string) ([]HistoryMessage, error) {
), nil
}

func previewSessionPage(sessionDir, path string, beforeTurn, limit int) (HistoryPage, error) {
sessionPath, _, err := validateSessionPath(sessionDir, path)
if err != nil {
return HistoryPage{}, err
}
if out, ok, err := previewEventSessionMessages(sessionPath); ok || err != nil {
if err != nil {
return HistoryPage{}, err
}
return historyPageFromMessages(out, beforeTurn, limit), nil
}
loaded, err := agent.LoadSession(sessionPath)
if err != nil {
return HistoryPage{}, err
}
return historyPageFromProviderMessages(
loaded.Snapshot(),
sessionDisplayResolver(sessionDir, sessionPath),
sessionPlannerDisplayTurns(sessionDir, sessionPath),
nil,
beforeTurn,
limit,
), nil
}

type previewEventRecord struct {
Kind string `json:"kind"`
Type string `json:"type"`
Expand Down
Loading
Loading