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() {