on main help sign in
Enhance UI feedback and improve file diff visualization
c185dc8Kujtim Hoxha
internal/llm/tools/diagnostics.go+3−2
@@ -72,6 +72,7 @@ func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*ls
72
72
73 // Create a notification handler that will signal when diagnostics are received
73 // Create a notification handler that will signal when diagnostics are received
74 handler := func(params json.RawMessage) {
74 handler := func(params json.RawMessage) {
75+ lsp.HandleDiagnostics(client, params)
75 var diagParams protocol.PublishDiagnosticsParams
76 var diagParams protocol.PublishDiagnosticsParams
76 if err := json.Unmarshal(params, &diagParams); err != nil {
77 if err := json.Unmarshal(params, &diagParams); err != nil {
77 return
78 return
@@ -103,8 +104,8 @@ func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*ls
103 select {
104 select {
104 case <-diagChan:
105 case <-diagChan:
105 // Diagnostics received
106 // Diagnostics received
106− case <-time.After(5 * time.Second):
107+ case <-time.After(10 * time.Second):
107− // Timeout after 2 seconds - this is a fallback in case no diagnostics are published
108+ // Timeout after 5 seconds - this is a fallback in case no diagnostics are published
108 case <-ctx.Done():
109 case <-ctx.Done():
109 // Context cancelled
110 // Context cancelled
110 }
111 }
internal/llm/tools/edit.go+28−5
@@ -303,23 +303,46 @@ func GenerateDiff(oldContent, newContent string) string {
303 diffs = dmp.DiffCharsToLines(diffs, dmpStrings)
303 diffs = dmp.DiffCharsToLines(diffs, dmpStrings)
304 diffs = dmp.DiffCleanupSemantic(diffs)
304 diffs = dmp.DiffCleanupSemantic(diffs)
305 buff := strings.Builder{}
305 buff := strings.Builder{}
306+
307+ // Add a header to make the diff more readable
308+ buff.WriteString("Changes:\n")
309+
306 for _, diff := range diffs {
310 for _, diff := range diffs {
307 text := diff.Text
311 text := diff.Text
308
312
309 switch diff.Type {
313 switch diff.Type {
310 case diffmatchpatch.DiffInsert:
314 case diffmatchpatch.DiffInsert:
311− for line := range strings.SplitSeq(text, "\n") {
315+ for _, line := range strings.Split(text, "\n") {
316+ if line == "" {
317+ continue
318+ }
312 _, _ = buff.WriteString("+ " + line + "\n")
319 _, _ = buff.WriteString("+ " + line + "\n")
313 }
320 }
314 case diffmatchpatch.DiffDelete:
321 case diffmatchpatch.DiffDelete:
315− for line := range strings.SplitSeq(text, "\n") {
322+ for _, line := range strings.Split(text, "\n") {
323+ if line == "" {
324+ continue
325+ }
316 _, _ = buff.WriteString("- " + line + "\n")
326 _, _ = buff.WriteString("- " + line + "\n")
317 }
327 }
318 case diffmatchpatch.DiffEqual:
328 case diffmatchpatch.DiffEqual:
329+ // Only show a small context for unchanged text
330+ lines := strings.Split(text, "\n")
319− if len(text) > 40 {
331+ if len(lines) > 3 {
332+ // Show only first and last line of context with a separator
333+ if lines[0] != "" {
320− _, _ = buff.WriteString(" " + text[:20] + "..." + text[len(text)-20:] + "\n")
334+ _, _ = buff.WriteString(" " + lines[0] + "\n")
335+ }
336+ _, _ = buff.WriteString(" ...\n")
337+ if lines[len(lines)-1] != "" {
338+ _, _ = buff.WriteString(" " + lines[len(lines)-1] + "\n")
339+ }
321 } else {
340 } else {
341+ // Show all lines for small contexts
322− for line := range strings.SplitSeq(text, "\n") {
342+ for _, line := range lines {
343+ if line == "" {
344+ continue
345+ }
323 _, _ = buff.WriteString(" " + line + "\n")
346 _, _ = buff.WriteString(" " + line + "\n")
324 }
347 }
325 }
348 }
internal/llm/tools/write.go+10−1
@@ -101,6 +101,15 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
101 }
101 }
102
102
103 notifyLspOpenFile(ctx, filePath, w.lspClients)
103 notifyLspOpenFile(ctx, filePath, w.lspClients)
104+ // Get old content for diff if file exists
105+ oldContent := ""
106+ if fileInfo != nil && !fileInfo.IsDir() {
107+ oldBytes, readErr := os.ReadFile(filePath)
108+ if readErr == nil {
109+ oldContent = string(oldBytes)
110+ }
111+ }
112+
104 p := permission.Default.Request(
113 p := permission.Default.Request(
105 permission.CreatePermissionRequest{
114 permission.CreatePermissionRequest{
106 Path: filePath,
115 Path: filePath,
@@ -109,7 +118,7 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
109 Description: fmt.Sprintf("Create file %s", filePath),
118 Description: fmt.Sprintf("Create file %s", filePath),
110 Params: WritePermissionsParams{
119 Params: WritePermissionsParams{
111 FilePath: filePath,
120 FilePath: filePath,
112− Content: GenerateDiff("", params.Content),
121+ Content: GenerateDiff(oldContent, params.Content),
113 },
122 },
114 },
123 },
115 )
124 )
internal/tui/components/dialog/permission.go+44−14
@@ -79,10 +79,18 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
79 p.isViewportFocus = !p.isViewportFocus
79 p.isViewportFocus = !p.isViewportFocus
80 if p.isViewportFocus {
80 if p.isViewportFocus {
81 p.selectOption.Blur()
81 p.selectOption.Blur()
82+ // Add a visual indicator for focus change
83+ cmds = append(cmds, tea.Batch(
84+ util.CmdHandler(util.InfoMsg("Viewing content - use arrow keys to scroll")),
85+ ))
82 } else {
86 } else {
83 p.selectOption.Focus()
87 p.selectOption.Focus()
88+ // Add a visual indicator for focus change
89+ cmds = append(cmds, tea.Batch(
90+ util.CmdHandler(util.InfoMsg("Select an action")),
91+ ))
84 }
92 }
85− return p, nil
93+ return p, tea.Batch(cmds...)
86 }
94 }
87 }
95 }
88
96
@@ -133,34 +141,55 @@ func (p *permissionDialogCmp) render() string {
133 case tools.BashToolName:
141 case tools.BashToolName:
134 pr := p.permission.Params.(tools.BashPermissionsParams)
142 pr := p.permission.Params.(tools.BashPermissionsParams)
135 headerParts = append(headerParts, keyStyle.Render("Command:"))
143 headerParts = append(headerParts, keyStyle.Render("Command:"))
136− content, _ = r.Render(fmt.Sprintf("```bash\n%s\n```", pr.Command))
144+ content = fmt.Sprintf("```bash\n%s\n```", pr.Command)
137 case tools.EditToolName:
145 case tools.EditToolName:
138 pr := p.permission.Params.(tools.EditPermissionsParams)
146 pr := p.permission.Params.(tools.EditPermissionsParams)
139 headerParts = append(headerParts, keyStyle.Render("Update"))
147 headerParts = append(headerParts, keyStyle.Render("Update"))
140− content, _ = r.Render(fmt.Sprintf("```diff\n%s\n```", pr.Diff))
148+ content = fmt.Sprintf("```\n%s\n```", pr.Diff)
141 case tools.WriteToolName:
149 case tools.WriteToolName:
142 pr := p.permission.Params.(tools.WritePermissionsParams)
150 pr := p.permission.Params.(tools.WritePermissionsParams)
143 headerParts = append(headerParts, keyStyle.Render("Content"))
151 headerParts = append(headerParts, keyStyle.Render("Content"))
144− content, _ = r.Render(fmt.Sprintf("```diff\n%s\n```", pr.Content))
152+ content = fmt.Sprintf("```\n%s\n```", pr.Content)
145 case tools.FetchToolName:
153 case tools.FetchToolName:
146 pr := p.permission.Params.(tools.FetchPermissionsParams)
154 pr := p.permission.Params.(tools.FetchPermissionsParams)
147 headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
155 headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
148 default:
156 default:
149− content, _ = r.Render(p.permission.Description)
157+ content = p.permission.Description
150 }
158 }
159+
160+ renderedContent, _ := r.Render(content)
151 headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
161 headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
152 p.contentViewPort.Width = p.width - 2 - 2
162 p.contentViewPort.Width = p.width - 2 - 2
153 p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
163 p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
154− p.contentViewPort.SetContent(content)
164+ p.contentViewPort.SetContent(renderedContent)
165+
166+ // Make focus change more apparent with different border styles and colors
155− contentBorder := lipgloss.RoundedBorder()
167+ var contentBorder lipgloss.Border
168+ var borderColor lipgloss.TerminalColor
169+
156 if p.isViewportFocus {
170 if p.isViewportFocus {
157 contentBorder = lipgloss.DoubleBorder()
171 contentBorder = lipgloss.DoubleBorder()
172+ borderColor = styles.Blue
173+ } else {
174+ contentBorder = lipgloss.RoundedBorder()
175+ borderColor = styles.Flamingo
176+ }
177+
178+ contentStyle := lipgloss.NewStyle().
179+ MarginTop(1).
180+ Padding(0, 1).
181+ Border(contentBorder).
182+ BorderForeground(borderColor)
183+
184+ if p.isViewportFocus {
185+ contentStyle = contentStyle.BorderBackground(styles.Surface0)
158 }
186 }
159− cotentStyle := lipgloss.NewStyle().MarginTop(1).Padding(0, 1).Border(contentBorder).BorderForeground(styles.Flamingo)
187+
160− contentFinal := cotentStyle.Render(p.contentViewPort.View())
188+ contentFinal := contentStyle.Render(p.contentViewPort.View())
161− if content == "" {
189+ if renderedContent == "" {
162 contentFinal = ""
190 contentFinal = ""
163 }
191 }
192+
164 return lipgloss.JoinVertical(
193 return lipgloss.JoinVertical(
165 lipgloss.Top,
194 lipgloss.Top,
166 headerContent,
195 headerContent,
@@ -241,12 +270,13 @@ func NewPermissionDialogCmd(permission permission.PermissionRequest) tea.Cmd {
241 minWidth := 100
270 minWidth := 100
242 minHeight := 30
271 minHeight := 30
243
272
273+ // Make the dialog size more appropriate for bash commands
244 switch permission.ToolName {
274 switch permission.ToolName {
245 case tools.BashToolName:
275 case tools.BashToolName:
246− widthRatio = 0.5
276+ widthRatio = 0.7
247− heightRatio = 0.3
277+ heightRatio = 0.5
248− minWidth = 80
278+ minWidth = 100
249− minHeight = 20
279+ minHeight = 30
250 }
280 }
251 // Return the dialog command
281 // Return the dialog command
252 return util.CmdHandler(core.DialogMsg{
282 return util.CmdHandler(core.DialogMsg{
internal/tui/tui.go+2
@@ -166,6 +166,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
166 a.dialog = d.(core.DialogCmp)
166 a.dialog = d.(core.DialogCmp)
167 return a, cmd
167 return a, cmd
168 }
168 }
169+ s, _ := a.status.Update(msg)
170+ a.status = s
169 p, cmd := a.pages[a.currentPage].Update(msg)
171 p, cmd := a.pages[a.currentPage].Update(msg)
170 a.pages[a.currentPage] = p
172 a.pages[a.currentPage] = p
171 return a, cmd
173 return a, cmd