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
3 changes: 3 additions & 0 deletions internal/tui/core/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ type appRuntimeState struct {
codeCopyBlocks map[int]string
pendingCopyID int
deferredEventCmd tea.Cmd
deferredLogPersistCmd tea.Cmd
nowFn func() time.Time
lastInputEditAt time.Time
lastPasteLikeAt time.Time
Expand Down Expand Up @@ -132,6 +133,8 @@ type appRuntimeState struct {
logViewerVisible bool
logViewerOffset int
logEntries []logEntry
logPersistDirty bool
logPersistVersion int
transcriptContent string
transcriptScrollbarDrag bool
footerErrorLast string
Expand Down
37 changes: 35 additions & 2 deletions internal/tui/core/app/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const providerAddManualModelsJSONTemplate = "[\n {\n \"id\": \"model-id\",\n
const sessionSwitchBusyMessage = "cannot switch sessions while run or compact is active"
const logViewerEntryLimit = 500
const logViewerPersistDir = "log-viewer"
const logViewerPersistDebounce = 300 * time.Millisecond
const footerErrorFlashDuration = 4 * time.Second

var panelOrder = []panel{panelTranscript, panelInput}
Expand All @@ -67,6 +68,10 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if a.isBusy() {
cmds = append(cmds, spinCmd)
}
if a.deferredLogPersistCmd != nil {
cmds = append(cmds, a.deferredLogPersistCmd)
a.deferredLogPersistCmd = nil
}

switch typed := msg.(type) {
case tea.WindowSizeMsg:
Expand All @@ -90,6 +95,12 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
cmds = append(cmds, ListenForRuntimeEvent(a.runtime.Events()))
return a, tea.Batch(cmds...)
case logPersistFlushMsg:
if typed.Version != a.logPersistVersion || !a.logPersistDirty {
return a, tea.Batch(cmds...)
}
a.persistLogEntriesForActiveSession()
return a, tea.Batch(cmds...)
case RuntimeClosedMsg:
a.state.IsAgentRunning = false
a.state.StreamingReply = false
Expand Down Expand Up @@ -615,6 +626,17 @@ func (a App) now() time.Time {
return a.nowFn()
}

type logPersistFlushMsg struct {
Version int
}

// scheduleLogPersistFlush 在短暂静默后触发日志落盘,避免每条活动都同步刷盘。
func scheduleLogPersistFlush(version int) tea.Cmd {
return tea.Tick(logViewerPersistDebounce, func(time.Time) tea.Msg {
return logPersistFlushMsg{Version: version}
})
}

func (a *App) shouldTreatEnterAsNewline(typed tea.KeyMsg, now time.Time) bool {
if !key.Matches(typed, a.keys.Send) || a.state.IsAgentRunning {
return false
Expand Down Expand Up @@ -1666,7 +1688,12 @@ func (a *App) addLogEntry(kind string, title string, detail string) {
if a.logViewerOffset > maxOffset {
a.logViewerOffset = maxOffset
}
a.persistLogEntriesForActiveSession()
if strings.TrimSpace(a.state.ActiveSessionID) == "" {
return
}
a.logPersistDirty = true
a.logPersistVersion++
a.deferredLogPersistCmd = scheduleLogPersistFlush(a.logPersistVersion)
}

func (a *App) syncActivityViewport(previousCount int) {
Expand Down Expand Up @@ -2565,6 +2592,9 @@ func (a *App) setActiveSessionID(sessionID string) {
a.state.ActiveSessionID = next
return
}
if current != "" && a.logPersistDirty {
a.persistLogEntriesForActiveSession()
}

previousEntries := a.logEntries
a.state.ActiveSessionID = next
Expand Down Expand Up @@ -2617,18 +2647,21 @@ func (a *App) readLogEntriesForSession(sessionID string) []logEntry {
func (a *App) persistLogEntriesForActiveSession() {
sessionID := strings.TrimSpace(a.state.ActiveSessionID)
if sessionID == "" {
a.logPersistDirty = false
return
}

logPath := a.sessionLogEntriesPath(sessionID)
if logPath == "" {
a.logPersistDirty = false
return
}
if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil {
return
}
payload, _ := json.Marshal(clampLogEntries(a.logEntries))
_ = os.WriteFile(logPath, payload, 0o600)
a.logPersistDirty = false
}

func (a App) sessionLogEntriesPath(sessionID string) string {
Expand Down Expand Up @@ -2837,7 +2870,7 @@ func currentProviderAddField(form *providerAddFormState) providerAddFieldID {
return fields[form.Step]
}

// isProviderAddEnumField 鍒ゆ柇褰撳墠鏂板 Provider 琛ㄥ崟鐒︾偣鏄惁鍦ㄦ灇涓惧瓧娈碉紙Driver/Model Source锛夈€
// isProviderAddEnumField 判断当前新增 Provider 表单焦点是否在枚举字段(Driver/Model Source)。
func isProviderAddEnumField(form *providerAddFormState) bool {
switch currentProviderAddField(form) {
case providerAddFieldDriver, providerAddFieldModelSource:
Expand Down
5 changes: 5 additions & 0 deletions internal/tui/core/app/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3531,6 +3531,11 @@ func TestSessionLogViewerPersistenceAndCap(t *testing.T) {
if !strings.Contains(app.logEntries[0].Message, "entry-020") {
t.Fatalf("expected oldest in-memory entry to be entry-020, got %q", app.logEntries[0].Message)
}
if app.deferredLogPersistCmd == nil {
t.Fatalf("expected deferred log persistence command to be queued")
}
model, _ := app.Update(logPersistFlushMsg{Version: app.logPersistVersion})
app = model.(App)
if _, err := os.Stat(app.sessionLogEntriesPath("session-one")); err != nil {
t.Fatalf("expected persisted log file for session-one, got err=%v", err)
}
Expand Down
Loading