diff --git a/pkg/leantui/leantui.go b/pkg/leantui/leantui.go index 207604569..ab2b16a91 100644 --- a/pkg/leantui/leantui.go +++ b/pkg/leantui/leantui.go @@ -67,6 +67,7 @@ func Run(ctx context.Context, cfg Config) error { } m.status.Branch = branchWatcher.Current() m.commitWelcome() + m.loadInitialSessionTranscript() m.refreshCommands(loopCtx) keys := make(chan ui.Key, 64) diff --git a/pkg/leantui/ui/render.go b/pkg/leantui/ui/render.go index bf5d03149..c4d179213 100644 --- a/pkg/leantui/ui/render.go +++ b/pkg/leantui/ui/render.go @@ -9,6 +9,11 @@ import ( "github.com/docker/docker-agent/pkg/tui/styles" ) +const ( + seqPromptStart = "\x1b]133;A;redraw=0\x07" + seqOutputStart = "\x1b]133;C\x07" +) + // RenderUserLines renders a submitted user message as committed scrollback, // echoing it with the same prompt marker used by the input box. func RenderUserLines(text string, width int) []string { @@ -19,7 +24,11 @@ func RenderUserLines(text string, width int) []string { innerWidth := max(width-boxStyle.GetHorizontalFrameSize(), 1) textStyle := lipgloss.NewStyle().Foreground(styles.AgentBadgeFg) content := strings.Join(RenderUserLinesWith(text, innerWidth, textStyle.Bold(true), textStyle), "\n") - return splitRenderedLines(styles.RenderComposite(boxStyle, content), width) + lines := splitRenderedLines(styles.RenderComposite(boxStyle, content), width) + if len(lines) > 0 { + lines[0] = seqPromptStart + lines[0] + seqOutputStart + } + return lines } func RenderPendingUserLines(msg PendingUserMessage, width int) []string { diff --git a/pkg/leantui/ui/render_test.go b/pkg/leantui/ui/render_test.go index 663001fe5..2fa9a5b14 100644 --- a/pkg/leantui/ui/render_test.go +++ b/pkg/leantui/ui/render_test.go @@ -1,6 +1,7 @@ package ui import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -9,6 +10,18 @@ import ( "github.com/docker/docker-agent/pkg/tui/styles" ) +func TestRenderUserLinesMarksPromptStart(t *testing.T) { + t.Parallel() + + lines := RenderUserLines("jump back here", 24) + require.NotEmpty(t, lines) + assert.True(t, strings.HasPrefix(lines[0], seqPromptStart)) + assert.True(t, strings.HasSuffix(lines[0], seqOutputStart)) + assert.Equal(t, 1, strings.Count(strings.Join(lines, ""), seqPromptStart)) + assert.Equal(t, 1, strings.Count(strings.Join(lines, ""), seqOutputStart)) + assert.Equal(t, 24, DisplayWidth(lines[0])) +} + func TestRenderUserLinesUsesDistinctFullWidthBackground(t *testing.T) { t.Parallel() diff --git a/pkg/leantui/update.go b/pkg/leantui/update.go index 23ee9940b..eb4169f2c 100644 --- a/pkg/leantui/update.go +++ b/pkg/leantui/update.go @@ -434,6 +434,13 @@ func (m *model) resumeSession(ctx context.Context, sessionID string) { m.addNotice("", "Resumed session: "+title, ui.StMuted()) } +func (m *model) loadInitialSessionTranscript() { + if m.app == nil || m.app.Session() == nil || len(m.app.Session().OwnMessages()) == 0 { + return + } + m.loadSessionTranscript(m.app.Session()) +} + func (m *model) loadSessionTranscript(sess *session.Session) { storedMessages := sess.OwnMessages() toolResults := make(map[string]chat.Message) diff --git a/pkg/leantui/update_test.go b/pkg/leantui/update_test.go index 61d56a248..480f7821c 100644 --- a/pkg/leantui/update_test.go +++ b/pkg/leantui/update_test.go @@ -234,6 +234,27 @@ func TestSessionsCommandListsCurrentDirectoryAndResumesSelection(t *testing.T) { assert.NotContains(t, transcript, "Other directory") } +func TestLoadInitialSessionTranscriptMarksRestoredUserMessages(t *testing.T) { + t.Parallel() + + sess := session.New() + sess.AddMessage(session.UserMessage("restored question")) + sess.AddMessage(session.NewAgentMessage("coder", &chat.Message{ + Role: chat.MessageRoleAssistant, + Content: "restored answer", + })) + + m := bareModel(80) + m.app = app.New(t.Context(), &cycleThinkingRuntime{}, sess) + m.sessionState = service.NewSessionState(sess) + m.loadInitialSessionTranscript() + + transcript := strings.Join(m.screen.Transcript.Lines(80, 0, false, m.sessionState, nil), "\n") + assert.Contains(t, transcript, "\x1b]133;A;redraw=0\x07") + assert.Contains(t, transcript, "restored question") + assert.Contains(t, transcript, "restored answer") +} + func TestLoadSessionTranscriptRestoresToolCalls(t *testing.T) { t.Parallel() sess := session.New()