refactor(ui): вынести панель «Диалог» (ChatPanel) из монолита окна

Спеца 12.12: интерфейс + Nil + FormatRole в internal/ui/chat_panel.go,
Fyne-реализация в internal/ui/desktop/chat_panel.go (транскрипт + композитор).
Окно как chat.Channel рендерит Send/Ask/историю через Append; state_panel
не трогаем.
This commit is contained in:
ki.sagidullin
2026-08-20 23:48:54 +05:00
parent 4c44956d3e
commit 5c93642cf8
5 changed files with 180 additions and 50 deletions

View File

@@ -24,7 +24,7 @@ import (
)
const (
bufLimit = 200_000 // обрезка буферов панелей окна (conv/state), чтобы не расти бесконечно
bufLimit = 200_000 // обрезка буфера панели «Состояние», чтобы не расти бесконечно
selectedPref = "task.selected"
splitHPref = "split.h"
splitVPref = "split.v"
@@ -45,10 +45,9 @@ type Window struct {
// виджеты
taskList *TaskListPanel
detail *TaskDetailPanel
convLbl *widget.Label
chat *ChatPanel
stateLbl *widget.Label
logs *LogPanel
input *widget.Entry
hsplit *container.Split
vsplit *container.Split
@@ -83,8 +82,8 @@ func (w *Window) SetOnQuit(fn func()) { w.onQuit = fn }
func (w *Window) build() {
w.detail = NewTaskDetailPanel()
w.convLbl = widget.NewLabel("Выберите задачу.")
w.convLbl.Wrapping = fyne.TextWrapWord
w.chat = NewChatPanel()
w.chat.SetCommands(w.commands)
w.logs = NewLogPanel()
@@ -105,10 +104,9 @@ func (w *Window) build() {
// Рабочая область: детали сверху, диалог снизу (сплит 2×2 в плане).
details := w.detail.Widget()
convScroll := container.NewScroll(w.convLbl)
right := container.NewVSplit(
container.NewScroll(details),
convScroll,
w.chat.Transcript(),
)
w.vsplit = right
@@ -118,25 +116,7 @@ func (w *Window) build() {
container.NewTabItem("Состояние", container.NewScroll(w.stateLbl)),
)
// Ввод + кнопки команд
w.input = widget.NewEntry()
w.input.SetPlaceHolder("Сообщение… (Enter — отправить)")
w.input.OnSubmitted = func(s string) {
s = strings.TrimSpace(s)
if s == "" {
return
}
w.input.SetText("")
w.commands.SendText(s)
}
cmdBar := container.NewHBox(
widget.NewButton("Новая", func() { w.commands.Start() }),
widget.NewButton("Создавай", func() { w.commands.Approve() }),
widget.NewButton("Пропустить", func() { w.commands.Skip() }),
widget.NewButton("Отмена", func() { w.commands.Cancel() }),
)
inputRow := container.NewBorder(nil, nil, cmdBar, w.input, nil)
bottom := container.NewBorder(nil, inputRow, nil, nil, bottomTabs)
bottom := container.NewBorder(nil, w.chat.Composer(), nil, nil, bottomTabs)
w.hsplit = container.NewHSplit(left, right)
@@ -170,7 +150,7 @@ func (w *Window) selectTask(id int64) {
t, err := w.store.GetTask(ctx, id)
if err != nil {
w.detail.ShowEmpty()
w.convLbl.SetText("")
w.chat.Clear()
return
}
w.detail.ShowTask(t)
@@ -182,7 +162,7 @@ func (w *Window) selectTask(id int64) {
}
var b strings.Builder
for _, h := range hist {
b.WriteString(formatRole(h.Role))
b.WriteString(ui.FormatRole(h.Role))
b.WriteString(h.Content)
b.WriteString("\n\n")
}
@@ -195,21 +175,12 @@ func (w *Window) selectTask(id int64) {
if b.Len() == 0 {
b.WriteString("Нет сообщений. /start — начать задачу.")
}
w.convLbl.SetText(b.String())
w.chat.SetTranscript(b.String())
// Состояние: сброс к снимку задач.
w.refreshState(ctx, id)
}
func formatRole(role string) string {
switch role {
case "user":
return "👤 "
default:
return "🤖 "
}
}
// refreshState — снимок «Состояния» выбранной задачи (агенты + трейсы).
func (w *Window) refreshState(ctx context.Context, id int64) {
traces, err := w.store.GetTraces(ctx, id)
@@ -242,15 +213,6 @@ func (w *Window) refreshList() {
w.taskList.Select(w.selected)
}
// appendConv добавляет строку в диалог (буфер ограничен).
func (w *Window) appendConv(text string) {
s := w.convLbl.Text + text + "\n\n"
if len(s) > bufLimit {
s = s[len(s)-bufLimit:]
}
w.convLbl.SetText(s)
}
// appendLog добавляет строку в панель «Логи» (делегирует в LogPanel).
func (w *Window) appendLog(text string) {
w.logs.Append(text)
@@ -306,7 +268,7 @@ func (w *Window) Send(_ context.Context, _ chat.Address, m chat.Message) error {
}
text = sb.String()
}
fyne.Do(func() { w.appendConv("🤖 " + text) })
fyne.Do(func() { w.chat.Append("🤖 " + text) })
return nil
}
@@ -316,7 +278,7 @@ func (w *Window) Ask(_ context.Context, _ chat.Address, m chat.Message) error {
for _, opt := range m.Options {
text += "\n" + opt.Label
}
fyne.Do(func() { w.appendConv("❓ " + text) })
fyne.Do(func() { w.chat.Append("❓ " + text) })
return nil
}
@@ -370,7 +332,7 @@ func (w *Window) OnTaskStatusChanged(e events.TaskStatusChanged) {
func (w *Window) OnHistoryAppended(e events.HistoryAppended) {
fyne.Do(func() {
if w.selected == e.TaskID {
w.appendConv(formatRole(e.Role) + e.Content)
w.chat.Append(ui.FormatRole(e.Role) + e.Content)
}
})
}