|
@@ -19,6 +19,7 @@ import (
|
|
|
"github.com/sst/opencode/internal/components/chat"
|
|
"github.com/sst/opencode/internal/components/chat"
|
|
|
cmdcomp "github.com/sst/opencode/internal/components/commands"
|
|
cmdcomp "github.com/sst/opencode/internal/components/commands"
|
|
|
"github.com/sst/opencode/internal/components/dialog"
|
|
"github.com/sst/opencode/internal/components/dialog"
|
|
|
|
|
+ "github.com/sst/opencode/internal/components/fileviewer"
|
|
|
"github.com/sst/opencode/internal/components/modal"
|
|
"github.com/sst/opencode/internal/components/modal"
|
|
|
"github.com/sst/opencode/internal/components/status"
|
|
"github.com/sst/opencode/internal/components/status"
|
|
|
"github.com/sst/opencode/internal/components/toast"
|
|
"github.com/sst/opencode/internal/components/toast"
|
|
@@ -40,6 +41,7 @@ const (
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const interruptDebounceTimeout = 1 * time.Second
|
|
const interruptDebounceTimeout = 1 * time.Second
|
|
|
|
|
+const fileViewerFullWidthCutoff = 200
|
|
|
|
|
|
|
|
type appModel struct {
|
|
type appModel struct {
|
|
|
width, height int
|
|
width, height int
|
|
@@ -56,6 +58,12 @@ type appModel struct {
|
|
|
toastManager *toast.ToastManager
|
|
toastManager *toast.ToastManager
|
|
|
interruptKeyState InterruptKeyState
|
|
interruptKeyState InterruptKeyState
|
|
|
lastScroll time.Time
|
|
lastScroll time.Time
|
|
|
|
|
+ messagesRight bool
|
|
|
|
|
+ fileViewer fileviewer.Model
|
|
|
|
|
+ lastMouse tea.Mouse
|
|
|
|
|
+ fileViewerStart int
|
|
|
|
|
+ fileViewerEnd int
|
|
|
|
|
+ fileViewerHit bool
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (a appModel) Init() tea.Cmd {
|
|
func (a appModel) Init() tea.Cmd {
|
|
@@ -71,6 +79,7 @@ func (a appModel) Init() tea.Cmd {
|
|
|
cmds = append(cmds, a.status.Init())
|
|
cmds = append(cmds, a.status.Init())
|
|
|
cmds = append(cmds, a.completions.Init())
|
|
cmds = append(cmds, a.completions.Init())
|
|
|
cmds = append(cmds, a.toastManager.Init())
|
|
cmds = append(cmds, a.toastManager.Init())
|
|
|
|
|
+ cmds = append(cmds, a.fileViewer.Init())
|
|
|
|
|
|
|
|
// Check if we should show the init dialog
|
|
// Check if we should show the init dialog
|
|
|
cmds = append(cmds, func() tea.Msg {
|
|
cmds = append(cmds, func() tea.Msg {
|
|
@@ -99,6 +108,7 @@ var BUGGED_SCROLL_KEYS = map[string]bool{
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
|
|
+ var cmd tea.Cmd
|
|
|
var cmds []tea.Cmd
|
|
var cmds []tea.Cmd
|
|
|
|
|
|
|
|
switch msg := msg.(type) {
|
|
switch msg := msg.(type) {
|
|
@@ -112,10 +122,20 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
if a.modal != nil {
|
|
if a.modal != nil {
|
|
|
switch keyString {
|
|
switch keyString {
|
|
|
// Escape always closes current modal
|
|
// Escape always closes current modal
|
|
|
- case "esc", "ctrl+c":
|
|
|
|
|
|
|
+ case "esc":
|
|
|
cmd := a.modal.Close()
|
|
cmd := a.modal.Close()
|
|
|
a.modal = nil
|
|
a.modal = nil
|
|
|
return a, cmd
|
|
return a, cmd
|
|
|
|
|
+ case "ctrl+c":
|
|
|
|
|
+ // give the modal a chance to handle the ctrl+c
|
|
|
|
|
+ updatedModal, cmd := a.modal.Update(msg)
|
|
|
|
|
+ a.modal = updatedModal.(layout.Modal)
|
|
|
|
|
+ if cmd != nil {
|
|
|
|
|
+ return a, cmd
|
|
|
|
|
+ }
|
|
|
|
|
+ cmd = a.modal.Close()
|
|
|
|
|
+ a.modal = nil
|
|
|
|
|
+ return a, cmd
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Pass all other key presses to the modal
|
|
// Pass all other key presses to the modal
|
|
@@ -246,10 +266,28 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
if a.modal != nil {
|
|
if a.modal != nil {
|
|
|
return a, nil
|
|
return a, nil
|
|
|
}
|
|
}
|
|
|
- updated, cmd := a.messages.Update(msg)
|
|
|
|
|
- a.messages = updated.(chat.MessagesComponent)
|
|
|
|
|
- cmds = append(cmds, cmd)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ var cmd tea.Cmd
|
|
|
|
|
+ if a.fileViewerHit {
|
|
|
|
|
+ a.fileViewer, cmd = a.fileViewer.Update(msg)
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ updated, cmd := a.messages.Update(msg)
|
|
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return a, tea.Batch(cmds...)
|
|
return a, tea.Batch(cmds...)
|
|
|
|
|
+ case tea.MouseMotionMsg:
|
|
|
|
|
+ a.lastMouse = msg.Mouse()
|
|
|
|
|
+ a.fileViewerHit = a.fileViewer.HasFile() &&
|
|
|
|
|
+ a.lastMouse.X > a.fileViewerStart &&
|
|
|
|
|
+ a.lastMouse.X < a.fileViewerEnd
|
|
|
|
|
+ case tea.MouseClickMsg:
|
|
|
|
|
+ a.lastMouse = msg.Mouse()
|
|
|
|
|
+ a.fileViewerHit = a.fileViewer.HasFile() &&
|
|
|
|
|
+ a.lastMouse.X > a.fileViewerStart &&
|
|
|
|
|
+ a.lastMouse.X < a.fileViewerEnd
|
|
|
case tea.BackgroundColorMsg:
|
|
case tea.BackgroundColorMsg:
|
|
|
styles.Terminal = &styles.TerminalInfo{
|
|
styles.Terminal = &styles.TerminalInfo{
|
|
|
Background: msg.Color,
|
|
Background: msg.Color,
|
|
@@ -266,6 +304,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
case modal.CloseModalMsg:
|
|
case modal.CloseModalMsg:
|
|
|
|
|
+ a.editor.Focus()
|
|
|
var cmd tea.Cmd
|
|
var cmd tea.Cmd
|
|
|
if a.modal != nil {
|
|
if a.modal != nil {
|
|
|
cmd = a.modal.Close()
|
|
cmd = a.modal.Close()
|
|
@@ -349,22 +388,47 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
|
|
slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
|
|
|
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
|
|
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
|
|
|
}
|
|
}
|
|
|
|
|
+ case opencode.EventListResponseEventFileWatcherUpdated:
|
|
|
|
|
+ if a.fileViewer.HasFile() {
|
|
|
|
|
+ if a.fileViewer.Filename() == msg.Properties.File {
|
|
|
|
|
+ return a.openFile(msg.Properties.File)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
case tea.WindowSizeMsg:
|
|
case tea.WindowSizeMsg:
|
|
|
msg.Height -= 2 // Make space for the status bar
|
|
msg.Height -= 2 // Make space for the status bar
|
|
|
a.width, a.height = msg.Width, msg.Height
|
|
a.width, a.height = msg.Width, msg.Height
|
|
|
|
|
+ container := min(a.width, 84)
|
|
|
|
|
+ if a.fileViewer.HasFile() {
|
|
|
|
|
+ if a.width < fileViewerFullWidthCutoff {
|
|
|
|
|
+ container = a.width
|
|
|
|
|
+ } else {
|
|
|
|
|
+ container = min(min(a.width, max(a.width/2, 50)), 84)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
layout.Current = &layout.LayoutInfo{
|
|
layout.Current = &layout.LayoutInfo{
|
|
|
Viewport: layout.Dimensions{
|
|
Viewport: layout.Dimensions{
|
|
|
Width: a.width,
|
|
Width: a.width,
|
|
|
Height: a.height,
|
|
Height: a.height,
|
|
|
},
|
|
},
|
|
|
Container: layout.Dimensions{
|
|
Container: layout.Dimensions{
|
|
|
- Width: min(a.width, 80),
|
|
|
|
|
|
|
+ Width: container,
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
- // Update child component sizes
|
|
|
|
|
- messagesHeight := a.height - 6 // Leave room for editor and status bar
|
|
|
|
|
- a.messages.SetSize(a.width, messagesHeight)
|
|
|
|
|
- a.editor.SetSize(min(a.width, 80), 5)
|
|
|
|
|
|
|
+ mainWidth := layout.Current.Container.Width
|
|
|
|
|
+ a.messages.SetWidth(mainWidth - 4)
|
|
|
|
|
+
|
|
|
|
|
+ sideWidth := a.width - mainWidth
|
|
|
|
|
+ if a.width < fileViewerFullWidthCutoff {
|
|
|
|
|
+ sideWidth = a.width
|
|
|
|
|
+ }
|
|
|
|
|
+ a.fileViewerStart = mainWidth
|
|
|
|
|
+ a.fileViewerEnd = a.fileViewerStart + sideWidth
|
|
|
|
|
+ if a.messagesRight {
|
|
|
|
|
+ a.fileViewerStart = 0
|
|
|
|
|
+ a.fileViewerEnd = sideWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ a.fileViewer, cmd = a.fileViewer.SetSize(sideWidth, layout.Current.Viewport.Height)
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
case app.SessionSelectedMsg:
|
|
case app.SessionSelectedMsg:
|
|
|
messages, err := a.app.ListMessages(context.Background(), msg.ID)
|
|
messages, err := a.app.ListMessages(context.Background(), msg.ID)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
@@ -373,6 +437,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
}
|
|
}
|
|
|
a.app.Session = msg
|
|
a.app.Session = msg
|
|
|
a.app.Messages = messages
|
|
a.app.Messages = messages
|
|
|
|
|
+ return a, util.CmdHandler(app.SessionLoadedMsg{})
|
|
|
case app.ModelSelectedMsg:
|
|
case app.ModelSelectedMsg:
|
|
|
a.app.Provider = &msg.Provider
|
|
a.app.Provider = &msg.Provider
|
|
|
a.app.Model = &msg.Model
|
|
a.app.Model = &msg.Model
|
|
@@ -395,24 +460,22 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
// Reset interrupt key state after timeout
|
|
// Reset interrupt key state after timeout
|
|
|
a.interruptKeyState = InterruptKeyIdle
|
|
a.interruptKeyState = InterruptKeyIdle
|
|
|
a.editor.SetInterruptKeyInDebounce(false)
|
|
a.editor.SetInterruptKeyInDebounce(false)
|
|
|
|
|
+ case dialog.FindSelectedMsg:
|
|
|
|
|
+ return a.openFile(msg.FilePath)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // update status bar
|
|
|
|
|
s, cmd := a.status.Update(msg)
|
|
s, cmd := a.status.Update(msg)
|
|
|
cmds = append(cmds, cmd)
|
|
cmds = append(cmds, cmd)
|
|
|
a.status = s.(status.StatusComponent)
|
|
a.status = s.(status.StatusComponent)
|
|
|
|
|
|
|
|
- // update editor
|
|
|
|
|
u, cmd := a.editor.Update(msg)
|
|
u, cmd := a.editor.Update(msg)
|
|
|
a.editor = u.(chat.EditorComponent)
|
|
a.editor = u.(chat.EditorComponent)
|
|
|
cmds = append(cmds, cmd)
|
|
cmds = append(cmds, cmd)
|
|
|
|
|
|
|
|
- // update messages
|
|
|
|
|
u, cmd = a.messages.Update(msg)
|
|
u, cmd = a.messages.Update(msg)
|
|
|
a.messages = u.(chat.MessagesComponent)
|
|
a.messages = u.(chat.MessagesComponent)
|
|
|
cmds = append(cmds, cmd)
|
|
cmds = append(cmds, cmd)
|
|
|
|
|
|
|
|
- // update modal
|
|
|
|
|
if a.modal != nil {
|
|
if a.modal != nil {
|
|
|
u, cmd := a.modal.Update(msg)
|
|
u, cmd := a.modal.Update(msg)
|
|
|
a.modal = u.(layout.Modal)
|
|
a.modal = u.(layout.Modal)
|
|
@@ -425,86 +488,95 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
cmds = append(cmds, cmd)
|
|
cmds = append(cmds, cmd)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ fv, cmd := a.fileViewer.Update(msg)
|
|
|
|
|
+ a.fileViewer = fv
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+
|
|
|
return a, tea.Batch(cmds...)
|
|
return a, tea.Batch(cmds...)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (a appModel) View() string {
|
|
func (a appModel) View() string {
|
|
|
- mainLayout := a.chat(layout.Current.Container.Width, lipgloss.Center)
|
|
|
|
|
|
|
+ t := theme.CurrentTheme()
|
|
|
|
|
+
|
|
|
|
|
+ var mainLayout string
|
|
|
|
|
+ mainWidth := layout.Current.Container.Width - 4
|
|
|
|
|
+ if a.app.Session.ID == "" {
|
|
|
|
|
+ mainLayout = a.home(mainWidth)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ mainLayout = a.chat(mainWidth)
|
|
|
|
|
+ }
|
|
|
|
|
+ mainLayout = styles.NewStyle().
|
|
|
|
|
+ Background(t.Background()).
|
|
|
|
|
+ Padding(0, 2).
|
|
|
|
|
+ Render(mainLayout)
|
|
|
|
|
+
|
|
|
|
|
+ mainHeight := lipgloss.Height(mainLayout)
|
|
|
|
|
+
|
|
|
|
|
+ if a.fileViewer.HasFile() {
|
|
|
|
|
+ file := a.fileViewer.View()
|
|
|
|
|
+ baseStyle := styles.NewStyle().Background(t.BackgroundPanel())
|
|
|
|
|
+ sidePanel := baseStyle.Height(mainHeight).Render(file)
|
|
|
|
|
+ if a.width >= fileViewerFullWidthCutoff {
|
|
|
|
|
+ if a.messagesRight {
|
|
|
|
|
+ mainLayout = lipgloss.JoinHorizontal(
|
|
|
|
|
+ lipgloss.Top,
|
|
|
|
|
+ sidePanel,
|
|
|
|
|
+ mainLayout,
|
|
|
|
|
+ )
|
|
|
|
|
+ } else {
|
|
|
|
|
+ mainLayout = lipgloss.JoinHorizontal(
|
|
|
|
|
+ lipgloss.Top,
|
|
|
|
|
+ mainLayout,
|
|
|
|
|
+ sidePanel,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ mainLayout = sidePanel
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ mainLayout = lipgloss.PlaceHorizontal(
|
|
|
|
|
+ a.width,
|
|
|
|
|
+ lipgloss.Center,
|
|
|
|
|
+ mainLayout,
|
|
|
|
|
+ styles.WhitespaceStyle(t.Background()),
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ mainStyle := styles.NewStyle().Background(t.Background())
|
|
|
|
|
+ mainLayout = mainStyle.Render(mainLayout)
|
|
|
|
|
+
|
|
|
if a.modal != nil {
|
|
if a.modal != nil {
|
|
|
mainLayout = a.modal.Render(mainLayout)
|
|
mainLayout = a.modal.Render(mainLayout)
|
|
|
}
|
|
}
|
|
|
mainLayout = a.toastManager.RenderOverlay(mainLayout)
|
|
mainLayout = a.toastManager.RenderOverlay(mainLayout)
|
|
|
|
|
+
|
|
|
if theme.CurrentThemeUsesAnsiColors() {
|
|
if theme.CurrentThemeUsesAnsiColors() {
|
|
|
mainLayout = util.ConvertRGBToAnsi16Colors(mainLayout)
|
|
mainLayout = util.ConvertRGBToAnsi16Colors(mainLayout)
|
|
|
}
|
|
}
|
|
|
return mainLayout + "\n" + a.status.View()
|
|
return mainLayout + "\n" + a.status.View()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func (a appModel) chat(width int, align lipgloss.Position) string {
|
|
|
|
|
- editorView := a.editor.View(width, align)
|
|
|
|
|
- lines := a.editor.Lines()
|
|
|
|
|
- messagesView := a.messages.View()
|
|
|
|
|
- if a.app.Session.ID == "" {
|
|
|
|
|
- messagesView = a.home()
|
|
|
|
|
- }
|
|
|
|
|
- editorHeight := max(lines, 5)
|
|
|
|
|
-
|
|
|
|
|
- t := theme.CurrentTheme()
|
|
|
|
|
- centeredEditorView := lipgloss.PlaceHorizontal(
|
|
|
|
|
- a.width,
|
|
|
|
|
- align,
|
|
|
|
|
- editorView,
|
|
|
|
|
- styles.WhitespaceStyle(t.Background()),
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- mainLayout := layout.Render(
|
|
|
|
|
- layout.FlexOptions{
|
|
|
|
|
- Direction: layout.Column,
|
|
|
|
|
- Width: a.width,
|
|
|
|
|
- Height: a.height,
|
|
|
|
|
- },
|
|
|
|
|
- layout.FlexItem{
|
|
|
|
|
- View: messagesView,
|
|
|
|
|
- Grow: true,
|
|
|
|
|
- },
|
|
|
|
|
- layout.FlexItem{
|
|
|
|
|
- View: centeredEditorView,
|
|
|
|
|
- FixedSize: 5,
|
|
|
|
|
|
|
+func (a appModel) openFile(filepath string) (tea.Model, tea.Cmd) {
|
|
|
|
|
+ var cmd tea.Cmd
|
|
|
|
|
+ response, err := a.app.Client.File.Read(
|
|
|
|
|
+ context.Background(),
|
|
|
|
|
+ opencode.FileReadParams{
|
|
|
|
|
+ Path: opencode.F(filepath),
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
- if lines > 1 {
|
|
|
|
|
- editorWidth := min(a.width, 80)
|
|
|
|
|
- editorX := (a.width - editorWidth) / 2
|
|
|
|
|
- editorY := a.height - editorHeight
|
|
|
|
|
- mainLayout = layout.PlaceOverlay(
|
|
|
|
|
- editorX,
|
|
|
|
|
- editorY,
|
|
|
|
|
- a.editor.Content(width, align),
|
|
|
|
|
- mainLayout,
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ slog.Error("Failed to read file", "error", err)
|
|
|
|
|
+ return a, toast.NewErrorToast("Failed to read file")
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- if a.showCompletionDialog {
|
|
|
|
|
- editorWidth := min(a.width, 80)
|
|
|
|
|
- editorX := (a.width - editorWidth) / 2
|
|
|
|
|
- a.completions.SetWidth(editorWidth)
|
|
|
|
|
- overlay := a.completions.View()
|
|
|
|
|
- overlayHeight := lipgloss.Height(overlay)
|
|
|
|
|
- editorY := a.height - editorHeight + 1
|
|
|
|
|
-
|
|
|
|
|
- mainLayout = layout.PlaceOverlay(
|
|
|
|
|
- editorX,
|
|
|
|
|
- editorY-overlayHeight,
|
|
|
|
|
- overlay,
|
|
|
|
|
- mainLayout,
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return mainLayout
|
|
|
|
|
|
|
+ a.fileViewer, cmd = a.fileViewer.SetFile(
|
|
|
|
|
+ filepath,
|
|
|
|
|
+ response.Content,
|
|
|
|
|
+ response.Type == "patch",
|
|
|
|
|
+ )
|
|
|
|
|
+ return a, cmd
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-func (a appModel) home() string {
|
|
|
|
|
|
|
+func (a appModel) home(width int) string {
|
|
|
t := theme.CurrentTheme()
|
|
t := theme.CurrentTheme()
|
|
|
baseStyle := styles.NewStyle().Background(t.Background())
|
|
baseStyle := styles.NewStyle().Background(t.Background())
|
|
|
base := baseStyle.Render
|
|
base := baseStyle.Render
|
|
@@ -536,7 +608,7 @@ func (a appModel) home() string {
|
|
|
|
|
|
|
|
logoAndVersion := strings.Join([]string{logo, version}, "\n")
|
|
logoAndVersion := strings.Join([]string{logo, version}, "\n")
|
|
|
logoAndVersion = lipgloss.PlaceHorizontal(
|
|
logoAndVersion = lipgloss.PlaceHorizontal(
|
|
|
- a.width,
|
|
|
|
|
|
|
+ width,
|
|
|
lipgloss.Center,
|
|
lipgloss.Center,
|
|
|
logoAndVersion,
|
|
logoAndVersion,
|
|
|
styles.WhitespaceStyle(t.Background()),
|
|
styles.WhitespaceStyle(t.Background()),
|
|
@@ -547,13 +619,15 @@ func (a appModel) home() string {
|
|
|
cmdcomp.WithLimit(6),
|
|
cmdcomp.WithLimit(6),
|
|
|
)
|
|
)
|
|
|
cmds := lipgloss.PlaceHorizontal(
|
|
cmds := lipgloss.PlaceHorizontal(
|
|
|
- a.width,
|
|
|
|
|
|
|
+ width,
|
|
|
lipgloss.Center,
|
|
lipgloss.Center,
|
|
|
commandsView.View(),
|
|
commandsView.View(),
|
|
|
styles.WhitespaceStyle(t.Background()),
|
|
styles.WhitespaceStyle(t.Background()),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
lines := []string{}
|
|
lines := []string{}
|
|
|
|
|
+ lines = append(lines, "")
|
|
|
|
|
+ lines = append(lines, "")
|
|
|
lines = append(lines, logoAndVersion)
|
|
lines = append(lines, logoAndVersion)
|
|
|
lines = append(lines, "")
|
|
lines = append(lines, "")
|
|
|
lines = append(lines, "")
|
|
lines = append(lines, "")
|
|
@@ -561,18 +635,100 @@ func (a appModel) home() string {
|
|
|
// lines = append(lines, base("config ")+muted(config))
|
|
// lines = append(lines, base("config ")+muted(config))
|
|
|
// lines = append(lines, "")
|
|
// lines = append(lines, "")
|
|
|
lines = append(lines, cmds)
|
|
lines = append(lines, cmds)
|
|
|
|
|
+ lines = append(lines, "")
|
|
|
|
|
+ lines = append(lines, "")
|
|
|
|
|
+
|
|
|
|
|
+ mainHeight := lipgloss.Height(strings.Join(lines, "\n"))
|
|
|
|
|
|
|
|
- return lipgloss.Place(
|
|
|
|
|
- a.width,
|
|
|
|
|
- a.height-5,
|
|
|
|
|
|
|
+ editorWidth := min(width, 80)
|
|
|
|
|
+ editorView := a.editor.View(editorWidth)
|
|
|
|
|
+ editorView = lipgloss.PlaceHorizontal(
|
|
|
|
|
+ width,
|
|
|
|
|
+ lipgloss.Center,
|
|
|
|
|
+ editorView,
|
|
|
|
|
+ styles.WhitespaceStyle(t.Background()),
|
|
|
|
|
+ )
|
|
|
|
|
+ lines = append(lines, editorView)
|
|
|
|
|
+
|
|
|
|
|
+ editorLines := a.editor.Lines()
|
|
|
|
|
+
|
|
|
|
|
+ mainLayout := lipgloss.Place(
|
|
|
|
|
+ width,
|
|
|
|
|
+ a.height,
|
|
|
lipgloss.Center,
|
|
lipgloss.Center,
|
|
|
lipgloss.Center,
|
|
lipgloss.Center,
|
|
|
baseStyle.Render(strings.Join(lines, "\n")),
|
|
baseStyle.Render(strings.Join(lines, "\n")),
|
|
|
styles.WhitespaceStyle(t.Background()),
|
|
styles.WhitespaceStyle(t.Background()),
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+ editorX := (width - editorWidth) / 2
|
|
|
|
|
+ editorY := (a.height / 2) + (mainHeight / 2) - 2
|
|
|
|
|
+
|
|
|
|
|
+ if editorLines > 1 {
|
|
|
|
|
+ mainLayout = layout.PlaceOverlay(
|
|
|
|
|
+ editorX,
|
|
|
|
|
+ editorY,
|
|
|
|
|
+ a.editor.Content(editorWidth),
|
|
|
|
|
+ mainLayout,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if a.showCompletionDialog {
|
|
|
|
|
+ a.completions.SetWidth(editorWidth)
|
|
|
|
|
+ overlay := a.completions.View()
|
|
|
|
|
+ overlayHeight := lipgloss.Height(overlay)
|
|
|
|
|
+
|
|
|
|
|
+ mainLayout = layout.PlaceOverlay(
|
|
|
|
|
+ editorX,
|
|
|
|
|
+ editorY-overlayHeight+1,
|
|
|
|
|
+ overlay,
|
|
|
|
|
+ mainLayout,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return mainLayout
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (a appModel) chat(width int) string {
|
|
|
|
|
+ editorView := a.editor.View(width)
|
|
|
|
|
+ lines := a.editor.Lines()
|
|
|
|
|
+ messagesView := a.messages.View(width, a.height-5)
|
|
|
|
|
+
|
|
|
|
|
+ editorWidth := lipgloss.Width(editorView)
|
|
|
|
|
+ editorHeight := max(lines, 5)
|
|
|
|
|
+
|
|
|
|
|
+ mainLayout := messagesView + "\n" + editorView
|
|
|
|
|
+ editorX := (a.width - editorWidth) / 2
|
|
|
|
|
+
|
|
|
|
|
+ if lines > 1 {
|
|
|
|
|
+ editorY := a.height - editorHeight
|
|
|
|
|
+ mainLayout = layout.PlaceOverlay(
|
|
|
|
|
+ editorX,
|
|
|
|
|
+ editorY,
|
|
|
|
|
+ a.editor.Content(width),
|
|
|
|
|
+ mainLayout,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if a.showCompletionDialog {
|
|
|
|
|
+ a.completions.SetWidth(editorWidth)
|
|
|
|
|
+ overlay := a.completions.View()
|
|
|
|
|
+ overlayHeight := lipgloss.Height(overlay)
|
|
|
|
|
+ editorY := a.height - editorHeight + 1
|
|
|
|
|
+
|
|
|
|
|
+ mainLayout = layout.PlaceOverlay(
|
|
|
|
|
+ editorX,
|
|
|
|
|
+ editorY-overlayHeight,
|
|
|
|
|
+ overlay,
|
|
|
|
|
+ mainLayout,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return mainLayout
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
|
func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
|
|
|
|
+ var cmd tea.Cmd
|
|
|
cmds := []tea.Cmd{
|
|
cmds := []tea.Cmd{
|
|
|
util.CmdHandler(commands.CommandExecutedMsg(command)),
|
|
util.CmdHandler(commands.CommandExecutedMsg(command)),
|
|
|
}
|
|
}
|
|
@@ -676,6 +832,22 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|
|
case commands.ThemeListCommand:
|
|
case commands.ThemeListCommand:
|
|
|
themeDialog := dialog.NewThemeDialog()
|
|
themeDialog := dialog.NewThemeDialog()
|
|
|
a.modal = themeDialog
|
|
a.modal = themeDialog
|
|
|
|
|
+ case commands.FileListCommand:
|
|
|
|
|
+ a.editor.Blur()
|
|
|
|
|
+ provider := completions.NewFileAndFolderContextGroup(a.app)
|
|
|
|
|
+ findDialog := dialog.NewFindDialog(provider)
|
|
|
|
|
+ findDialog.SetWidth(layout.Current.Container.Width - 8)
|
|
|
|
|
+ a.modal = findDialog
|
|
|
|
|
+ case commands.FileCloseCommand:
|
|
|
|
|
+ a.fileViewer, cmd = a.fileViewer.Clear()
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ case commands.FileDiffToggleCommand:
|
|
|
|
|
+ a.fileViewer, cmd = a.fileViewer.ToggleDiff()
|
|
|
|
|
+ a.app.State.SplitDiff = a.fileViewer.DiffStyle() == fileviewer.DiffStyleSplit
|
|
|
|
|
+ a.app.SaveState()
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ case commands.FileSearchCommand:
|
|
|
|
|
+ return a, nil
|
|
|
case commands.ProjectInitCommand:
|
|
case commands.ProjectInitCommand:
|
|
|
cmds = append(cmds, a.app.InitializeProject(context.Background()))
|
|
cmds = append(cmds, a.app.InitializeProject(context.Background()))
|
|
|
case commands.InputClearCommand:
|
|
case commands.InputClearCommand:
|
|
@@ -697,20 +869,6 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|
|
updated, cmd := a.editor.Newline()
|
|
updated, cmd := a.editor.Newline()
|
|
|
a.editor = updated.(chat.EditorComponent)
|
|
a.editor = updated.(chat.EditorComponent)
|
|
|
cmds = append(cmds, cmd)
|
|
cmds = append(cmds, cmd)
|
|
|
- case commands.HistoryPreviousCommand:
|
|
|
|
|
- if a.showCompletionDialog {
|
|
|
|
|
- return a, nil
|
|
|
|
|
- }
|
|
|
|
|
- updated, cmd := a.editor.Previous()
|
|
|
|
|
- a.editor = updated.(chat.EditorComponent)
|
|
|
|
|
- cmds = append(cmds, cmd)
|
|
|
|
|
- case commands.HistoryNextCommand:
|
|
|
|
|
- if a.showCompletionDialog {
|
|
|
|
|
- return a, nil
|
|
|
|
|
- }
|
|
|
|
|
- updated, cmd := a.editor.Next()
|
|
|
|
|
- a.editor = updated.(chat.EditorComponent)
|
|
|
|
|
- cmds = append(cmds, cmd)
|
|
|
|
|
case commands.MessagesFirstCommand:
|
|
case commands.MessagesFirstCommand:
|
|
|
updated, cmd := a.messages.First()
|
|
updated, cmd := a.messages.First()
|
|
|
a.messages = updated.(chat.MessagesComponent)
|
|
a.messages = updated.(chat.MessagesComponent)
|
|
@@ -720,21 +878,62 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
|
|
a.messages = updated.(chat.MessagesComponent)
|
|
a.messages = updated.(chat.MessagesComponent)
|
|
|
cmds = append(cmds, cmd)
|
|
cmds = append(cmds, cmd)
|
|
|
case commands.MessagesPageUpCommand:
|
|
case commands.MessagesPageUpCommand:
|
|
|
- updated, cmd := a.messages.PageUp()
|
|
|
|
|
- a.messages = updated.(chat.MessagesComponent)
|
|
|
|
|
- cmds = append(cmds, cmd)
|
|
|
|
|
|
|
+ if a.fileViewer.HasFile() {
|
|
|
|
|
+ a.fileViewer, cmd = a.fileViewer.PageUp()
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ updated, cmd := a.messages.PageUp()
|
|
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ }
|
|
|
case commands.MessagesPageDownCommand:
|
|
case commands.MessagesPageDownCommand:
|
|
|
- updated, cmd := a.messages.PageDown()
|
|
|
|
|
- a.messages = updated.(chat.MessagesComponent)
|
|
|
|
|
- cmds = append(cmds, cmd)
|
|
|
|
|
|
|
+ if a.fileViewer.HasFile() {
|
|
|
|
|
+ a.fileViewer, cmd = a.fileViewer.PageDown()
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ updated, cmd := a.messages.PageDown()
|
|
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ }
|
|
|
case commands.MessagesHalfPageUpCommand:
|
|
case commands.MessagesHalfPageUpCommand:
|
|
|
- updated, cmd := a.messages.HalfPageUp()
|
|
|
|
|
|
|
+ if a.fileViewer.HasFile() {
|
|
|
|
|
+ a.fileViewer, cmd = a.fileViewer.HalfPageUp()
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ updated, cmd := a.messages.HalfPageUp()
|
|
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ }
|
|
|
|
|
+ case commands.MessagesHalfPageDownCommand:
|
|
|
|
|
+ if a.fileViewer.HasFile() {
|
|
|
|
|
+ a.fileViewer, cmd = a.fileViewer.HalfPageDown()
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ updated, cmd := a.messages.HalfPageDown()
|
|
|
|
|
+ a.messages = updated.(chat.MessagesComponent)
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ }
|
|
|
|
|
+ case commands.MessagesPreviousCommand:
|
|
|
|
|
+ updated, cmd := a.messages.Previous()
|
|
|
a.messages = updated.(chat.MessagesComponent)
|
|
a.messages = updated.(chat.MessagesComponent)
|
|
|
cmds = append(cmds, cmd)
|
|
cmds = append(cmds, cmd)
|
|
|
- case commands.MessagesHalfPageDownCommand:
|
|
|
|
|
- updated, cmd := a.messages.HalfPageDown()
|
|
|
|
|
|
|
+ case commands.MessagesNextCommand:
|
|
|
|
|
+ updated, cmd := a.messages.Next()
|
|
|
a.messages = updated.(chat.MessagesComponent)
|
|
a.messages = updated.(chat.MessagesComponent)
|
|
|
cmds = append(cmds, cmd)
|
|
cmds = append(cmds, cmd)
|
|
|
|
|
+ case commands.MessagesLayoutToggleCommand:
|
|
|
|
|
+ a.messagesRight = !a.messagesRight
|
|
|
|
|
+ a.app.State.MessagesRight = a.messagesRight
|
|
|
|
|
+ a.app.SaveState()
|
|
|
|
|
+ case commands.MessagesCopyCommand:
|
|
|
|
|
+ selected := a.messages.Selected()
|
|
|
|
|
+ if selected != "" {
|
|
|
|
|
+ cmd = tea.SetClipboard(selected)
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ cmd = toast.NewSuccessToast("Message copied to clipboard")
|
|
|
|
|
+ cmds = append(cmds, cmd)
|
|
|
|
|
+ }
|
|
|
|
|
+ case commands.MessagesRevertCommand:
|
|
|
case commands.AppExitCommand:
|
|
case commands.AppExitCommand:
|
|
|
return a, tea.Quit
|
|
return a, tea.Quit
|
|
|
}
|
|
}
|
|
@@ -776,6 +975,8 @@ func NewModel(app *app.App) tea.Model {
|
|
|
showCompletionDialog: false,
|
|
showCompletionDialog: false,
|
|
|
toastManager: toast.NewToastManager(),
|
|
toastManager: toast.NewToastManager(),
|
|
|
interruptKeyState: InterruptKeyIdle,
|
|
interruptKeyState: InterruptKeyIdle,
|
|
|
|
|
+ fileViewer: fileviewer.New(app),
|
|
|
|
|
+ messagesRight: app.State.MessagesRight,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return model
|
|
return model
|