feat: Phase 2 — chat templates, reasoning parser, collapsible TUI, mouse support
- internal/llama/template.go: ChatML/Llama3 template engine with stop tokens - internal/agent/reasoning.go: parse <think>/<reasoning> tags from LLM output - companion/promptbuilder.go: output structured BuildResult, add date/time context - agent/agent.go: wire templates + reasoning split into message loop - storage: add reasoning TEXT column to messages table + migration - channel/tui.go: collapsible reasoning (tab toggle), mouse scroll, polished dimmed style - config: CHAT_TEMPLATE env var (chatml|llama3|none) - plan.md: mark Phase 2 complete, add themes TODO
This commit is contained in:
@@ -23,6 +23,22 @@ var (
|
||||
Foreground(lipgloss.Color("#10B981")).
|
||||
Bold(true)
|
||||
|
||||
reasoningLabelStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#6B7280")).
|
||||
Background(lipgloss.Color("#1F2937")).
|
||||
Padding(0, 1).
|
||||
Italic(true)
|
||||
|
||||
reasoningContentStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#6B7280")).
|
||||
Italic(true)
|
||||
|
||||
reasoningHeaderStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#6B7280")).
|
||||
Background(lipgloss.Color("#1F2937")).
|
||||
Padding(0, 1).
|
||||
Bold(true)
|
||||
|
||||
inputStyle = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("#374151"))
|
||||
@@ -40,20 +56,23 @@ type model struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
viewport viewport.Model
|
||||
input string
|
||||
state sessionState
|
||||
messages []types.Message
|
||||
ready bool
|
||||
viewport viewport.Model
|
||||
input string
|
||||
state sessionState
|
||||
messages []types.Message
|
||||
reasoningExpanded map[string]bool
|
||||
reasoningOrder []string
|
||||
ready bool
|
||||
}
|
||||
|
||||
func NewTUIModel(a *agent.Agent) tea.Model {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &model{
|
||||
agent: a,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
state: stateReady,
|
||||
agent: a,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
state: stateReady,
|
||||
reasoningExpanded: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +93,11 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case tea.MouseMsg:
|
||||
var cmd tea.Cmd
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
return m, cmd
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "ctrl+q":
|
||||
@@ -88,6 +112,15 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, m.sendMessage(input)
|
||||
}
|
||||
|
||||
case "tab":
|
||||
for i := len(m.messages) - 1; i >= 0; i-- {
|
||||
if m.messages[i].Role == types.RoleAssistant && m.messages[i].Reasoning != "" {
|
||||
id := m.messages[i].ID
|
||||
m.reasoningExpanded[id] = !m.reasoningExpanded[id]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case "backspace":
|
||||
if len(m.input) > 0 {
|
||||
m.input = m.input[:len(m.input)-1]
|
||||
@@ -107,7 +140,22 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
Content: fmt.Sprintf("error: %v", msg.err),
|
||||
})
|
||||
} else {
|
||||
m.messages = m.agent.Messages()
|
||||
newMsgs := m.agent.Messages()
|
||||
for _, msg := range newMsgs {
|
||||
if msg.Role == types.RoleAssistant && msg.Reasoning != "" {
|
||||
found := false
|
||||
for _, id := range m.reasoningOrder {
|
||||
if id == msg.ID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
m.reasoningOrder = append(m.reasoningOrder, msg.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
m.messages = newMsgs
|
||||
}
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
@@ -134,14 +182,26 @@ func (m *model) View() string {
|
||||
default:
|
||||
prefix = "system"
|
||||
}
|
||||
b.WriteString(style.Render(prefix+": ") + msg.Content + "\n\n")
|
||||
b.WriteString(style.Render(prefix+": ") + msg.Content + "\n")
|
||||
|
||||
if msg.Role == types.RoleAssistant && msg.Reasoning != "" {
|
||||
if m.reasoningExpanded[msg.ID] {
|
||||
b.WriteString(reasoningHeaderStyle.Render(" reasoning ") + "\n")
|
||||
b.WriteString(reasoningContentStyle.Render(msg.Reasoning) + "\n")
|
||||
} else {
|
||||
label := reasoningLabelStyle.Render(" ` reasoning (tab)")
|
||||
b.WriteString(label + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
m.viewport.SetContent(b.String())
|
||||
|
||||
prompt := "> "
|
||||
if m.state == stateWaiting {
|
||||
prompt = "waiting... "
|
||||
prompt = "... "
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s\n%s%s", m.viewport.View(), inputStyle.Render(prompt), m.input)
|
||||
|
||||
Reference in New Issue
Block a user