on main help sign in
Merge pull request #41 from charmbracelet/messages-adjustments

All message UI/UX adjustments

commit324e2077a1d48c6afae55970d299a3ede9115fb0 authorKujtim Hoxha <kujtimii.h@gmail.com> dateFri 20 Jun 2025, 15:50 (1 year ago) parentd4fdfbdb6c792b
22 files changed+342−184
cspell.json+1−1 go.mod+1−1 internal/app/app.go+1 internal/llm/agent/agent.go+8 internal/llm/prompt/coder.go+6−6 internal/tui/components/chat/chat.go+13−4 internal/tui/components/chat/messages/messages.go+64−44 internal/tui/components/chat/messages/renderer.go+83−31 internal/tui/components/chat/messages/tool.go+15−11 internal/tui/components/chat/sidebar/sidebar.go+3−3 internal/tui/components/core/helpers.go+2−1 internal/tui/components/core/list/keys.go+3−3 internal/tui/components/core/list/list.go+4−2 internal/tui/components/core/status/status.go+27−9 internal/tui/components/dialogs/commands/arguments.go−9 internal/tui/components/dialogs/models/models.go+4−4 internal/tui/keys.go+2−2 internal/tui/page/chat/chat.go+32−5 internal/tui/styles/crush.go+8−4 internal/tui/styles/theme.go+55−37 internal/tui/tui.go+1−7 todos.md+9
cspell.json+1−1
@@ -1 +1 @@
1−{"version":"0.2","language":"en","flagWords":[],"words":["crush","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph","filepicker","imageorient","rasterx","oksvg","termenv","trashhalo","lucasb","nfnt","srwiley","Lanczos","fsext"]}
1+{"flagWords":[],"words":["crush","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph","filepicker","imageorient","rasterx","oksvg","termenv","trashhalo","lucasb","nfnt","srwiley","Lanczos","fsext","GROQ","alecthomas","Preproc","Emph","charmtone","Charple","Guac","diffview","Strikethrough","Unticked","uniseg","rivo"],"version":"0.2","language":"en"}
go.mod+1−1
@@ -32,7 +32,6 @@ require (
3232 github.com/pressly/goose/v3 v3.24.2
3333 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
3434 github.com/sahilm/fuzzy v0.1.1
35− github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
3635 github.com/spf13/cobra v1.9.1
3736 github.com/spf13/viper v1.20.0
3837 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
@@ -105,6 +104,7 @@ require (
105104 github.com/rivo/uniseg v0.4.7
106105 github.com/rogpeppe/go-internal v1.14.1 // indirect
107106 github.com/sagikazarmark/locafero v0.7.0 // indirect
107+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
108108 github.com/sethvargo/go-retry v0.3.0 // indirect
109109 github.com/sourcegraph/conc v0.3.0 // indirect
110110 github.com/spf13/afero v1.12.0 // indirect
internal/app/app.go+1
@@ -163,4 +163,5 @@ func (app *App) Shutdown() {
163163 }
164164 cancel()
165165 }
166+ app.CoderAgent.CancelAll()
166167}
internal/llm/agent/agent.go+8
@@ -50,6 +50,7 @@ type Service interface {
5050 Model() models.Model
5151 Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
5252 Cancel(sessionID string)
53+ CancelAll()
5354 IsSessionBusy(sessionID string) bool
5455 IsBusy() bool
5556 Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error)
@@ -698,6 +699,13 @@ func (a *agent) Summarize(ctx context.Context, sessionID string) error {
698699 return nil
699700}
700701
702+func (a *agent) CancelAll() {
703+ a.activeRequests.Range(func(key, value any) bool {
704+ a.Cancel(key.(string)) // key is sessionID
705+ return true
706+ })
707+}
708+
701709func createAgentProvider(agentName config.AgentName) (provider.Provider, error) {
702710 cfg := config.Get()
703711 agentConfig, ok := cfg.Agents[agentName]
internal/llm/prompt/coder.go+6−6
@@ -30,10 +30,6 @@ You are operating as and within the Crush CLI, a terminal-based agentic coding a
3030You can:
3131- Receive user prompts, project context, and files.
3232- Stream responses and emit function calls (e.g., shell commands, code edits).
33−- Apply patches, run commands, and manage user approvals based on policy.
34−- Work inside a sandboxed, git-backed workspace with rollback support.
35−- Log telemetry so sessions can be replayed or inspected later.
36−- More details on your functionality are available at "crush --help"
3733
3834
3935You are an agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. If you are not sure about file content or codebase structure pertaining to the user's request, use your tools to read files and gather the relevant information: do NOT guess or make up an answer.
@@ -64,17 +60,19 @@ You MUST adhere to the following criteria when executing the task:
6460- If completing the user's task DOES NOT require writing or modifying files (e.g., the user asks a question about the code base):
6561 - Respond in a friendly tune as a remote teammate, who is knowledgeable, capable and eager to help with coding.
6662- When your task involves writing or modifying files:
67− - Do NOT tell the user to "save the file" or "copy the code into a file" if you already created or modified the file using "apply_patch". Instead, reference the file as already saved.
63+ - Do NOT tell the user to "save the file" or "copy the code into a file" if you already created or modified the file using "edit/write". Instead, reference the file as already saved.
6864 - Do NOT show the full contents of large files you have already written, unless the user explicitly asks for them.
6965- When doing things with paths, always use use the full path, if the working directory is /abc/xyz and you want to edit the file abc.go in the working dir refer to it as /abc/xyz/abc.go.
7066- If you send a path not including the working dir, the working dir will be prepended to it.
7167- Remember the user does not see the full output of tools
68+- NEVER use emojis in your responses
7269`
7370
7471const baseAnthropicCoderPrompt = `You are Crush, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
7572
7673IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure.
7774
75+
7876# Memory
7977If the current working directory contains a file called CRUSH.md, it will be automatically added to your context. This file serves multiple purposes:
80781. Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time
@@ -131,7 +129,7 @@ assistant: src/foo.c
131129
132130<example>
133131user: write tests for new feature
134−assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit/patch file tool to write new tests]
132+assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests]
135133</example>
136134
137135# Proactiveness
@@ -165,6 +163,8 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN
165163- If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in parallel.
166164- IMPORTANT: The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user.
167165
166+VERY IMPORTANT NEVER use emojis in your responses.
167+
168168You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.`
169169
170170func getEnvironmentInfo() string {
internal/tui/components/chat/chat.go+13−4
@@ -272,7 +272,7 @@ func (m *messageListCmp) findAssistantMessageAndToolCalls(items []util.Model, me
272272 assistantIndex = i
273273 }
274274 } else if tc, ok := item.(messages.ToolCallCmp); ok {
275− if tc.ParentMessageId() == messageID {
275+ if tc.ParentMessageID() == messageID {
276276 toolCalls[i] = tc
277277 }
278278 }
@@ -295,9 +295,17 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi
295295 assistantIndex,
296296 messages.NewMessageCmp(
297297 msg,
298− messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
299298 ),
300299 )
300+
301+ if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
302+ m.listCmp.AppendItem(
303+ messages.NewAssistantSection(
304+ msg,
305+ time.Unix(m.lastUserMessageTime, 0),
306+ ),
307+ )
308+ }
301309 } else if hasToolCallsOnly {
302310 m.listCmp.DeleteItem(assistantIndex)
303311 }
@@ -347,7 +355,6 @@ func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd
347355 cmd := m.listCmp.AppendItem(
348356 messages.NewMessageCmp(
349357 msg,
350− messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
351358 ),
352359 )
353360 cmds = append(cmds, cmd)
@@ -412,6 +419,9 @@ func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message,
412419 uiMessages = append(uiMessages, messages.NewMessageCmp(msg))
413420 case message.Assistant:
414421 uiMessages = append(uiMessages, m.convertAssistantMessage(msg, toolResultMap)...)
422+ if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
423+ uiMessages = append(uiMessages, messages.NewAssistantSection(msg, time.Unix(m.lastUserMessageTime, 0)))
424+ }
415425 }
416426 }
417427
@@ -428,7 +438,6 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult
428438 uiMessages,
429439 messages.NewMessageCmp(
430440 msg,
431− messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
432441 ),
433442 )
434443 }
internal/tui/components/chat/messages/messages.go+64−44
@@ -8,13 +8,14 @@ import (
88
99 "github.com/charmbracelet/bubbles/v2/spinner"
1010 tea "github.com/charmbracelet/bubbletea/v2"
11− "github.com/charmbracelet/crush/internal/llm/models"
1211 "github.com/charmbracelet/lipgloss/v2"
1312
13+ "github.com/charmbracelet/crush/internal/llm/models"
1414 "github.com/charmbracelet/crush/internal/message"
1515 "github.com/charmbracelet/crush/internal/tui/components/anim"
1616 "github.com/charmbracelet/crush/internal/tui/components/core"
1717 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
18+ "github.com/charmbracelet/crush/internal/tui/components/core/list"
1819 "github.com/charmbracelet/crush/internal/tui/styles"
1920 "github.com/charmbracelet/crush/internal/tui/util"
2021)
@@ -37,32 +38,17 @@ type messageCmp struct {
3738 focused bool // Focus state for border styling
3839
3940 // Core message data and state
40− message message.Message // The underlying message content
41− spinning bool // Whether to show loading animation
42− anim util.Model // Animation component for loading states
43− lastUserMessageTime time.Time // Used for calculating response duration
44−}
45−
46−// MessageOption provides functional options for configuring message components
47−type MessageOption func(*messageCmp)
48−
49−// WithLastUserMessageTime sets the timestamp of the last user message
50−// for calculating assistant response duration
51−func WithLastUserMessageTime(t time.Time) MessageOption {
52− return func(m *messageCmp) {
53− m.lastUserMessageTime = t
54− }
41+ message message.Message // The underlying message content
42+ spinning bool // Whether to show loading animation
43+ anim util.Model // Animation component for loading states
5544}
5645
5746// NewMessageCmp creates a new message component with the given message and options
58−func NewMessageCmp(msg message.Message, opts ...MessageOption) MessageCmp {
47+func NewMessageCmp(msg message.Message) MessageCmp {
5948 m := &messageCmp{
6049 message: msg,
6150 anim: anim.New(15, ""),
6251 }
63− for _, opt := range opts {
64− opt(m)
65− }
6652 return m
6753}
6854
@@ -145,32 +131,10 @@ func (msg *messageCmp) style() lipgloss.Style {
145131// renderAssistantMessage renders assistant messages with optional footer information.
146132// Shows model name, response time, and finish reason when the message is complete.
147133func (m *messageCmp) renderAssistantMessage() string {
148− t := styles.CurrentTheme()
149134 parts := []string{
150135 m.markdownContent(),
151136 }
152137
153− finished := m.message.IsFinished()
154− finishData := m.message.FinishPart()
155− // Only show the footer if the message is not a tool call
156− if finished && finishData.Reason != message.FinishReasonToolUse {
157− infoMsg := ""
158− switch finishData.Reason {
159− case message.FinishReasonEndTurn:
160− finishTime := time.Unix(finishData.Time, 0)
161− duration := finishTime.Sub(m.lastUserMessageTime)
162− infoMsg = duration.String()
163− case message.FinishReasonCanceled:
164− infoMsg = "canceled"
165− case message.FinishReasonError:
166− infoMsg = "error"
167− case message.FinishReasonPermissionDenied:
168− infoMsg = "permission denied"
169− }
170− assistant := t.S().Muted.Render(fmt.Sprintf("%s %s (%s)", styles.ModelIcon, models.SupportedModels[m.message.Model].Name, infoMsg))
171− parts = append(parts, core.Section(assistant, m.textWidth()))
172− }
173−
174138 joined := lipgloss.JoinVertical(lipgloss.Left, parts...)
175139 return m.style().Render(joined)
176140}
@@ -200,7 +164,7 @@ func (m *messageCmp) renderUserMessage() string {
200164 parts = append(parts, "", strings.Join(attachments, ""))
201165 }
202166 joined := lipgloss.JoinVertical(lipgloss.Left, parts...)
203− return m.style().MarginBottom(1).Render(joined)
167+ return m.style().Render(joined)
204168}
205169
206170// toMarkdown converts text content to rendered markdown using the configured renderer
@@ -225,7 +189,7 @@ func (m *messageCmp) markdownContent() string {
225189 } else if finished && content == "" && finishedData.Reason == message.FinishReasonEndTurn {
226190 // Sometimes the LLMs respond with no content when they think the previous tool result
227191 // provides the requested question
228− content = "*Finished without output*"
192+ content = ""
229193 } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
230194 content = "*Canceled*"
231195 }
@@ -287,3 +251,59 @@ func (m *messageCmp) SetSize(width int, height int) tea.Cmd {
287251func (m *messageCmp) Spinning() bool {
288252 return m.spinning
289253}
254+
255+type AssistantSection interface {
256+ util.Model
257+ layout.Sizeable
258+ list.SectionHeader
259+}
260+type assistantSectionModel struct {
261+ width int
262+ message message.Message
263+ lastUserMessageTime time.Time
264+}
265+
266+func NewAssistantSection(message message.Message, lastUserMessageTime time.Time) AssistantSection {
267+ return &assistantSectionModel{
268+ width: 0,
269+ message: message,
270+ lastUserMessageTime: lastUserMessageTime,
271+ }
272+}
273+
274+func (m *assistantSectionModel) Init() tea.Cmd {
275+ return nil
276+}
277+
278+func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
279+ return m, nil
280+}
281+
282+func (m *assistantSectionModel) View() tea.View {
283+ t := styles.CurrentTheme()
284+ finishData := m.message.FinishPart()
285+ finishTime := time.Unix(finishData.Time, 0)
286+ duration := finishTime.Sub(m.lastUserMessageTime)
287+ infoMsg := t.S().Subtle.Render(duration.String())
288+ icon := t.S().Subtle.Render(styles.ModelIcon)
289+ model := t.S().Muted.Render(models.SupportedModels[m.message.Model].Name)
290+ assistant := fmt.Sprintf("%s %s %s", icon, model, infoMsg)
291+ return tea.NewView(
292+ t.S().Base.PaddingLeft(2).Render(
293+ core.Section(assistant, m.width-2),
294+ ),
295+ )
296+}
297+
298+func (m *assistantSectionModel) GetSize() (int, int) {
299+ return m.width, 1
300+}
301+
302+func (m *assistantSectionModel) SetSize(width int, height int) tea.Cmd {
303+ m.width = width
304+ return nil
305+}
306+
307+func (m *assistantSectionModel) IsSectionHeader() bool {
308+ return true
309+}
internal/tui/components/chat/messages/renderer.go+83−31
@@ -111,8 +111,18 @@ func (br baseRenderer) unmarshalParams(input string, target any) error {
111111 return json.Unmarshal([]byte(input), target)
112112}
113113
114+// makeHeader builds the tool call header with status icon and parameters for a nested tool call.
115+func (br baseRenderer) makeNestedHeader(v *toolCallCmp, tool string, width int, params ...string) string {
116+ t := styles.CurrentTheme()
117+ tool = t.S().Base.Foreground(t.FgHalfMuted).Render(tool) + " "
118+ return tool + renderParamList(true, width-lipgloss.Width(tool), params...)
119+}
120+
114121// makeHeader builds "<Tool>: param (key=value)" and truncates as needed.
115122func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params ...string) string {
123+ if v.isNested {
124+ return br.makeNestedHeader(v, tool, width, params...)
125+ }
116126 t := styles.CurrentTheme()
117127 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
118128 if v.result.ToolCallID != "" {
@@ -125,8 +135,8 @@ func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params
125135 icon = t.S().Muted.Render(styles.ToolPending)
126136 }
127137 tool = t.S().Base.Foreground(t.Blue).Render(tool)
128− prefix := fmt.Sprintf("%s %s: ", icon, tool)
129− return prefix + renderParamList(width-lipgloss.Width(prefix), params...)
138+ prefix := fmt.Sprintf("%s %s ", icon, tool)
139+ return prefix + renderParamList(false, width-lipgloss.Width(prefix), params...)
130140}
131141
132142// renderError provides consistent error rendering
@@ -260,8 +270,10 @@ func (er editRenderer) Render(v *toolCallCmp) string {
260270 formatter := core.DiffFormatter().
261271 Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
262272 After(fsext.PrettyPath(params.FilePath), meta.NewContent).
263− Split().
264273 Width(v.textWidth() - 2) // -2 for padding
274+ if v.textWidth() > 120 {
275+ formatter = formatter.Split()
276+ }
265277 return formatter.String()
266278 })
267279}
@@ -475,25 +487,45 @@ type agentRenderer struct {
475487 baseRenderer
476488}
477489
490+func RoundedEnumerator(children tree.Children, index int) string {
491+ if children.Length()-1 == index {
492+ return " ╰──"
493+ }
494+ return " ├──"
495+}
496+
478497// Render displays agent task parameters and result content
479498func (tr agentRenderer) Render(v *toolCallCmp) string {
499+ t := styles.CurrentTheme()
480500 var params agent.AgentParams
481501 if err := tr.unmarshalParams(v.call.Input, &params); err != nil {
482502 return tr.renderError(v, "Invalid task parameters")
483503 }
484504 prompt := params.Prompt
485505 prompt = strings.ReplaceAll(prompt, "\n", " ")
486− args := newParamBuilder().addMain(prompt).build()
487506
488− header := tr.makeHeader(v, "Task", v.textWidth(), args...)
489− t := tree.Root(header)
507+ header := tr.makeHeader(v, "Agent", v.textWidth())
508+ taskTag := t.S().Base.Padding(0, 1).MarginLeft(1).Background(t.BlueLight).Foreground(t.White).Render("Task")
509+ remainingWidth := v.textWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2 // -2 for padding
510+ prompt = t.S().Muted.Width(remainingWidth).Render(prompt)
511+ header = lipgloss.JoinVertical(
512+ lipgloss.Left,
513+ header,
514+ "",
515+ lipgloss.JoinHorizontal(
516+ lipgloss.Left,
517+ taskTag,
518+ " ",
519+ prompt,
520+ ),
521+ )
522+ childTools := tree.Root(header)
490523
491524 for _, call := range v.nestedToolCalls {
492− t.Child(call.View())
525+ childTools.Child(call.View())
493526 }
494−
495527 parts := []string{
496− t.Enumerator(tree.RoundedEnumerator).String(),
528+ childTools.Enumerator(RoundedEnumerator).String(),
497529 }
498530 if v.result.ToolCallID == "" {
499531 v.spinning = true
@@ -516,7 +548,8 @@ func (tr agentRenderer) Render(v *toolCallCmp) string {
516548}
517549
518550// renderParamList renders params, params[0] (params[1]=params[2] ....)
519−func renderParamList(paramsWidth int, params ...string) string {
551+func renderParamList(nested bool, paramsWidth int, params ...string) string {
552+ t := styles.CurrentTheme()
520553 if len(params) == 0 {
521554 return ""
522555 }
@@ -526,7 +559,10 @@ func renderParamList(paramsWidth int, params ...string) string {
526559 }
527560
528561 if len(params) == 1 {
529− return mainParam
562+ if nested {
563+ return t.S().Muted.Render(mainParam)
564+ }
565+ return t.S().Subtle.Render(mainParam)
530566 }
531567 otherParams := params[1:]
532568 // create pairs of key/value
@@ -547,15 +583,21 @@ func renderParamList(paramsWidth int, params ...string) string {
547583 partsRendered := strings.Join(parts, ", ")
548584 remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
549585 if remainingWidth < 30 {
586+ if nested {
587+ return t.S().Muted.Render(mainParam)
588+ }
550589 // No space for the params, just show the main
551− return mainParam
590+ return t.S().Subtle.Render(mainParam)
552591 }
553592
554593 if len(parts) > 0 {
555594 mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
556595 }
557596
558− return ansi.Truncate(mainParam, paramsWidth, "...")
597+ if nested {
598+ return t.S().Muted.Render(ansi.Truncate(mainParam, paramsWidth, "..."))
599+ }
600+ return t.S().Subtle.Render(ansi.Truncate(mainParam, paramsWidth, "..."))
559601}
560602
561603// earlyState returns immediately‑rendered error/cancelled/ongoing states.
@@ -580,7 +622,7 @@ func earlyState(header string, v *toolCallCmp) (string, bool) {
580622func joinHeaderBody(header, body string) string {
581623 t := styles.CurrentTheme()
582624 body = t.S().Base.PaddingLeft(2).Render(body)
583− return lipgloss.JoinVertical(lipgloss.Left, header, body, "")
625+ return lipgloss.JoinVertical(lipgloss.Left, header, "", body, "")
584626}
585627
586628func renderPlainContent(v *toolCallCmp, content string) string {
@@ -600,46 +642,56 @@ func renderPlainContent(v *toolCallCmp, content string) string {
600642 }
601643 out = append(out, t.S().Muted.
602644 Width(width).
603− Background(t.BgSubtle).
645+ Background(t.BgBaseLighter).
604646 Render(ln))
605647 }
606648
607649 if len(lines) > responseContextHeight {
608650 out = append(out, t.S().Muted.
609− Background(t.BgSubtle).
651+ Background(t.BgBaseLighter).
610652 Width(width).
611653 Render(fmt.Sprintf("... (%d lines)", len(lines)-responseContextHeight)))
612654 }
613655 return strings.Join(out, "\n")
614656}
615657
658+func pad(v any, width int) string {
659+ s := fmt.Sprintf("%v", v)
660+ w := ansi.StringWidth(s)
661+ if w >= width {
662+ return s
663+ }
664+ return strings.Repeat(" ", width-w) + s
665+}
666+
616667func renderCodeContent(v *toolCallCmp, path, content string, offset int) string {
617668 t := styles.CurrentTheme()
618669 truncated := truncateHeight(content, responseContextHeight)
619670
620− highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgSubtle)
671+ highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgBase)
621672 lines := strings.Split(highlighted, "\n")
622673
623674 if len(strings.Split(content, "\n")) > responseContextHeight {
624675 lines = append(lines, t.S().Muted.
625− Background(t.BgSubtle).
626− Width(v.textWidth()-2).
627− Render(fmt.Sprintf("... (%d lines)", len(strings.Split(content, "\n"))-responseContextHeight)))
676+ Background(t.BgBase).
677+ Render(fmt.Sprintf(" …(%d lines)", len(strings.Split(content, "\n"))-responseContextHeight)))
628678 }
629679
680+ maxLineNumber := len(lines) + offset
681+ padding := lipgloss.Width(fmt.Sprintf("%d", maxLineNumber))
630682 for i, ln := range lines {
631− num := t.S().Muted.
632− Background(t.BgSubtle).
633− PaddingLeft(4).
634− PaddingRight(2).
635− Render(fmt.Sprintf("%d", i+1+offset))
636− w := v.textWidth() - 2 - lipgloss.Width(num) // -2 for left padding
683+ num := t.S().Base.
684+ Foreground(t.FgMuted).
685+ Background(t.BgBase).
686+ PaddingRight(1).
687+ PaddingLeft(1).
688+ Render(pad(i+1+offset, padding))
689+ w := v.textWidth() - 10 - lipgloss.Width(num) // -4 for left padding
637690 lines[i] = lipgloss.JoinHorizontal(lipgloss.Left,
638691 num,
639692 t.S().Base.
640− Width(w).
641− Background(t.BgSubtle).
642− Render(v.fit(ln, w)))
693+ PaddingLeft(1).
694+ Render(v.fit(ln, w-1)))
643695 }
644696 return lipgloss.JoinVertical(lipgloss.Left, lines...)
645697}
@@ -648,7 +700,7 @@ func (v *toolCallCmp) renderToolError() string {
648700 t := styles.CurrentTheme()
649701 err := strings.ReplaceAll(v.result.Content, "\n", " ")
650702 err = fmt.Sprintf("Error: %s", err)
651− return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth()))
703+ return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth()-2))
652704}
653705
654706func truncateHeight(s string, h int) string {
@@ -662,7 +714,7 @@ func truncateHeight(s string, h int) string {
662714func prettifyToolName(name string) string {
663715 switch name {
664716 case agent.AgentToolName:
665− return "Task"
717+ return "Agent"
666718 case tools.BashToolName:
667719 return "Bash"
668720 case tools.EditToolName:
internal/tui/components/chat/messages/tool.go+15−11
@@ -25,7 +25,7 @@ type ToolCallCmp interface {
2525 SetToolResult(message.ToolResult) // Update tool result
2626 SetToolCall(message.ToolCall) // Update tool call
2727 SetCancelled() // Mark as cancelled
28− ParentMessageId() string // Get parent message ID
28+ ParentMessageID() string // Get parent message ID
2929 Spinning() bool // Animation state for pending tools
3030 GetNestedToolCalls() []ToolCallCmp // Get nested tool calls
3131 SetNestedToolCalls([]ToolCallCmp) // Set nested tool calls
@@ -83,10 +83,10 @@ func WithToolCallNestedCalls(calls []ToolCallCmp) ToolCallOption {
8383
8484// NewToolCallCmp creates a new tool call component with the given parent message ID,
8585// tool call, and optional configuration
86−func NewToolCallCmp(parentMessageId string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp {
86+func NewToolCallCmp(parentMessageID string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp {
8787 m := &toolCallCmp{
8888 call: tc,
89− parentMessageID: parentMessageId,
89+ parentMessageID: parentMessageID,
9090 }
9191 for _, opt := range opts {
9292 opt(m)
@@ -137,9 +137,6 @@ func (m *toolCallCmp) View() tea.View {
137137 box := m.style()
138138
139139 if !m.call.Finished && !m.cancelled {
140− if m.isNested {
141− return tea.NewView(box.Render(m.renderPending()))
142− }
143140 return tea.NewView(box.Render(m.renderPending()))
144141 }
145142
@@ -166,8 +163,8 @@ func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
166163 }
167164}
168165
169−// ParentMessageId returns the ID of the message that initiated this tool call
170−func (m *toolCallCmp) ParentMessageId() string {
166+// ParentMessageID returns the ID of the message that initiated this tool call
167+func (m *toolCallCmp) ParentMessageID() string {
171168 return m.parentMessageID
172169}
173170
@@ -210,9 +207,13 @@ func (m *toolCallCmp) SetIsNested(isNested bool) {
210207// renderPending displays the tool name with a loading animation for pending tool calls
211208func (m *toolCallCmp) renderPending() string {
212209 t := styles.CurrentTheme()
210+ if m.isNested {
211+ tool := t.S().Base.Foreground(t.FgHalfMuted).Render(prettifyToolName(m.call.Name))
212+ return fmt.Sprintf("%s %s", tool, m.anim.View())
213+ }
213214 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
214215 tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name))
215− return fmt.Sprintf("%s %s: %s", icon, tool, m.anim.View())
216+ return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
216217}
217218
218219// style returns the lipgloss style for the tool call component.
@@ -229,14 +230,17 @@ func (m *toolCallCmp) style() lipgloss.Style {
229230// textWidth calculates the available width for text content,
230231// accounting for borders and padding
231232func (m *toolCallCmp) textWidth() int {
233+ if m.isNested {
234+ return m.width - 6
235+ }
232236 return m.width - 5 // take into account the border and PaddingLeft
233237}
234238
235239// fit truncates content to fit within the specified width with ellipsis
236240func (m *toolCallCmp) fit(content string, width int) string {
237241 t := styles.CurrentTheme()
238− lineStyle := t.S().Muted.Background(t.BgSubtle)
239− dots := lineStyle.Render("...")
242+ lineStyle := t.S().Muted
243+ dots := lineStyle.Render("…")
240244 return ansi.Truncate(content, width, dots)
241245}
242246
internal/tui/components/chat/sidebar/sidebar.go+3−3
@@ -288,9 +288,9 @@ func (m *sidebarCmp) filesBlock() string {
288288 })
289289
290290 for _, file := range files {
291− // Extract just the filename from the path
292−
293− // Create status indicators for additions/deletions
291+ if file.Additions == 0 && file.Deletions == 0 {
292+ continue // skip files with no changes
293+ }
294294 var statusParts []string
295295 if file.Additions > 0 {
296296 statusParts = append(statusParts, t.S().Base.Foreground(t.Success).Render(fmt.Sprintf("+%d", file.Additions)))
internal/tui/components/core/helpers.go+2−1
@@ -148,8 +148,9 @@ func SelectableButtons(buttons []ButtonOpts, spacing string) string {
148148}
149149
150150func DiffFormatter() *diffview.DiffView {
151+ t := styles.CurrentTheme()
151152 formatDiff := diffview.New()
152153 style := chroma.MustNewStyle("crush", styles.GetChromaTheme())
153− diff := formatDiff.ChromaStyle(style)
154+ diff := formatDiff.ChromaStyle(style).Style(t.S().Diff)
154155 return diff
155156}
internal/tui/components/core/list/keys.go+3−3
@@ -32,10 +32,10 @@ func DefaultKeyMap() KeyMap {
3232 key.WithKeys("k"),
3333 ),
3434 UpOneItem: key.NewBinding(
35− key.WithKeys("shift+up", "shift+k"),
35+ key.WithKeys("shift+up", "K"),
3636 ),
3737 DownOneItem: key.NewBinding(
38− key.WithKeys("shift+down", "shift+j"),
38+ key.WithKeys("shift+down", "J"),
3939 ),
4040 HalfPageDown: key.NewBinding(
4141 key.WithKeys("d"),
@@ -47,7 +47,7 @@ func DefaultKeyMap() KeyMap {
4747 key.WithKeys("g", "home"),
4848 ),
4949 End: key.NewBinding(
50− key.WithKeys("shift+g", "end"),
50+ key.WithKeys("G", "end"),
5151 ),
5252 }
5353}
internal/tui/components/core/list/list.go+4−2
@@ -749,8 +749,8 @@ func (m *model) ensureVisibleReverse(cachedItem renderedItem) {
749749func (m *model) goToBottom() tea.Cmd {
750750 cmds := []tea.Cmd{m.blurSelected()}
751751 m.viewState.reverse = true
752+ m.selectionState.selectedIndex = m.findLastSelectableItem()
752753 if m.isFocused {
753− m.selectionState.selectedIndex = m.findLastSelectableItem()
754754 cmds = append(cmds, m.focusSelected())
755755 }
756756 m.ResetView()
@@ -764,7 +764,9 @@ func (m *model) goToTop() tea.Cmd {
764764 cmds := []tea.Cmd{m.blurSelected()}
765765 m.viewState.reverse = false
766766 m.selectionState.selectedIndex = m.findFirstSelectableItem()
767− cmds = append(cmds, m.focusSelected())
767+ if m.isFocused {
768+ cmds = append(cmds, m.focusSelected())
769+ }
768770 m.ResetView()
769771 return tea.Batch(cmds...)
770772}
internal/tui/components/core/status/status.go+27−9
@@ -1,6 +1,7 @@
11package status
22
33import (
4+ "strings"
45 "time"
56
67 "github.com/charmbracelet/bubbles/v2/help"
@@ -10,6 +11,8 @@ import (
1011 "github.com/charmbracelet/crush/internal/session"
1112 "github.com/charmbracelet/crush/internal/tui/styles"
1213 "github.com/charmbracelet/crush/internal/tui/util"
14+ "github.com/charmbracelet/lipgloss/v2"
15+ "github.com/charmbracelet/x/ansi"
1316)
1417
1518type StatusCmp interface {
@@ -85,6 +88,7 @@ func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8588 TTL: msg.Payload.PersistTime,
8689 }
8790 }
91+ return m, m.clearMessageCmd(m.info.TTL)
8892 }
8993 }
9094 return m, nil
@@ -94,18 +98,32 @@ func (m *statusCmp) View() tea.View {
9498 t := styles.CurrentTheme()
9599 status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(m.keyMap))
96100 if m.info.Msg != "" {
97− switch m.info.Type {
98− case util.InfoTypeError:
99− status = t.S().Base.Background(t.Error).Padding(0, 1).Width(m.width).Render(m.info.Msg)
100− case util.InfoTypeWarn:
101− status = t.S().Base.Background(t.Warning).Padding(0, 1).Width(m.width).Render(m.info.Msg)
102− default:
103− status = t.S().Base.Background(t.Info).Padding(0, 1).Width(m.width).Render(m.info.Msg)
104− }
101+ status = m.infoMsg()
105102 }
106103 return tea.NewView(status)
107104}
108105
106+func (m *statusCmp) infoMsg() string {
107+ t := styles.CurrentTheme()
108+ message := ""
109+ infoType := ""
110+ switch m.info.Type {
111+ case util.InfoTypeError:
112+ infoType = t.S().Base.Background(t.Red).Padding(0, 1).Render("ERROR")
113+ width := m.width - lipgloss.Width(infoType)
114+ message = t.S().Base.Background(t.Error).Foreground(t.White).Padding(0, 1).Width(width).Render(ansi.Truncate(m.info.Msg, width, "…"))
115+ case util.InfoTypeWarn:
116+ infoType = t.S().Base.Foreground(t.BgOverlay).Background(t.Yellow).Padding(0, 1).Render("WARNING")
117+ width := m.width - lipgloss.Width(infoType)
118+ message = t.S().Base.Foreground(t.BgOverlay).Background(t.Warning).Padding(0, 1).Width(width).Render(ansi.Truncate(m.info.Msg, width, "…"))
119+ default:
120+ infoType = t.S().Base.Foreground(t.BgOverlay).Background(t.Green).Padding(0, 1).Render("OKAY!")
121+ width := m.width - lipgloss.Width(infoType)
122+ message = t.S().Base.Background(t.Success).Foreground(t.White).Padding(0, 1).Width(width).Render(ansi.Truncate(m.info.Msg, width, "…"))
123+ }
124+ return strings.Join([]string{infoType, message}, "")
125+}
126+
109127func (m *statusCmp) ToggleFullHelp() {
110128 m.help.ShowAll = !m.help.ShowAll
111129}
@@ -119,7 +137,7 @@ func NewStatusCmp(keyMap help.KeyMap) StatusCmp {
119137 help := help.New()
120138 help.Styles = t.S().Help
121139 return &statusCmp{
122− messageTTL: 10 * time.Second,
140+ messageTTL: 5 * time.Second,
123141 help: help,
124142 keyMap: keyMap,
125143 }
internal/tui/components/dialogs/commands/arguments.go−9
@@ -211,15 +211,6 @@ func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
211211 return cursor
212212}
213213
214−func (c *commandArgumentsDialogCmp) style() lipgloss.Style {
215− t := styles.CurrentTheme()
216− return t.S().Base.
217− Width(c.width).
218− Padding(1).
219− Border(lipgloss.RoundedBorder()).
220− BorderForeground(t.BorderFocus)
221−}
222−
223214func (c *commandArgumentsDialogCmp) Position() (int, int) {
224215 row := c.wHeight / 2
225216 row -= c.wHeight / 2
internal/tui/components/dialogs/models/models.go+4−4
@@ -193,15 +193,15 @@ func (m *modelDialogCmp) listHeight() int {
193193
194194func GetSelectedModel(cfg *config.Config) models.Model {
195195 agentCfg := cfg.Agents[config.AgentCoder]
196− selectedModelId := agentCfg.Model
197− return models.SupportedModels[selectedModelId]
196+ selectedModelID := agentCfg.Model
197+ return models.SupportedModels[selectedModelID]
198198}
199199
200200func getEnabledProviders(cfg *config.Config) []models.ModelProvider {
201201 var providers []models.ModelProvider
202− for providerId, provider := range cfg.Providers {
202+ for providerID, provider := range cfg.Providers {
203203 if !provider.Disabled {
204− providers = append(providers, providerId)
204+ providers = append(providers, providerID)
205205 }
206206 }
207207
internal/tui/keys.go+2−2
@@ -61,8 +61,8 @@ func (k KeyMap) FullHelp() [][]key.Binding {
6161 }
6262 }
6363
64− for i := 0; i < len(cleaned); i += 2 {
65− end := min(i+2, len(cleaned))
64+ for i := 0; i < len(cleaned); i += 3 {
65+ end := min(i+3, len(cleaned))
6666 m = append(m, cleaned[i:end])
6767 }
6868 return m
internal/tui/page/chat/chat.go+32−5
@@ -3,6 +3,7 @@ package chat
33import (
44 "context"
55 "strings"
6+ "time"
67
78 "github.com/charmbracelet/bubbles/v2/key"
89 tea "github.com/charmbracelet/bubbletea/v2"
@@ -33,6 +34,7 @@ type (
3334 ChatFocusedMsg struct {
3435 Focused bool // True if the chat input is focused, false otherwise
3536 }
37+ CancelTimerExpiredMsg struct{}
3638)
3739
3840type ChatPage interface {
@@ -57,6 +59,8 @@ type chatPage struct {
5759 showDetails bool // Show details in the header
5860 header header.Header
5961 compactSidebar layout.Container
62+
63+ cancelPending bool // True if ESC was pressed once and waiting for second press
6064}
6165
6266func (p *chatPage) Init() tea.Cmd {
@@ -67,9 +71,19 @@ func (p *chatPage) Init() tea.Cmd {
6771 )
6872}
6973
74+// cancelTimerCmd creates a command that expires the cancel timer after 2 seconds
75+func (p *chatPage) cancelTimerCmd() tea.Cmd {
76+ return tea.Tick(2*time.Second, func(time.Time) tea.Msg {
77+ return CancelTimerExpiredMsg{}
78+ })
79+}
80+
7081func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7182 var cmds []tea.Cmd
7283 switch msg := msg.(type) {
84+ case CancelTimerExpiredMsg:
85+ p.cancelPending = false
86+ return p, nil
7387 case tea.WindowSizeMsg:
7488 h, cmd := p.header.Update(msg)
7589 cmds = append(cmds, cmd)
@@ -181,10 +195,16 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
181195 return p, tea.Batch(cmds...)
182196 case key.Matches(msg, p.keyMap.Cancel):
183197 if p.session.ID != "" {
184− // Cancel the current session's generation process
185− // This allows users to interrupt long-running operations
186− p.app.CoderAgent.Cancel(p.session.ID)
187− return p, nil
198+ if p.cancelPending {
199+ // Second ESC press - actually cancel the session
200+ p.cancelPending = false
201+ p.app.CoderAgent.Cancel(p.session.ID)
202+ return p, nil
203+ } else {
204+ // First ESC press - start the timer
205+ p.cancelPending = true
206+ return p, p.cancelTimerCmd()
207+ }
188208 }
189209 case key.Matches(msg, p.keyMap.Details):
190210 if p.session.ID == "" || !p.compactMode {
@@ -336,7 +356,14 @@ func (p *chatPage) Bindings() []key.Binding {
336356 p.keyMap.AddAttachment,
337357 }
338358 if p.app.CoderAgent.IsBusy() {
339− bindings = append([]key.Binding{p.keyMap.Cancel}, bindings...)
359+ cancelBinding := p.keyMap.Cancel
360+ if p.cancelPending {
361+ cancelBinding = key.NewBinding(
362+ key.WithKeys("esc"),
363+ key.WithHelp("esc", "press again to cancel"),
364+ )
365+ }
366+ bindings = append([]key.Binding{cancelBinding}, bindings...)
340367 }
341368
342369 if p.chatFocused {
internal/tui/styles/crush.go+8−4
@@ -14,9 +14,10 @@ func NewCrushTheme() *Theme {
1414 Tertiary: charmtone.Bok,
1515 Accent: charmtone.Zest,
1616 // Backgrounds
17− BgBase: charmtone.Pepper,
18− BgSubtle: charmtone.Charcoal,
19− BgOverlay: charmtone.Iron,
17+ BgBase: charmtone.Pepper,
18+ BgBaseLighter: Lighten(charmtone.Pepper, 2),
19+ BgSubtle: charmtone.Charcoal,
20+ BgOverlay: charmtone.Iron,
2021
2122 // Foregrounds
2223 FgBase: charmtone.Ash,
@@ -38,7 +39,10 @@ func NewCrushTheme() *Theme {
3839 // Colors
3940 White: charmtone.Butter,
4041
41− Blue: charmtone.Malibu,
42+ BlueLight: charmtone.Sardine,
43+ Blue: charmtone.Malibu,
44+
45+ Yellow: charmtone.Mustard,
4246
4347 Green: charmtone.Julep,
4448 GreenDark: charmtone.Guac,
internal/tui/styles/theme.go+55−37
@@ -10,6 +10,7 @@ import (
1010 "github.com/charmbracelet/bubbles/v2/textarea"
1111 "github.com/charmbracelet/bubbles/v2/textinput"
1212 tea "github.com/charmbracelet/bubbletea/v2"
13+ "github.com/charmbracelet/crush/internal/exp/diffview"
1314 "github.com/charmbracelet/glamour/v2/ansi"
1415 "github.com/charmbracelet/lipgloss/v2"
1516 "github.com/lucasb-eyer/go-colorful"
@@ -31,9 +32,10 @@ type Theme struct {
3132 Tertiary color.Color
3233 Accent color.Color
3334
34− BgBase color.Color
35− BgSubtle color.Color
36− BgOverlay color.Color
35+ BgBase color.Color
36+ BgBaseLighter color.Color
37+ BgSubtle color.Color
38+ BgOverlay color.Color
3739
3840 FgBase color.Color
3941 FgMuted color.Color
@@ -52,8 +54,13 @@ type Theme struct {
5254 // Colors
5355 // White
5456 White color.Color
57+
5558 // Blues
56− Blue color.Color
59+ BlueLight color.Color
60+ Blue color.Color
61+
62+ // Yellows
63+ Yellow color.Color
5764
5865 // Greens
5966 Green color.Color
@@ -65,26 +72,9 @@ type Theme struct {
6572 RedDark color.Color
6673 RedLight color.Color
6774
68− // TODO: add any others needed
69−
7075 styles *Styles
7176}
7277
73−type Diff struct {
74− Added color.Color
75− Removed color.Color
76− Context color.Color
77− HunkHeader color.Color
78− HighlightAdded color.Color
79− HighlightRemoved color.Color
80− AddedBg color.Color
81− RemovedBg color.Color
82− ContextBg color.Color
83− LineNumber color.Color
84− AddedLineNumberBg color.Color
85− RemovedLineNumberBg color.Color
86−}
87−
8878type Styles struct {
8979 Base lipgloss.Style
9080 SelectedBase lipgloss.Style
@@ -112,7 +102,7 @@ type Styles struct {
112102 Help help.Styles
113103
114104 // Diff
115− Diff Diff
105+ Diff diffview.Style
116106
117107 // FilePicker
118108 FilePicker filepicker.Styles
@@ -421,22 +411,50 @@ func (t *Theme) buildStyles() *Styles {
421411 FullSeparator: base.Foreground(t.Border),
422412 },
423413
424− // TODO: Fix this this is bad
425− Diff: Diff{
426− Added: t.Green,
427− Removed: t.Red,
428− Context: t.FgSubtle,
429− HunkHeader: t.FgSubtle,
430− HighlightAdded: t.GreenLight,
431− HighlightRemoved: t.RedLight,
432− AddedBg: t.GreenDark,
433− RemovedBg: t.RedDark,
434− ContextBg: t.BgSubtle,
435− LineNumber: t.FgMuted,
436− AddedLineNumberBg: t.GreenDark,
437− RemovedLineNumberBg: t.RedDark,
414+ Diff: diffview.Style{
415+ DividerLine: diffview.LineStyle{
416+ LineNumber: lipgloss.NewStyle().
417+ Foreground(t.FgHalfMuted).
418+ Background(t.BgBaseLighter),
419+ Code: lipgloss.NewStyle().
420+ Foreground(t.FgHalfMuted).
421+ Background(t.BgBaseLighter),
422+ },
423+ MissingLine: diffview.LineStyle{
424+ LineNumber: lipgloss.NewStyle().
425+ Background(t.BgBaseLighter),
426+ Code: lipgloss.NewStyle().
427+ Background(t.BgBaseLighter),
428+ },
429+ EqualLine: diffview.LineStyle{
430+ LineNumber: lipgloss.NewStyle().
431+ Foreground(t.FgMuted).
432+ Background(t.BgBase),
433+ Code: lipgloss.NewStyle().
434+ Foreground(t.FgMuted).
435+ Background(t.BgBase),
436+ },
437+ InsertLine: diffview.LineStyle{
438+ LineNumber: lipgloss.NewStyle().
439+ Foreground(lipgloss.Color("#629657")).
440+ Background(lipgloss.Color("#2b322a")),
441+ Symbol: lipgloss.NewStyle().
442+ Foreground(lipgloss.Color("#629657")).
443+ Background(lipgloss.Color("#323931")),
444+ Code: lipgloss.NewStyle().
445+ Background(lipgloss.Color("#323931")),
446+ },
447+ DeleteLine: diffview.LineStyle{
448+ LineNumber: lipgloss.NewStyle().
449+ Foreground(lipgloss.Color("#a45c59")).
450+ Background(lipgloss.Color("#312929")),
451+ Symbol: lipgloss.NewStyle().
452+ Foreground(lipgloss.Color("#a45c59")).
453+ Background(lipgloss.Color("#383030")),
454+ Code: lipgloss.NewStyle().
455+ Background(lipgloss.Color("#383030")),
456+ },
438457 },
439−
440458 FilePicker: filepicker.Styles{
441459 DisabledCursor: base.Foreground(t.FgMuted),
442460 Cursor: base.Foreground(t.FgBase),
internal/tui/tui.go+1−7
@@ -94,12 +94,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9494
9595 switch msg := msg.(type) {
9696 case tea.KeyboardEnhancementsMsg:
97− logging.Info(
98− "Keyboard enhancements detected",
99− "Disambiguation", msg.SupportsKeyDisambiguation(),
100− "ReleaseKeys", msg.SupportsKeyReleases(),
101− "UniformKeys", msg.SupportsUniformKeyLayout(),
102− )
10397 return a, nil
10498 case tea.WindowSizeMsg:
10599 return a, a.handleWindowResize(msg.Width, msg.Height)
@@ -260,7 +254,7 @@ func (a *appModel) handleWindowResize(width, height int) tea.Cmd {
260254 var cmds []tea.Cmd
261255 a.wWidth, a.wHeight = width, height
262256 if a.showingFullHelp {
263− height -= 3
257+ height -= 4
264258 } else {
265259 height -= 2
266260 }
todos.md+9
@@ -20,7 +20,16 @@
2020- [ ] Parallel tool calls and permissions
2121 - [ ] Run the tools in parallel and add results in parallel
2222 - [ ] Show multiple permissions dialogs
23+- [ ] Add another space around buttons
24+- [ ] Completions
25+ - [ ] Should change the help to show the completions stuff
26+ - [ ] Should make it wider
27+ - [ ] Tab and ctrl+y should accept
28+ - [ ] Words should line up
29+ - [ ] If there are no completions and cick tab/ctrl+y/enter it should close it
2330- [ ] Investigate messages issues
31+ - [ ] Make the agent separator look like the
32+ - [ ] Cleanup tool calls (watch all states)
2433 - [ ] Weird behavior sometimes the message does not update
2534 - [ ] Message length (I saw the message go beyond the correct length when there are errors)
2635 - [ ] Address UX issues