From 57b05b7beff1e143f8c0d8e7283cfde9bdb04d4b Mon Sep 17 00:00:00 2001 From: xgopilot Date: Mon, 20 Apr 2026 08:09:46 +0000 Subject: [PATCH] fix(tui): debounce log viewer persistence and restore utf8 comment Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: creatang <165447160+creatang@users.noreply.github.com> --- internal/tui/core/app/app.go | 3 +++ internal/tui/core/app/update.go | 37 ++++++++++++++++++++++++++-- internal/tui/core/app/update_test.go | 5 ++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/internal/tui/core/app/app.go b/internal/tui/core/app/app.go index 14443a0f..35ec9fc0 100644 --- a/internal/tui/core/app/app.go +++ b/internal/tui/core/app/app.go @@ -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 @@ -132,6 +133,8 @@ type appRuntimeState struct { logViewerVisible bool logViewerOffset int logEntries []logEntry + logPersistDirty bool + logPersistVersion int transcriptContent string transcriptScrollbarDrag bool footerErrorLast string diff --git a/internal/tui/core/app/update.go b/internal/tui/core/app/update.go index 8e35902a..70d21ab7 100644 --- a/internal/tui/core/app/update.go +++ b/internal/tui/core/app/update.go @@ -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} @@ -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: @@ -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 @@ -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 @@ -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) { @@ -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 @@ -2617,11 +2647,13 @@ 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 { @@ -2629,6 +2661,7 @@ func (a *App) persistLogEntriesForActiveSession() { } payload, _ := json.Marshal(clampLogEntries(a.logEntries)) _ = os.WriteFile(logPath, payload, 0o600) + a.logPersistDirty = false } func (a App) sessionLogEntriesPath(sessionID string) string { @@ -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: diff --git a/internal/tui/core/app/update_test.go b/internal/tui/core/app/update_test.go index 9cc2d068..a9f4a2a1 100644 --- a/internal/tui/core/app/update_test.go +++ b/internal/tui/core/app/update_test.go @@ -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) }