feat(ui): каждая вкладка chat — независимая LLM-сессия
Some checks failed
CI / test (pull_request) Failing after 1m20s
CI / build-and-package (amd64, linux) (pull_request) Failing after 1m2s
CI / build-and-package (amd64, windows) (pull_request) Successful in 36s

- ui.Session (UserID/Address/Title/BoundTask) + фабрики NewFreeSession/NewTaskSession и префикс ui://sess-*
- свободная вкладка без задачи: свой UserID, диалог с LLM в памяти (agent=chat, resume по sessionID)
- /start в свободной вкладке создаёт задачу с ChatID == UserID вкладки -> вкладка становится task-сессией
- desktop окно: container.AppTabs с вкладками, кнопка «Новый чат», маршрутизация Send/Ask/истории по Address вкладки
- app.FreeChat: in-memory связка uid -> opencode sessionID; роутинг свободного чата в handleIncoming
- агент chat.md + регистрация 'chat' в agents.Names
- тесты: session_test.go (фабрики/хелперы)
This commit is contained in:
ki.sagidullin
2026-08-21 10:33:05 +05:00
parent de21ec9f31
commit 9420b276e9
6 changed files with 356 additions and 64 deletions

43
internal/ui/session.go Normal file
View File

@@ -0,0 +1,43 @@
package ui
import (
"github.com/kamelion/ratatoskr-go/internal/chat"
"github.com/kamelion/ratatoskr-go/internal/storage"
)
// Session — одна вкладка-сессия UI: независимый диалог с LLM.
//
// Каждая вкладка имеет собственный UserID и Address, поэтому core видит её как
// отдельный чат со своим активным task (политика «одна задача на чат» работает
// из коробки). Свободная сессия (BoundTask==0) — in-memory диалог с моделью
// вне машины задач; task-сессия (BoundTask>0) — диалог конкретной задачи, у
// которой UserID == task.ChatID (чат задачи уникален).
type Session struct {
UserID chat.UserID
Address chat.Address
Title string
BoundTask int64 // 0 = свободный чат; >0 — задача, к которой привязана вкладка
}
// NewFreeSession создаёт свободную сессию (без задачи). userID/address должны
// быть уникальными (например "ui://sess-<n>"). Транскрипт живёт только в памяти.
func NewFreeSession(userID chat.UserID, address chat.Address) *Session {
return &Session{UserID: userID, Address: address, Title: "Новый чат", BoundTask: 0}
}
// NewTaskSession создаёт сессию, привязанную к задаче. UserID == task.ChatID,
// чтобы Router/core находили активную задачу этой вкладки. address — уникальный
// адрес доставки вкладки (не должен совпадать с UserID других вкладок).
func NewTaskSession(address chat.Address, t storage.Task) *Session {
title := t.Title
if title == "" {
title = "Задача #" + Itoa(t.ID)
}
return &Session{UserID: chat.UserID(t.ChatID), Address: address, Title: title, BoundTask: t.ID}
}
// SessionID — префикс идентификаторов UI-сессий (и UserID, и Address).
const SessionID = "ui://sess-"
// IsFreeSession — true, если вкладка не привязана к задаче.
func IsFreeSession(s *Session) bool { return s != nil && s.BoundTask == 0 }