项目文件夹

文件
2026-07-13 13:00:08 +08:00

139 行
4.2 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package cli
import (
"fmt"
"strconv"
"strings"
"reasonix/internal/agent"
"reasonix/internal/i18n"
)
const resumeListCap = 10
// recentSessions returns the newest saved sessions under dir, capped so the
// 1-based indices the list shows match what /resume <n> and its completion
// resolve. A missing dir or read error yields an empty list.
func recentSessions(dir string) []agent.SessionInfo {
if dir == "" {
return nil
}
sessions, err := agent.ListSessions(dir)
if err != nil {
return nil
}
if len(sessions) > resumeListCap {
sessions = sessions[:resumeListCap]
}
return sessions
}
// runResumeCommand handles "/resume": with no argument it lists the most recent
// saved sessions (newest first, active one marked); "/resume <n>" loads that
// session into the running controller in place — keeping the current model and
// replaying the transcript into scrollback.
func (m *chatTUI) runResumeCommand(input string) {
sessions := recentSessions(m.ctrl.SessionDir())
if len(sessions) == 0 {
m.notice(i18n.M.NoSessionToResume)
return
}
args := tokenizeArgs(input) // args[0] == "/resume"
if len(args) < 2 {
m.showSessions(sessions) // write list to scrollback (above input)
m.openResumePicker() // open interactive picker below
return
}
if m.ctrl.Running() {
m.notice(i18n.M.ResumeBusy)
return
}
idx, err := strconv.Atoi(strings.TrimSpace(args[1]))
if err != nil || idx < 1 || idx > len(sessions) {
m.notice(fmt.Sprintf(i18n.M.ResumeBadIndexFmt, len(sessions)))
return
}
target := sessions[idx-1]
if target.Path == m.ctrl.SessionPath() {
m.notice(i18n.M.ResumeAlreadyActive)
return
}
loaded, err := agent.LoadSession(target.Path)
if err != nil {
m.notice("resume: " + err.Error())
return
}
// Persist the conversation we're leaving so switching back later restores it.
// Snapshot before moving the lease: the outgoing session must be written
// while this process still owns it.
_ = m.ctrl.Snapshot()
m.followSessionLease()
if err := m.rebindSessionLease(target.Path); err != nil {
m.notice("resume: " + sessionLeaseHeldNotice(err))
return
}
m.ctrl.Resume(loaded, target.Path)
m.replayActiveBranch(i18n.M.ResumedTitle)
}
// showSessions renders the recent-session list with 1-based indices, timestamp,
// turn count and preview, marking the one currently active.
func (m *chatTUI) showSessions(sessions []agent.SessionInfo) {
active := m.ctrl.SessionPath()
var b strings.Builder
b.WriteString(dim(" · " + i18n.M.ResumeListHeader + "\n"))
for i, s := range sessions {
marker := " "
if s.Path == active {
marker = accent(" ")
}
fmt.Fprintf(&b, "%s%d %s %s\n", marker, i+1,
s.ModTime.Local().Format("01-02 15:04"), dim(sessionSummary(s)))
}
m.notice(strings.TrimRight(b.String(), "\n"))
}
// resumeArgItems completes the index argument of "/resume <n>": once past the
// command word it lists recent sessions, inserting the 1-based index and
// showing timestamp + turn count + preview as the hint. Indices match
// showSessions because both window through recentSessions.
func (m *chatTUI) resumeArgItems(val string) ([]compItem, int, bool) {
cmdEnd := strings.IndexAny(val, " \t")
if cmdEnd < 0 || val[:cmdEnd] != "/resume" {
return nil, 0, false
}
from := strings.LastIndexAny(val, " \t") + 1
if len(strings.Fields(val[:from])) != 1 || m.ctrl == nil {
return nil, from, true
}
cur := val[from:]
var out []compItem
for i, s := range recentSessions(m.ctrl.SessionDir()) {
idx := strconv.Itoa(i + 1)
if cur != "" && !strings.HasPrefix(idx, cur) {
continue
}
hint := fmt.Sprintf("%s · %s", s.ModTime.Local().Format("01-02 15:04"), sessionSummary(s))
out = append(out, compItem{label: idx, insert: idx, hint: hint})
}
return out, from, true
}
// sessionSummary is the "N turns · display title" line shared by the /resume
// list and its argument completion. Explicit session renames win, then topic
// titles, then the raw preview so the user can identify sessions at a glance.
func sessionSummary(s agent.SessionInfo) string {
preview := s.CustomTitle
if preview == "" {
preview = s.TopicTitle
}
if preview == "" {
preview = s.Preview
}
if preview == "" {
preview = "(no user message yet)"
}
return fmt.Sprintf("%d turns · %s", s.Turns, preview)
}