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") }