diff --git a/cmd/jean/resource_windows_amd64.syso b/cmd/jean/resource_windows_amd64.syso index e4be988..091e13d 100644 Binary files a/cmd/jean/resource_windows_amd64.syso and b/cmd/jean/resource_windows_amd64.syso differ diff --git a/cmd/jean/resource_windows_arm64.syso b/cmd/jean/resource_windows_arm64.syso index 75f2362..136c8d5 100644 Binary files a/cmd/jean/resource_windows_arm64.syso and b/cmd/jean/resource_windows_arm64.syso differ diff --git a/cmd/jean/versioninfo.json b/cmd/jean/versioninfo.json index 5b23256..72ddce7 100644 --- a/cmd/jean/versioninfo.json +++ b/cmd/jean/versioninfo.json @@ -3,13 +3,13 @@ "FileVersion": { "Major": 0, "Minor": 4, - "Patch": 7, + "Patch": 12, "Build": 0 }, "ProductVersion": { "Major": 0, "Minor": 4, - "Patch": 7, + "Patch": 12, "Build": 0 }, "FileFlagsMask": "3f", @@ -25,7 +25,7 @@ "LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.", "OriginalFilename": "jean.exe", "ProductName": "Jean", - "ProductVersion": "0.4.7", + "ProductVersion": "0.4.12", "Comments": "https://github.com/nathaninline/jean — projet open source (MIT)" }, "VarFileInfo": { diff --git a/internal/jean/llm_client.go b/internal/jean/llm_client.go index 401914e..12496f2 100644 --- a/internal/jean/llm_client.go +++ b/internal/jean/llm_client.go @@ -10,6 +10,7 @@ import ( "net/http" "sort" "strings" + "unicode/utf8" ) // Message is one entry in the chat history sent to llama.cpp. @@ -574,12 +575,21 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps break } } else { - // Inside the prompt-opened block: stream to the - // reasoning bubble until the closing , then switch - // the remainder to normal content. + // The prompt opened a block. Stream `content` LIVE as + // the answer, holding back only a short tail that could be the + // start of a literal "". A reasoning-aware backend + // (llama.cpp with --reasoning-format, Nathan's fork) strips the + // think tags server-side, so never appears in content + // and the whole answer streams straight through — including + // when the model answers WITHOUT thinking (no reasoning_content, + // no ), which is exactly what used to get dumped into + // the reasoning bubble. A vanilla build that leaves the thinking + // inline still gets carved at the below. thinkTail.WriteString(ch.Delta.Content) s := thinkTail.String() if i := strings.Index(s, thinkClose); i >= 0 { + // Vanilla inline think: reasoning before , answer + // after. Route the reasoning to its bubble, drop the tag. reason := s[:i] after := strings.TrimLeft(s[i+len(thinkClose):], "\r\n") thinkOpen = false @@ -593,13 +603,19 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps break } } else { - // Hold back a tail that could be a partial "". - keep := len(thinkClose) - 1 - if len(s) > keep { - emit := s[:len(s)-keep] + // No yet: stream as content, holding back a tail + // that could be a partial "". Back the cut up to a + // UTF-8 rune boundary so a multi-byte char (é, …) is never + // split — otherwise the two halves decode as � (mojibake). + cut := len(s) - (len(thinkClose) - 1) + for cut > 0 && !utf8.RuneStart(s[cut]) { + cut-- + } + if cut > 0 { + emit := s[:cut] thinkTail.Reset() - thinkTail.WriteString(s[len(s)-keep:]) - if !cb(StreamEvent{Reasoning: emit}) { + thinkTail.WriteString(s[cut:]) + if !cb(StreamEvent{Content: emit}) { aborted = true break } @@ -608,9 +624,9 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps } } } - // Flush any buffered reasoning if the stream ended mid-think. + // Flush the held-back tail (never part of a ): it's answer text. if !aborted && thinkOpen && thinkTail.Len() > 0 { - cb(StreamEvent{Reasoning: thinkTail.String()}) + cb(StreamEvent{Content: strings.TrimLeft(thinkTail.String(), "\r\n")}) } resp.Body.Close() if aborted { diff --git a/internal/jean/run.go b/internal/jean/run.go index 1038a6d..33a5d52 100644 --- a/internal/jean/run.go +++ b/internal/jean/run.go @@ -10,7 +10,7 @@ import ( "strings" ) -const Version = "0.4.11" +const Version = "0.4.12" // Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler). func Main() {