Update 0.2.0
- siehe Changelog
This commit is contained in:
parent
a97013d876
commit
bd7e13385d
8 changed files with 137 additions and 30 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -7,6 +7,31 @@ Dieses Projekt ist privat und folgt keinem formalen Versionsschema
|
|||
([SemVer](https://semver.org/lang/de/)) — Versionsnummern hier dienen nur
|
||||
der zeitlichen Einordnung.
|
||||
|
||||
## [0.2.0] - 2026-07-25
|
||||
|
||||
### Behoben
|
||||
|
||||
- TUI: Markdown/Syntax-Highlighting einer Antwort erschien teils erst nach
|
||||
einem Fenster-Resize, weil nach Abschluss eines Turns (`TurnDone`) das
|
||||
Viewport nicht neu gerendert wurde.
|
||||
- TUI: Statuszeile zeigte während laufendem Tool-Output (`bash` & Co.) die
|
||||
Tool-Call-ID statt des Tool-Namens an.
|
||||
- TUI: Statuszeile überschrieb bei parallelen Tool-Calls den Namen des einen
|
||||
mit dem des anderen — zeigt jetzt alle gerade laufenden Tools an.
|
||||
- OpenAI-Adapter: Reasoning-Modelle (z.B. `o1`) senden jetzt
|
||||
`max_completion_tokens` statt `max_tokens`, wie von der API verlangt.
|
||||
|
||||
### Geändert
|
||||
|
||||
- TUI: Fließtext (Assistant/User/Thinking/...) wird auf max. 100 Spalten
|
||||
gewrappt statt auf volle Terminalbreite — bessere Lesbarkeit bei breiten
|
||||
Terminals. Tool-Output/Diffs behalten die volle Breite.
|
||||
- TUI: Tool-Call und -Ergebnis werden jetzt als ein kompakter Block
|
||||
angezeigt (Ergebnis ersetzt den wartenden Call-Eintrag) statt als zwei
|
||||
separate Blöcke im Transkript.
|
||||
- TUI: Eingabefeld-Platzhalter vereinfacht; Tastenkürzel (Enter, Alt+Enter,
|
||||
↑/↓) stehen jetzt unter `/help`.
|
||||
|
||||
## [0.1.0] - 2026-07-25
|
||||
|
||||
### Hinzugefügt
|
||||
|
|
|
|||
|
|
@ -12,10 +12,14 @@ const systemSeparator = "--- system ---\n"
|
|||
func buildRequest(req llm.Request, caps llm.Caps) wireRequest {
|
||||
wr := wireRequest{
|
||||
Model: req.Model,
|
||||
MaxTokens: req.MaxTokens,
|
||||
Temperature: req.Temp,
|
||||
Stream: true,
|
||||
}
|
||||
if caps.Reasoning {
|
||||
wr.MaxCompletionTokens = req.MaxTokens
|
||||
} else {
|
||||
wr.MaxTokens = req.MaxTokens
|
||||
}
|
||||
if caps.UsageInStream {
|
||||
wr.StreamOptions = &streamOptions{IncludeUsage: true}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,16 @@ package openai
|
|||
import "encoding/json"
|
||||
|
||||
type wireRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []wireMessage `json:"messages"`
|
||||
Tools []wireToolDef `json:"tools,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
StreamOptions *streamOptions `json:"stream_options,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Messages []wireMessage `json:"messages"`
|
||||
Tools []wireToolDef `json:"tools,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
// MaxCompletionTokens ersetzt MaxTokens für Reasoning-Modelle (o1 & Co.):
|
||||
// die OpenAI-API lehnt "max_tokens" dort ab und verlangt dieses Feld.
|
||||
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
StreamOptions *streamOptions `json:"stream_options,omitempty"`
|
||||
}
|
||||
|
||||
type streamOptions struct {
|
||||
|
|
|
|||
|
|
@ -120,7 +120,11 @@ func (m *Model) helpText() string {
|
|||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("Kommandos:\n")
|
||||
b.WriteString("Tasten:\n")
|
||||
b.WriteString(" Enter Nachricht senden\n")
|
||||
b.WriteString(" Alt+Enter während einer laufenden Antwort als Follow-up einreihen\n")
|
||||
b.WriteString(" ↑/↓ Verlauf durchblättern (bei leerem Eingabefeld)\n")
|
||||
b.WriteString("\nKommandos:\n")
|
||||
for _, c := range helpCommands {
|
||||
fmt.Fprintf(&b, " %-*s %s\n", width, c.cmd, c.desc)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,12 @@ type Model struct {
|
|||
tokensUsed int
|
||||
cacheReadTokens int // kumulierte cache_read_tokens, Nachweis für Prompt-Caching (M7)
|
||||
turnActive bool
|
||||
statusMsg string
|
||||
|
||||
// activeTools sind die gerade laufenden Tool-Calls (ID -> Name), in
|
||||
// Start-Reihenfolge. Bei parallelen Calls (mehrere Tools gleichzeitig,
|
||||
// siehe Caps.ParallelToolCalls) zeigt die Statuszeile alle Namen statt
|
||||
// nur den zuletzt gestarteten zu überschreiben.
|
||||
activeTools []toolStatus
|
||||
|
||||
modal *listModal
|
||||
|
||||
|
|
@ -85,10 +90,29 @@ type Model struct {
|
|||
// defaultInputPlaceholder ist der Standardtext des Eingabefelds. Wird
|
||||
// während einer offenen question-Tool-Rückfrage kurzzeitig überschrieben
|
||||
// (siehe askUser/answerQuestion in question.go).
|
||||
const defaultInputPlaceholder = "Nachricht eingeben — Enter sendet, Alt+Enter queued als Follow-up, /help für Kommandos"
|
||||
const defaultInputPlaceholder = "Nachricht eingeben — /help für Kommandos"
|
||||
|
||||
const askInputPlaceholder = "Antwort eingeben — Enter sendet"
|
||||
|
||||
type toolStatus struct {
|
||||
id string
|
||||
name string
|
||||
}
|
||||
|
||||
// addActiveTool/removeActiveTool pflegen m.activeTools für die Statuszeile.
|
||||
func (m *Model) addActiveTool(id, name string) {
|
||||
m.activeTools = append(m.activeTools, toolStatus{id: id, name: name})
|
||||
}
|
||||
|
||||
func (m *Model) removeActiveTool(id string) {
|
||||
for i, t := range m.activeTools {
|
||||
if t.id == id {
|
||||
m.activeTools = append(m.activeTools[:i], m.activeTools[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type uiEventMsg struct {
|
||||
gen int
|
||||
ev tool.UIEvent
|
||||
|
|
@ -174,7 +198,7 @@ func (m *Model) startSession(store *session.Store) tea.Cmd {
|
|||
m.tokensUsed = usage.InputTokens + usage.OutputTokens
|
||||
m.cacheReadTokens = usage.CacheReadTokens
|
||||
m.turnActive = false
|
||||
m.statusMsg = ""
|
||||
m.activeTools = nil
|
||||
m.followupQueue = nil
|
||||
m.liveText.Reset()
|
||||
m.liveThink.Reset()
|
||||
|
|
@ -352,8 +376,12 @@ func (m *Model) renderStatusLine() string {
|
|||
state := "idle"
|
||||
if m.turnActive {
|
||||
state = m.spinner.View() + " running"
|
||||
if m.statusMsg != "" {
|
||||
state += ": " + m.statusMsg
|
||||
if len(m.activeTools) > 0 {
|
||||
names := make([]string, len(m.activeTools))
|
||||
for i, t := range m.activeTools {
|
||||
names[i] = t.name
|
||||
}
|
||||
state += ": " + strings.Join(names, ", ")
|
||||
}
|
||||
}
|
||||
if m.permReq != nil {
|
||||
|
|
@ -409,25 +437,23 @@ func (m *Model) renderViewport() {
|
|||
if i > 0 {
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
b.WriteString(renderEntry(e))
|
||||
b.WriteString(wrapForViewport(renderEntry(e), entryWrapWidth(e.kind, m.viewport.Width)))
|
||||
}
|
||||
if m.hasLiveThink {
|
||||
if len(m.entries) > 0 {
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
b.WriteString(styleThinking.Render(m.liveThink.String()))
|
||||
b.WriteString(wrapForViewport(styleThinking.Render(m.liveThink.String()), entryWrapWidth(entryThinking, m.viewport.Width)))
|
||||
}
|
||||
if m.hasLiveText {
|
||||
if len(m.entries) > 0 || m.hasLiveThink {
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
b.WriteString(styleAssistant.Render(m.liveText.String()))
|
||||
b.WriteString(wrapForViewport(styleAssistant.Render(m.liveText.String()), entryWrapWidth(entryAssistant, m.viewport.Width)))
|
||||
}
|
||||
|
||||
content := wrapForViewport(b.String(), m.viewport.Width)
|
||||
|
||||
atBottom := m.viewport.AtBottom()
|
||||
m.viewport.SetContent(content)
|
||||
m.viewport.SetContent(b.String())
|
||||
if atBottom {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
|
|
@ -514,27 +540,40 @@ func (m *Model) handleUIEvent(ev tool.UIEvent) tea.Cmd {
|
|||
|
||||
case tool.ToolCallStart:
|
||||
m.commitLive()
|
||||
m.statusMsg = e.Name
|
||||
m.entries = append(m.entries, entry{kind: entryToolCall, text: renderToolCall(e.Name, e.Input)})
|
||||
m.addActiveTool(e.ID, e.Name)
|
||||
m.entries = append(m.entries, entry{kind: entryToolCall, id: e.ID, text: renderToolCall(e.Name, e.Input)})
|
||||
m.renderViewport()
|
||||
|
||||
case tool.ToolStream:
|
||||
// Kompaktes Live-Log für bash & Co.; nicht Zeile für Zeile ins
|
||||
// Transkript, das würde es bei langem Output unlesbar machen.
|
||||
m.statusMsg = e.ID + "…"
|
||||
// Statuszeile zeigt bereits den Tool-Namen (seit ToolCallStart).
|
||||
|
||||
case tool.ToolCallOutput:
|
||||
m.statusMsg = ""
|
||||
m.entries = append(m.entries, entry{kind: entryToolResult, text: renderToolResult(e.Name, e.Result)})
|
||||
m.removeActiveTool(e.ID)
|
||||
// Ersetzt den wartenden "→ tool"-Eintrag in-place statt einen
|
||||
// zweiten Block anzuhängen — halbiert den vertikalen Platzbedarf
|
||||
// pro Tool-Aufruf und hält die Position im Transkript stabil.
|
||||
replaced := false
|
||||
for i := len(m.entries) - 1; i >= 0; i-- {
|
||||
if m.entries[i].kind == entryToolCall && m.entries[i].id == e.ID {
|
||||
m.entries[i] = entry{kind: entryToolResult, id: e.ID, text: renderToolResult(e.Name, e.Result)}
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
m.entries = append(m.entries, entry{kind: entryToolResult, id: e.ID, text: renderToolResult(e.Name, e.Result)})
|
||||
}
|
||||
m.renderViewport()
|
||||
|
||||
case tool.TurnDone:
|
||||
m.commitLive()
|
||||
m.renderViewport()
|
||||
m.tokensUsed += e.Usage.InputTokens + e.Usage.OutputTokens
|
||||
m.cacheReadTokens += e.Usage.CacheReadTokens
|
||||
if e.Stop != llm.StopToolUse {
|
||||
m.turnActive = false
|
||||
m.statusMsg = ""
|
||||
if len(m.followupQueue) > 0 {
|
||||
next := m.followupQueue[0]
|
||||
m.followupQueue = m.followupQueue[1:]
|
||||
|
|
@ -545,7 +584,7 @@ func (m *Model) handleUIEvent(ev tool.UIEvent) tea.Cmd {
|
|||
case tool.ErrorEvent:
|
||||
m.commitLive()
|
||||
m.turnActive = false
|
||||
m.statusMsg = ""
|
||||
m.activeTools = nil
|
||||
m.entries = append(m.entries, entry{kind: entryError, text: e.Err.Error()})
|
||||
m.renderViewport()
|
||||
|
||||
|
|
|
|||
|
|
@ -164,9 +164,13 @@ func TestRunCommand_HelpListsOneCommandPerLineWithDescription(t *testing.T) {
|
|||
t.Fatalf("entries = %+v, want a single entryCommand", m.entries)
|
||||
}
|
||||
text := m.entries[0].text
|
||||
lines := strings.Split(text, "\n")
|
||||
if len(lines) != len(helpCommands)+1 { // +1 für die "Kommandos:"-Kopfzeile
|
||||
t.Fatalf("got %d lines, want %d (one per command + header):\n%s", len(lines), len(helpCommands)+1, text)
|
||||
_, cmdSection, ok := strings.Cut(text, "Kommandos:\n")
|
||||
if !ok {
|
||||
t.Fatalf("help text missing \"Kommandos:\" header:\n%s", text)
|
||||
}
|
||||
lines := strings.Split(cmdSection, "\n")
|
||||
if len(lines) != len(helpCommands) {
|
||||
t.Fatalf("got %d command lines, want %d (one per command):\n%s", len(lines), len(helpCommands), text)
|
||||
}
|
||||
for _, c := range helpCommands {
|
||||
if !strings.Contains(text, c.cmd) || !strings.Contains(text, c.desc) {
|
||||
|
|
|
|||
|
|
@ -43,9 +43,29 @@ const (
|
|||
|
||||
type entry struct {
|
||||
kind entryKind
|
||||
id string // Tool-Call-ID; verknüpft entryToolCall mit dem späteren entryToolResult
|
||||
text string // bereits gestylter, fertiger Text
|
||||
}
|
||||
|
||||
// proseMaxWidth begrenzt die Zeilenlänge von Fließtext (Assistant, User,
|
||||
// Thinking, ...) auf einen lesbaren Wert — bei breiten Terminals wird lange
|
||||
// Prosa sonst unangenehm zu lesen. Tool-Output/Diffs bekommen die volle
|
||||
// Viewport-Breite, da Tabellen/Diffs von der zusätzlichen Breite profitieren.
|
||||
const proseMaxWidth = 100
|
||||
|
||||
// entryWrapWidth liefert die Wrap-Breite für einen Entry-Typ.
|
||||
func entryWrapWidth(k entryKind, viewportWidth int) int {
|
||||
if viewportWidth <= 0 || viewportWidth > proseMaxWidth {
|
||||
switch k {
|
||||
case entryAssistant, entryUser, entryQueued, entryThinking, entrySystem, entryCommand, entryError:
|
||||
if viewportWidth > 0 {
|
||||
return proseMaxWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
return viewportWidth
|
||||
}
|
||||
|
||||
var markdownRenderer, _ = glamour.NewTermRenderer(
|
||||
glamour.WithAutoStyle(),
|
||||
glamour.WithWordWrap(0), // kein Wrap hier — Model.renderViewport bricht den gesamten Inhalt einmalig auf die aktuelle Breite um
|
||||
|
|
|
|||
8
test.py
Normal file
8
test.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
def main():
|
||||
print("Hello, World!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Reference in a new issue