From 7eb31f0f5ab23db8953c2afd08e3e45f9e433bcf Mon Sep 17 00:00:00 2001 From: onevcat Date: Thu, 4 Jun 2026 23:57:28 +0900 Subject: [PATCH] Fix prowl read `truncated` to mean "may be incomplete", not "fewer than requested" `prowl read --last N` set `truncated: true` whenever it returned fewer lines than requested, even when the full screen+scrollback buffer was retrievable and simply held less history than N. That is the opposite of useful: the response actually contained everything the pane retains, yet the flag implied data loss and invited needless retries. Reserve `truncated` for genuine incompleteness: it is now true only on the viewport-only fallbacks, where the full buffer could not be read and the visible viewport had fewer lines than requested, so older content may exist beyond reach. When the full buffer is available, the result is complete regardless of how it compares to N -> `truncated: false`. This also aligns read's `truncated` with `send --capture`'s, where true already means "captured text may be missing content". Updates the read contract doc and the handler test that pinned the old behavior. --- doc-onevcat/contracts/cli/read.md | 10 +++++++++- supacode/CLIService/ReadCommandHandler.swift | 13 ++++++++++++- supacodeTests/CLIReadCommandHandlerTests.swift | 7 +++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/doc-onevcat/contracts/cli/read.md b/doc-onevcat/contracts/cli/read.md index 2324de95..0832a86a 100644 --- a/doc-onevcat/contracts/cli/read.md +++ b/doc-onevcat/contracts/cli/read.md @@ -101,7 +101,15 @@ This file defines the **JSON output contract** for: - `"scrollback"`: satisfied from scrollback/history - `"mixed"`: combined view when the runtime had to stitch sources together - `truncated`: boolean - - `true` when the runtime could not return the full requested text volume + - `true` only when the returned `text` may be **incomplete** — i.e. the runtime could not + retrieve the pane's full screen+scrollback buffer and the visible viewport alone held fewer + lines than requested, so older content may exist beyond reach. + - `false` when `text` holds every line the pane retains for the request. In particular, + receiving fewer lines than `--last N` because the pane simply has less history than `N` is + **not** truncation — you still got everything available. + - This matches the meaning of `truncated` in the `send --capture` response (`prowl.cli.send`): + in both, `true` signals "the captured text may be missing content", never "you got less than + you asked for but it is all there is". - `line_count`: integer - number of newline-delimited lines present in `text` - `text`: string diff --git a/supacode/CLIService/ReadCommandHandler.swift b/supacode/CLIService/ReadCommandHandler.swift index 6ed91363..3fdc7d8a 100644 --- a/supacode/CLIService/ReadCommandHandler.swift +++ b/supacode/CLIService/ReadCommandHandler.swift @@ -227,6 +227,9 @@ final class ReadCommandHandler: CommandHandler { } guard let screenText else { + // The full screen+scrollback buffer could not be read, so we only have the visible + // viewport. If it holds fewer lines than requested, older history may exist beyond our + // reach — the result may be incomplete, which is exactly what `truncated` should signal. return ReadCapture( text: joinLines(viewportLines.suffix(min(requestedLineCount, viewportLines.count))), source: .mixed, @@ -236,6 +239,9 @@ final class ReadCommandHandler: CommandHandler { let screenLines = splitLines(screenText) if screenLines.count < viewportLines.count { + // The full-buffer read returned fewer lines than the viewport itself — an unreliable + // capture. We fall back to the viewport and, as above, cannot vouch for completeness + // when it is shorter than requested. return ReadCapture( text: joinLines(viewportLines.suffix(min(requestedLineCount, viewportLines.count))), source: .mixed, @@ -246,10 +252,15 @@ final class ReadCommandHandler: CommandHandler { let source: ReadSource = screenLines.count > viewportLines.count ? .scrollback : .screen let text = joinLines(screenLines.suffix(min(requestedLineCount, screenLines.count))) + // The full screen+scrollback buffer was retrievable, so `text` already holds every line + // the pane retains. Returning fewer lines than `--last` requested here only means the + // pane has less history than asked for — not that content was lost — so it is complete, + // not truncated. `truncated` stays reserved for cases where content may be unreachable + // (see the viewport-only fallbacks above). return ReadCapture( text: text, source: source, - truncated: screenLines.count < requestedLineCount + truncated: false ) } diff --git a/supacodeTests/CLIReadCommandHandlerTests.swift b/supacodeTests/CLIReadCommandHandlerTests.swift index aa53089b..431ceeed 100644 --- a/supacodeTests/CLIReadCommandHandlerTests.swift +++ b/supacodeTests/CLIReadCommandHandlerTests.swift @@ -124,7 +124,7 @@ struct CLIReadCommandHandlerTests { #expect(payload.text == "b\nc\nd") } - @Test func lastMarksTruncatedWhenScreenInsufficient() async throws { + @Test func lastReturnsFullBufferWithoutTruncationWhenHistoryShorterThanRequest() async throws { let handler = ReadCommandHandler( resolveProvider: { _ in .success(Self.makeTarget()) }, captureProvider: { _ in @@ -140,7 +140,10 @@ struct CLIReadCommandHandlerTests { #expect(response.ok) let payload = try #require(try response.data?.decode(as: ReadCommandPayload.self)) #expect(payload.source == .scrollback) - #expect(payload.truncated == true) + // The full screen+scrollback buffer was retrievable and holds only 4 lines, so the + // response contains everything the pane retains. Fewer lines than `--last 10` requested + // does not mean content was lost, so it must not be flagged truncated. + #expect(payload.truncated == false) #expect(payload.lineCount == 4) #expect(payload.text == "one\ntwo\nthree\nfour") } -- 2.51.2