Fix stream chunk truncation; style TUI initial state from state file

server.go: the stream read handler truncated each chunk to the client's
requested Count and advanced waitBase, silently dropping bytes beyond
the buffer (e.g. a chat block larger than 64KB). It now caches each
stream chunk per-fid (readCache + streamBase) and pages within it by
absolute offset, fetching the next chunk only once the current one is
fully drained, preserving 9P offset semantics.

o tui: read the state file for the initial status bar and apply the same
color styling and status text the event loop uses, so the bar is correct
immediately on attach; the event stream drives subsequent updates.
This commit is contained in:
Ollie Agent 2026-10-10 16:37:00 +02:00
parent 89fbb7438a
commit cd26595cb7
2 changed files with 59 additions and 26 deletions

View File

@ -73,6 +73,7 @@ type fid struct {
writeBuf []byte
readCache []byte // cached content for offset-based paging
waitBase string
streamBase int64 // absolute byte offset where readCache begins (stream mode)
dirCache []byte
// Event stream subscription (for event file with filter)
@ -684,31 +685,53 @@ func read(root *fs.Tree, cs *connState, fc *plan9.Fcall, ctx context.Context) *p
}
}
// Default stream handling (no filter)
// Default stream handling (no filter).
//
// A stream chunk may exceed the client's requested Count. Cache the
// chunk per-fid and page within it by absolute offset; only fetch the
// next chunk once the current one is fully drained. This preserves 9P
// offset semantics and never drops bytes beyond Count.
cs.mu.RLock()
f, fidOK := cs.fids[fc.Fid]
var base string
if fidOK {
f.mu.Lock()
base = f.waitBase
f.mu.Unlock()
}
cs.mu.RUnlock()
if !fidOK {
return errFcall(fc, "bad fid")
}
f.mu.Lock()
cached := f.readCache
streamBase := f.streamBase
f.mu.Unlock()
// Serve from the current cached chunk if the requested offset falls
// within it.
if cached != nil && int64(fc.Offset) >= streamBase && int64(fc.Offset) < streamBase+int64(len(cached)) {
local := int(int64(fc.Offset) - streamBase)
end := local + int(fc.Count)
if end > len(cached) {
end = len(cached)
}
data := cached[local:end]
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
}
// Current chunk drained (or first read): fetch the next chunk.
f.mu.Lock()
base := f.waitBase
f.mu.Unlock()
waitCtx, waitCancel := context.WithCancel(ctx)
defer waitCancel()
content, nextBase, err := entry.BlockingRead(waitCtx, base)
if err != nil {
return errFcall(fc, err.Error())
}
if nextBase != "" {
cs.mu.Lock()
if f, ok := cs.fids[fc.Fid]; ok {
f.mu.Lock()
if nextBase != "" {
f.waitBase = nextBase
}
f.readCache = content
f.streamBase = int64(fc.Offset)
f.mu.Unlock()
}
cs.mu.Unlock()
}
count := int(fc.Count)
if count > len(content) {
count = len(content)

View File

@ -524,10 +524,20 @@ cmd_tui() {
sleep 0.2
tmux send-keys -t "$tmux_session" "o '$ctx' prompt" Enter
# Get initial state
local initial_state
# Get initial state directly from the state file, then style it the same
# way the event loop below does, so the status bar is correct immediately.
local initial_state initial_status initial_bg
initial_state=$(ollie-9p read "session/$s/agent/$a/state" 2>/dev/null || echo "unknown")
tmux set-option -t "$tmux_session" status-right "$initial_state | %H:%M"
case "$initial_state" in
idle) initial_bg="colour70" ;;
calling*) initial_bg="colour214" ;;
thinking) initial_bg="colour33" ;;
paused) initial_bg="colour245" ;;
*) initial_bg="default" ;;
esac
initial_status=$(ollie-9p read "${PREFIX}/status" 2>/dev/null | head -n1)
tmux set-option -t "$tmux_session" status-right-style "fg=default,bg=$initial_bg"
tmux set-option -t "$tmux_session" status-right "${initial_status:-$initial_state} | %H:%M"
tmux set-option -t "$tmux_session" status-right-length 64
# Get session and agent IDs for event subscription