Enhance UI feedback and improve file diff visualization

- Improve diff display in permission dialogs with better formatting
- Add visual indicators for focus changes in permission dialogs
- Increase diagnostics timeout from 5 to 10 seconds
- Fix write tool to show proper diffs for existing files
- Update status component to properly handle messages

🤖 Generated with termai
Co-Authored-By: termai <noreply@termai.io>
This commit is contained in:
Kujtim Hoxha 2025-04-04 14:36:57 +02:00
parent 6bb1c84f7f
commit c185dc84d6
5 changed files with 87 additions and 22 deletions

View file

@ -72,6 +72,7 @@ func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*ls
// Create a notification handler that will signal when diagnostics are received // Create a notification handler that will signal when diagnostics are received
handler := func(params json.RawMessage) { handler := func(params json.RawMessage) {
lsp.HandleDiagnostics(client, params)
var diagParams protocol.PublishDiagnosticsParams var diagParams protocol.PublishDiagnosticsParams
if err := json.Unmarshal(params, &diagParams); err != nil { if err := json.Unmarshal(params, &diagParams); err != nil {
return return
@ -103,8 +104,8 @@ func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*ls
select { select {
case <-diagChan: case <-diagChan:
// Diagnostics received // Diagnostics received
case <-time.After(5 * time.Second): case <-time.After(10 * time.Second):
// Timeout after 2 seconds - this is a fallback in case no diagnostics are published // Timeout after 5 seconds - this is a fallback in case no diagnostics are published
case <-ctx.Done(): case <-ctx.Done():
// Context cancelled // Context cancelled
} }

View file

@ -303,23 +303,46 @@ func GenerateDiff(oldContent, newContent string) string {
diffs = dmp.DiffCharsToLines(diffs, dmpStrings) diffs = dmp.DiffCharsToLines(diffs, dmpStrings)
diffs = dmp.DiffCleanupSemantic(diffs) diffs = dmp.DiffCleanupSemantic(diffs)
buff := strings.Builder{} buff := strings.Builder{}
// Add a header to make the diff more readable
buff.WriteString("Changes:\n")
for _, diff := range diffs { for _, diff := range diffs {
text := diff.Text text := diff.Text
switch diff.Type { switch diff.Type {
case diffmatchpatch.DiffInsert: case diffmatchpatch.DiffInsert:
for line := range strings.SplitSeq(text, "\n") { for _, line := range strings.Split(text, "\n") {
if line == "" {
continue
}
_, _ = buff.WriteString("+ " + line + "\n") _, _ = buff.WriteString("+ " + line + "\n")
} }
case diffmatchpatch.DiffDelete: case diffmatchpatch.DiffDelete:
for line := range strings.SplitSeq(text, "\n") { for _, line := range strings.Split(text, "\n") {
if line == "" {
continue
}
_, _ = buff.WriteString("- " + line + "\n") _, _ = buff.WriteString("- " + line + "\n")
} }
case diffmatchpatch.DiffEqual: case diffmatchpatch.DiffEqual:
if len(text) > 40 { // Only show a small context for unchanged text
_, _ = buff.WriteString(" " + text[:20] + "..." + text[len(text)-20:] + "\n") lines := strings.Split(text, "\n")
if len(lines) > 3 {
// Show only first and last line of context with a separator
if lines[0] != "" {
_, _ = buff.WriteString(" " + lines[0] + "\n")
}
_, _ = buff.WriteString(" ...\n")
if lines[len(lines)-1] != "" {
_, _ = buff.WriteString(" " + lines[len(lines)-1] + "\n")
}
} else { } else {
for line := range strings.SplitSeq(text, "\n") { // Show all lines for small contexts
for _, line := range lines {
if line == "" {
continue
}
_, _ = buff.WriteString(" " + line + "\n") _, _ = buff.WriteString(" " + line + "\n")
} }
} }

View file

@ -101,6 +101,15 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
} }
notifyLspOpenFile(ctx, filePath, w.lspClients) notifyLspOpenFile(ctx, filePath, w.lspClients)
// Get old content for diff if file exists
oldContent := ""
if fileInfo != nil && !fileInfo.IsDir() {
oldBytes, readErr := os.ReadFile(filePath)
if readErr == nil {
oldContent = string(oldBytes)
}
}
p := permission.Default.Request( p := permission.Default.Request(
permission.CreatePermissionRequest{ permission.CreatePermissionRequest{
Path: filePath, Path: filePath,
@ -109,7 +118,7 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
Description: fmt.Sprintf("Create file %s", filePath), Description: fmt.Sprintf("Create file %s", filePath),
Params: WritePermissionsParams{ Params: WritePermissionsParams{
FilePath: filePath, FilePath: filePath,
Content: GenerateDiff("", params.Content), Content: GenerateDiff(oldContent, params.Content),
}, },
}, },
) )

View file

@ -79,10 +79,18 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
p.isViewportFocus = !p.isViewportFocus p.isViewportFocus = !p.isViewportFocus
if p.isViewportFocus { if p.isViewportFocus {
p.selectOption.Blur() p.selectOption.Blur()
// Add a visual indicator for focus change
cmds = append(cmds, tea.Batch(
util.CmdHandler(util.InfoMsg("Viewing content - use arrow keys to scroll")),
))
} else { } else {
p.selectOption.Focus() p.selectOption.Focus()
// Add a visual indicator for focus change
cmds = append(cmds, tea.Batch(
util.CmdHandler(util.InfoMsg("Select an action")),
))
} }
return p, nil return p, tea.Batch(cmds...)
} }
} }
@ -133,34 +141,55 @@ func (p *permissionDialogCmp) render() string {
case tools.BashToolName: case tools.BashToolName:
pr := p.permission.Params.(tools.BashPermissionsParams) pr := p.permission.Params.(tools.BashPermissionsParams)
headerParts = append(headerParts, keyStyle.Render("Command:")) headerParts = append(headerParts, keyStyle.Render("Command:"))
content, _ = r.Render(fmt.Sprintf("```bash\n%s\n```", pr.Command)) content = fmt.Sprintf("```bash\n%s\n```", pr.Command)
case tools.EditToolName: case tools.EditToolName:
pr := p.permission.Params.(tools.EditPermissionsParams) pr := p.permission.Params.(tools.EditPermissionsParams)
headerParts = append(headerParts, keyStyle.Render("Update")) headerParts = append(headerParts, keyStyle.Render("Update"))
content, _ = r.Render(fmt.Sprintf("```diff\n%s\n```", pr.Diff)) content = fmt.Sprintf("```\n%s\n```", pr.Diff)
case tools.WriteToolName: case tools.WriteToolName:
pr := p.permission.Params.(tools.WritePermissionsParams) pr := p.permission.Params.(tools.WritePermissionsParams)
headerParts = append(headerParts, keyStyle.Render("Content")) headerParts = append(headerParts, keyStyle.Render("Content"))
content, _ = r.Render(fmt.Sprintf("```diff\n%s\n```", pr.Content)) content = fmt.Sprintf("```\n%s\n```", pr.Content)
case tools.FetchToolName: case tools.FetchToolName:
pr := p.permission.Params.(tools.FetchPermissionsParams) pr := p.permission.Params.(tools.FetchPermissionsParams)
headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL)) headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
default: default:
content, _ = r.Render(p.permission.Description) content = p.permission.Description
} }
renderedContent, _ := r.Render(content)
headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...)) headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
p.contentViewPort.Width = p.width - 2 - 2 p.contentViewPort.Width = p.width - 2 - 2
p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1 p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
p.contentViewPort.SetContent(content) p.contentViewPort.SetContent(renderedContent)
contentBorder := lipgloss.RoundedBorder()
// Make focus change more apparent with different border styles and colors
var contentBorder lipgloss.Border
var borderColor lipgloss.TerminalColor
if p.isViewportFocus { if p.isViewportFocus {
contentBorder = lipgloss.DoubleBorder() contentBorder = lipgloss.DoubleBorder()
borderColor = styles.Blue
} else {
contentBorder = lipgloss.RoundedBorder()
borderColor = styles.Flamingo
} }
cotentStyle := lipgloss.NewStyle().MarginTop(1).Padding(0, 1).Border(contentBorder).BorderForeground(styles.Flamingo)
contentFinal := cotentStyle.Render(p.contentViewPort.View()) contentStyle := lipgloss.NewStyle().
if content == "" { MarginTop(1).
Padding(0, 1).
Border(contentBorder).
BorderForeground(borderColor)
if p.isViewportFocus {
contentStyle = contentStyle.BorderBackground(styles.Surface0)
}
contentFinal := contentStyle.Render(p.contentViewPort.View())
if renderedContent == "" {
contentFinal = "" contentFinal = ""
} }
return lipgloss.JoinVertical( return lipgloss.JoinVertical(
lipgloss.Top, lipgloss.Top,
headerContent, headerContent,
@ -241,12 +270,13 @@ func NewPermissionDialogCmd(permission permission.PermissionRequest) tea.Cmd {
minWidth := 100 minWidth := 100
minHeight := 30 minHeight := 30
// Make the dialog size more appropriate for bash commands
switch permission.ToolName { switch permission.ToolName {
case tools.BashToolName: case tools.BashToolName:
widthRatio = 0.5 widthRatio = 0.7
heightRatio = 0.3 heightRatio = 0.5
minWidth = 80 minWidth = 100
minHeight = 20 minHeight = 30
} }
// Return the dialog command // Return the dialog command
return util.CmdHandler(core.DialogMsg{ return util.CmdHandler(core.DialogMsg{

View file

@ -166,6 +166,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.dialog = d.(core.DialogCmp) a.dialog = d.(core.DialogCmp)
return a, cmd return a, cmd
} }
s, _ := a.status.Update(msg)
a.status = s
p, cmd := a.pages[a.currentPage].Update(msg) p, cmd := a.pages[a.currentPage].Update(msg)
a.pages[a.currentPage] = p a.pages[a.currentPage] = p
return a, cmd return a, cmd