From 255d66a6d6f014d1a1f9483c46eddd33e0911961 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Fri, 11 Sep 2026 14:46:53 -0400 Subject: [PATCH 1/3] chat: a landed card quotes the report's first prose line, not its literal first line A report that opens with a code fence is the model spelling its answer correctly; the card reading it literally quoted the fence marker itself and spent the one row that carries the work's own sentence on punctuation. firstProseLine skips leading blanks and fence markers and quotes the first line that says something; a fence-only report quotes its first line inside, and a report with no prose keeps the emptiness law it already had. Co-Authored-By: aforge --- .../889-done-card-first-prose-line.md | 26 ++++++ internal/manual/chat/reading-a-task-page.md | 6 ++ internal/manual/chat/tasks.md | 7 ++ internal/tui3/doneoutcomeline_test.go | 85 +++++++++++++++++++ internal/tui3/render.go | 65 ++++++++++++++ internal/tui3/taskdone.go | 7 +- 6 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 docs/changes/unreleased/889-done-card-first-prose-line.md create mode 100644 internal/tui3/doneoutcomeline_test.go diff --git a/docs/changes/unreleased/889-done-card-first-prose-line.md b/docs/changes/unreleased/889-done-card-first-prose-line.md new file mode 100644 index 0000000000..1197c6b3e6 --- /dev/null +++ b/docs/changes/unreleased/889-done-card-first-prose-line.md @@ -0,0 +1,26 @@ +--- +kind: fixed +title: a task card quotes the report's first line that says something, not its literal first line +pr: 889 +surface: [chat] +invalidates: + - "The landed card's quoted sentence was the report's literal first line (`firstLine(node.report)` in `internal/tui3/taskdone.go`), so a report that opened with a code fence drew \"```\" as its outcome. It is the first line that says something now: leading blank lines and fence markers — ``` and ~~~, with or without a language word — are skipped, a report that is nothing but a fenced block quotes the first line inside it, and a report with no prose at all draws the stamp alone rather than empty quotation marks. `internal/tui3`'s `firstProseLine` is the one reader, and every road that fills a card's quoted half goes through it." + - "The card's row was believed to quote `session.TaskNotice.Report` verbatim from its first byte. It never did anything so simple: the quote is a reading of the report, and the reading now knows what a fence is." +--- + +A quick task landed on 2026-09-11 (`dev@333acc67d`) whose report opened with a +fence around its answer, and the card drew `"```" · started 10:45 · ctrl+o +output` — the one row that exists to carry the work's own sentence, spent on the +punctuation around it. + +The reports are the model's own markdown and a fence in front of an answer is +not a defect in them: anything that reports a diff, a command's output or a JSON +result is *correctly* fenced. The defect was the surface reading the first line +literally, which is a reading that only works for prose. + +The quoted half is now the first line of the report that says something — +`firstProseLine` in `internal/tui3/render.go`, sitting beside the `mdFenceOpen` +it borrows from, so the two agree on what a fence is. A report with no prose at +all keeps the emptiness law it always had: the row falls back to the subtitle +and, failing that, draws the start stamp alone rather than an empty pair of +quotation marks. diff --git a/internal/manual/chat/reading-a-task-page.md b/internal/manual/chat/reading-a-task-page.md index c14f1fb7d5..3b94707362 100644 --- a/internal/manual/chat/reading-a-task-page.md +++ b/internal/manual/chat/reading-a-task-page.md @@ -36,6 +36,12 @@ Nothing here is thrown away — what is folded is one keypress from open. `started 14:02` only when the record carries the instant the work began. Reopening the conversation does not replace that instant with the time you sat down. +**The quoted sentence above the stamp is the report's first line that says something.** A +task that answers with a diff, a command's output or a JSON block opens its report with the +fence around that answer, and the card quotes the first line of prose under the fence — +never the fence marker itself, which says nothing. A report that is all fence quotes its +first line inside; a report with no prose at all draws the stamp with no quotation marks. + An older record may carry a duration but no start or landing instant. When that duration is at least one second, the settled task page shows it in the header — for example `12m` — while the completion card omits the entire `started 14:02` segment. A shorter or absent diff --git a/internal/manual/chat/tasks.md b/internal/manual/chat/tasks.md index b9921830f7..67423309a7 100644 --- a/internal/manual/chat/tasks.md +++ b/internal/manual/chat/tasks.md @@ -1776,6 +1776,13 @@ the task's row on the roster. The head is what happened. The muted line under it is what came of it, in the task's own first sentence, quoted because they are its words and not aforge's. +**The quoted sentence is the first line of the report that says something.** Work that ran +a command, produced a diff or answered in JSON often opens its report with the code fence +around that — the fence is how the answer is spelled, not a sentence — and the card quotes +the first line of prose beneath it rather than the fence marker. A report that is nothing +but a fenced block quotes the first line inside the fence. A report with no prose to quote +draws no quotation marks at all, only the start stamp. + **Every fact on the head is joined by ` · `, the state word included.** It used to read `done 4m12s`, with the state and the clock fused into one phrase while `3 files` beside them was properly separated — so on a card asking for a hand, the word and the clock ran diff --git a/internal/tui3/doneoutcomeline_test.go b/internal/tui3/doneoutcomeline_test.go new file mode 100644 index 0000000000..8f32437106 --- /dev/null +++ b/internal/tui3/doneoutcomeline_test.go @@ -0,0 +1,85 @@ +package tui3 + +// ── THE QUOTED HALF OF A LANDED CARD ──────────────────────────────────────── +// +// The card's second row quotes what the work came to, and the report it quotes +// is the model's own markdown. A model that opens with a fenced block — a diff, +// a command's output, a JSON result — spends that row on the fence marker itself +// unless the reading skips it, and `"```"` is the row saying nothing at all +// (#889). These pin every road the reading can take. + +import ( + "strings" + "testing" + "time" +) + +// THE FIRST PROSE LINE, OVER THE TABLE. A leading fence is skipped whether a +// language word follows it or not, blank lines are skipped, a report that is +// nothing but a fence quotes the first line inside it, and a report with no +// prose at all returns nothing — which is the emptiness law's case, not this +// reader's. +func TestFirstProseLineSkipsFencesBlankLinesAndWrappers(t *testing.T) { + for _, tc := range []struct { + name string + report string + want string + }{ + {"a report that opens with prose", "the seven files are written and the suite passes", "the seven files are written and the suite passes"}, + {"a leading fence is skipped", "```\nthe seven files are written and the suite passes\n```", "the seven files are written and the suite passes"}, + {"a fence's language word is skipped too", "```go\npackage main\n```\nthe suite passes", "package main"}, + {"a tilde fence is skipped too", "~~~\nthe suite passes\n~~~", "the suite passes"}, + {"a leading blank line is skipped", "\n\nthe suite passes", "the suite passes"}, + {"blank lines inside a fence are skipped", "```\n\nthe suite passes\n```", "the suite passes"}, + {"a report that is nothing but a fence quotes its first line", "```\n7 files changed, 15 insertions(+)\n```", "7 files changed, 15 insertions(+)"}, + {"a closed fence with nothing inside falls on", "```\n```\nthe suite passes", "the suite passes"}, + {"an unclosed fence with nothing yet inside falls on", "```go\n\nthe suite passes", "the suite passes"}, + {"an indented fence marker is still a fence", " ```\nthe suite passes", "the suite passes"}, + {"an empty report says nothing", "", ""}, + {"a report of blank lines says nothing", "\n \n", ""}, + {"a report of fence markers alone says nothing", "```\n", ""}, + } { + if got := firstProseLine(tc.report); got != tc.want { + t.Errorf("%s: firstProseLine(%q) = %q, want %q", tc.name, tc.report, got, tc.want) + } + } +} + +// A DONE CARD SKIPS A FENCE AND QUOTES THE FIRST PROSE LINE, which is #889 +// whole: a quick task lands with a fenced block in front of its answer and the +// row is spent on the answer rather than on the punctuation around it. +func TestADoneCardSkipsAFenceAndQuotesTheFirstProseLine(t *testing.T) { + a, _, _ := taskApp(t) + began := a.now().Add(-10 * time.Minute) + card := &taskDone{ + title: "Fix nil-map crash", + outcome: firstProseLine("```\nthe seven files are written and the suite passes\n```"), + started: began, + } + under := plain(a.doneUnder(card, 120)) + if !strings.Contains(under, `"the seven files are written and the suite passes"`) { + t.Fatalf("the card's second row reads\n\t%q\nand it should quote the first prose line, not the fence:\n\t%q", under, `"the seven files are written and the suite passes"`) + } + if strings.Contains(under, "```") { + t.Fatalf("the card's second row still quotes a fence marker:\n\t%q", under) + } + if !strings.Contains(under, "started "+began.Format("15:04")) { + t.Fatalf("the card's second row is missing its start stamp:\n\t%q", under) + } +} + +// A DONE CARD THAT LANDS WITH NO PROSE TO QUOTE DRAWS THE TAIL ALONE. The +// emptiness law owns this case — no empty pair of quotation marks claiming the +// work said something, and no fence marker standing in for a sentence either. +func TestADoneCardWithNoProseDrawsNoEmptyQuotes(t *testing.T) { + a, _, _ := taskApp(t) + began := a.now().Add(-10 * time.Minute) + card := &taskDone{title: "Fix nil-map crash", outcome: firstProseLine("```\n"), started: began} + under := plain(a.doneUnder(card, 120)) + if strings.Contains(under, `""`) { + t.Fatalf("a card with nothing to quote drew an empty pair of quotes:\n\t%q", under) + } + if !strings.Contains(under, "started "+began.Format("15:04")) { + t.Fatalf("the card's second row lost its start stamp:\n\t%q", under) + } +} diff --git a/internal/tui3/render.go b/internal/tui3/render.go index dd2fb03dff..dae8c5ad06 100644 --- a/internal/tui3/render.go +++ b/internal/tui3/render.go @@ -4321,3 +4321,68 @@ func firstLine(s string) string { } return s } + +// firstProseLine is the first line of a report that says something, and it is +// what every quoted half of a card is built from. +// +// A REPORT IS THE MODEL'S OWN MARKDOWN, and a model that has just run a command +// or produced a diff opens with the fence around it rather than with a sentence +// — the fence is not a defect in the report, it is how the answer is spelled. +// The literal first line of such a report is ``` or ~~~, which is a line of +// punctuation and says nothing at all; quoting it spends the one row the card +// exists to draw on the wrapper around the answer. So leading blank lines and +// fence markers are skipped — with or without a language word after them, the +// way [mdFenceOpen] reads them for rendering (````go`, `~~~sh`) — and the first +// line of prose beneath them is what is quoted. +// +// A REPORT THAT IS NOTHING BUT A FENCED BLOCK QUOTES ITS FIRST LINE INSIDE. The +// work's answer is then the block's own contents, and the first line of it is +// the closest thing to a sentence the report carries — a card that skipped the +// whole fence would quote nothing, which is the emptiness law doing the opposite +// of its job: there was something to say and the row went silent. +// +// AND A REPORT WITH NO PROSE AT ALL RETURNS NOTHING, which hands the row to the +// emptiness law where it belongs: the card falls back to its subtitle and, if +// that is gone too, draws the start stamp alone rather than an empty pair of +// quotation marks claiming the work said something ([app.doneUnder] does that +// already, and nothing here has to learn it twice). +func firstProseLine(s string) string { + lines := strings.Split(s, "\n") + for i, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + continue + } + // A fence marker is skipped, and everything after it down to its closing + // fence is prose from the moment one line of it says something — the + // block IS the answer for a report that never leaves it. + if delim, _, ok := mdFenceOpen(trimmed); ok { + if inner := firstProseInFence(lines[i+1:], delim); inner != "" { + return inner + } + // AND AN UNCLOSED FENCE IS A FENCE STILL BEING WRITTEN. Nothing inside + // one yet has said anything, so the reading keeps going below it rather + // than treating the rest of the report as inside a block it cannot see + // the end of. + continue + } + return trimmed + } + return "" +} + +// firstProseInFence is the first line inside a fenced block that says +// something, or "" when the block is empty. It is [firstProseLine]'s case for +// a report that is nothing but a fence, and it stops at the closing marker +// because what follows the block is prose the caller has not reached yet. +func firstProseInFence(lines []string, delim string) string { + for _, line := range lines { + if mdFenceClose(line, delim) { + return "" + } + if trimmed := strings.TrimSpace(line); trimmed != "" { + return trimmed + } + } + return "" +} diff --git a/internal/tui3/taskdone.go b/internal/tui3/taskdone.go index 0db983a8d5..a6c2edeaa5 100644 --- a/internal/tui3/taskdone.go +++ b/internal/tui3/taskdone.go @@ -231,7 +231,7 @@ func (a *app) landedCard(node *taskNode) { span: node.elapsed, started: node.spawnedAt(), landed: landed, - outcome: strings.TrimSpace(firstLine(node.report)), + outcome: firstProseLine(node.report), report: strings.TrimSpace(node.report), result: strings.TrimSpace(node.produced), resultWhole: strings.TrimSpace(node.producedWhole), @@ -762,8 +762,9 @@ func (card *taskDone) hasDetail() bool { // account to draw. This is every ordinary two-line landing, and it is // byte-for-byte what this card drew before the split existed. // - It did not: the report LEADS with the sentence saying so (the engine -// composes it that way, and [taskDone.outcome] is already that first line), -// so that sentence is the account and the rest of the report is the answer. +// composes it that way, and [taskDone.outcome] is already that first line +// that says something), so that sentence is the account and the rest of the +// report is the answer. // // Nothing here reads the wording of a report. The question asked is the card's // own settled state, which is the same rule internal/session's From fbce6ca2918f4a88010e00cffc10922726a56eb2 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Fri, 11 Sep 2026 16:09:09 -0400 Subject: [PATCH 2/3] chat: the reading is one law, and the manual page a person can find it on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first-prose-line reader arrived as two functions and a fallthrough — a walk that stepped into a fenced block, a second walk inside it, and a comment about a fence still being written. Every answer it gave is the answer one sentence gives: a blank line says nothing, a fence marker says nothing, and the first line that is neither is what the work came to. So it is one loop now, and [mdFenceOpen] is still the only opinion the surface has about what a fence is. The manual page moved with it. The rule was a paragraph inside the section about when a task started, which is not a heading anybody asking "why does my card show three backticks" would ever reach — headings are the search index, and a page that is correct and unreachable is a page the chat talks over. It has its own heading now, in the asker's own words, with the card it draws and the row it used to draw. Co-Authored-By: Claude Opus 5 (1M context) --- .../889-done-card-first-prose-line.md | 12 ++-- internal/manual/chat/reading-a-task-page.md | 31 +++++++-- internal/manual/chat/tasks.md | 14 ++-- internal/tui3/doneoutcomeline_test.go | 1 + internal/tui3/render.go | 67 +++++++------------ 5 files changed, 64 insertions(+), 61 deletions(-) diff --git a/docs/changes/unreleased/889-done-card-first-prose-line.md b/docs/changes/unreleased/889-done-card-first-prose-line.md index 1197c6b3e6..d9ef144657 100644 --- a/docs/changes/unreleased/889-done-card-first-prose-line.md +++ b/docs/changes/unreleased/889-done-card-first-prose-line.md @@ -19,8 +19,10 @@ result is *correctly* fenced. The defect was the surface reading the first line literally, which is a reading that only works for prose. The quoted half is now the first line of the report that says something — -`firstProseLine` in `internal/tui3/render.go`, sitting beside the `mdFenceOpen` -it borrows from, so the two agree on what a fence is. A report with no prose at -all keeps the emptiness law it always had: the row falls back to the subtitle -and, failing that, draws the start stamp alone rather than an empty pair of -quotation marks. +`firstProseLine` in `internal/tui3/render.go`, which passes over a blank line and +a fence marker alike and returns the first line that is neither. It reads a +marker through `mdFenceOpen`, the same door the renderer reads one through, so +the surface has one opinion about what a fence is rather than two. A report with +no line to quote keeps the emptiness law it always had: the row falls back to the +subtitle and, failing that, draws the start stamp alone rather than an empty pair +of quotation marks. diff --git a/internal/manual/chat/reading-a-task-page.md b/internal/manual/chat/reading-a-task-page.md index 3b94707362..23a4b35535 100644 --- a/internal/manual/chat/reading-a-task-page.md +++ b/internal/manual/chat/reading-a-task-page.md @@ -36,18 +36,37 @@ Nothing here is thrown away — what is folded is one keypress from open. `started 14:02` only when the record carries the instant the work began. Reopening the conversation does not replace that instant with the time you sat down. -**The quoted sentence above the stamp is the report's first line that says something.** A -task that answers with a diff, a command's output or a JSON block opens its report with the -fence around that answer, and the card quotes the first line of prose under the fence — -never the fence marker itself, which says nothing. A report that is all fence quotes its -first line inside; a report with no prose at all draws the stamp with no quotation marks. - An older record may carry a duration but no start or landing instant. When that duration is at least one second, the settled task page shows it in the header — for example `12m` — while the completion card omits the entire `started 14:02` segment. A shorter or absent duration leaves the header figure out too. The duration is never used to invent a wall-clock time. +## The card quotes three backticks instead of a sentence — a code fence at the top of a task's report + +**The quoted line on a landed card is the report's first line that says something**, not its +literal first line. A task that answers with a diff, a command's output or a JSON block +opens its report with the code fence around that answer, and a fence marker — three +backticks or three tildes, with or without a language word like `go` or `diff` after it — is +punctuation rather than a sentence. Blank lines and fence markers are passed over, and the +first line that is neither is what the card quotes. + +So a report that is nothing but a fenced block quotes the first line inside the block, which +is the answer itself: + +``` +✓ ◆ Fix nil-map crash · done · 4m12s · 3 files + "the guard is in and the regression test passes" · started 14:02 · ctrl+o output +``` + +A report with no line to quote at all draws no quotation marks — an empty pair would be the +card claiming the work said something. The card falls back to its subtitle and, failing +that, to `started 14:02` on its own. + +Until 2026-09-11 the card took the report's first line literally, so work that answered +inside a fence drew a quoted half of three backticks and nothing else — the one line the +card exists for spent on the punctuation around the answer. + ## Why most of the work is hidden on a task page — the `▸ worked` chips, the caption outline, and `ctrl+e` **This is the answer to "my task page is hiding most of the work", "where did the tool diff --git a/internal/manual/chat/tasks.md b/internal/manual/chat/tasks.md index 67423309a7..91d2903c36 100644 --- a/internal/manual/chat/tasks.md +++ b/internal/manual/chat/tasks.md @@ -1776,12 +1776,14 @@ the task's row on the roster. The head is what happened. The muted line under it is what came of it, in the task's own first sentence, quoted because they are its words and not aforge's. -**The quoted sentence is the first line of the report that says something.** Work that ran -a command, produced a diff or answered in JSON often opens its report with the code fence -around that — the fence is how the answer is spelled, not a sentence — and the card quotes -the first line of prose beneath it rather than the fence marker. A report that is nothing -but a fenced block quotes the first line inside the fence. A report with no prose to quote -draws no quotation marks at all, only the start stamp. +**The quoted sentence is the first line of the report that says something**, not its +literal first line. Work that ran a command, produced a diff or answered in JSON often +opens its report with the code fence around that — the fence is how the answer is spelled, +not a sentence — so a fence marker, three backticks or three tildes with or without a +language word after it, is passed over the way a blank line is. The first line that is +neither is what the card quotes, which for a report that is nothing but a fenced block is +the first line inside it. A report with no line to quote draws no quotation marks at all, +only the start stamp. **Every fact on the head is joined by ` · `, the state word included.** It used to read `done 4m12s`, with the state and the clock fused into one phrase while `3 files` beside diff --git a/internal/tui3/doneoutcomeline_test.go b/internal/tui3/doneoutcomeline_test.go index 8f32437106..fe1fd4be59 100644 --- a/internal/tui3/doneoutcomeline_test.go +++ b/internal/tui3/doneoutcomeline_test.go @@ -35,6 +35,7 @@ func TestFirstProseLineSkipsFencesBlankLinesAndWrappers(t *testing.T) { {"a closed fence with nothing inside falls on", "```\n```\nthe suite passes", "the suite passes"}, {"an unclosed fence with nothing yet inside falls on", "```go\n\nthe suite passes", "the suite passes"}, {"an indented fence marker is still a fence", " ```\nthe suite passes", "the suite passes"}, + {"a marker is skipped wherever it stands, not only in front", "```\n```\n```diff\n- old\n+ new\n```", "- old"}, {"an empty report says nothing", "", ""}, {"a report of blank lines says nothing", "\n \n", ""}, {"a report of fence markers alone says nothing", "```\n", ""}, diff --git a/internal/tui3/render.go b/internal/tui3/render.go index dae8c5ad06..44f5baae54 100644 --- a/internal/tui3/render.go +++ b/internal/tui3/render.go @@ -4323,66 +4323,45 @@ func firstLine(s string) string { } // firstProseLine is the first line of a report that says something, and it is -// what every quoted half of a card is built from. +// what the quoted half of a card is built from. // // A REPORT IS THE MODEL'S OWN MARKDOWN, and a model that has just run a command // or produced a diff opens with the fence around it rather than with a sentence // — the fence is not a defect in the report, it is how the answer is spelled. -// The literal first line of such a report is ``` or ~~~, which is a line of -// punctuation and says nothing at all; quoting it spends the one row the card -// exists to draw on the wrapper around the answer. So leading blank lines and -// fence markers are skipped — with or without a language word after them, the -// way [mdFenceOpen] reads them for rendering (````go`, `~~~sh`) — and the first -// line of prose beneath them is what is quoted. -// -// A REPORT THAT IS NOTHING BUT A FENCED BLOCK QUOTES ITS FIRST LINE INSIDE. The -// work's answer is then the block's own contents, and the first line of it is -// the closest thing to a sentence the report carries — a card that skipped the -// whole fence would quote nothing, which is the emptiness law doing the opposite -// of its job: there was something to say and the row went silent. +// The literal first line of such a report is ``` or ~~~, which is a row of +// punctuation and says nothing at all; quoting it spends the one line the card +// exists to draw on the wrapper around the answer. +// +// SO THE WHOLE LAW IS ONE SENTENCE: a blank line says nothing, a fence marker +// says nothing, and the first line that is neither is what the work came to. +// The markers are read by [mdFenceOpen], the same door the renderer reads them +// through, so a marker with a language word after it — go, sh, diff, json — +// is a marker here for exactly the reason it is one there: a line this surface +// would DRAW as prose is quoted as prose, and there is no second opinion about +// what a fence is. +// +// THAT MEANS A REPORT THAT IS NOTHING BUT A FENCED BLOCK QUOTES THE FIRST LINE +// INSIDE IT, which is the point rather than an exception to it: the work's +// answer IS the block's contents, and a reading that stepped over the whole +// block to look for prose underneath would quote nothing at all — the emptiness +// law doing the opposite of its job, going silent where there was something to +// say. // // AND A REPORT WITH NO PROSE AT ALL RETURNS NOTHING, which hands the row to the -// emptiness law where it belongs: the card falls back to its subtitle and, if -// that is gone too, draws the start stamp alone rather than an empty pair of +// emptiness law where it does belong: the card falls back to its subtitle and, +// failing that, draws the start stamp alone rather than an empty pair of // quotation marks claiming the work said something ([app.doneUnder] does that // already, and nothing here has to learn it twice). func firstProseLine(s string) string { - lines := strings.Split(s, "\n") - for i, line := range lines { + for _, line := range strings.Split(s, "\n") { trimmed := strings.TrimSpace(line) if trimmed == "" { continue } - // A fence marker is skipped, and everything after it down to its closing - // fence is prose from the moment one line of it says something — the - // block IS the answer for a report that never leaves it. - if delim, _, ok := mdFenceOpen(trimmed); ok { - if inner := firstProseInFence(lines[i+1:], delim); inner != "" { - return inner - } - // AND AN UNCLOSED FENCE IS A FENCE STILL BEING WRITTEN. Nothing inside - // one yet has said anything, so the reading keeps going below it rather - // than treating the rest of the report as inside a block it cannot see - // the end of. + if _, _, fence := mdFenceOpen(trimmed); fence { continue } return trimmed } return "" } - -// firstProseInFence is the first line inside a fenced block that says -// something, or "" when the block is empty. It is [firstProseLine]'s case for -// a report that is nothing but a fence, and it stops at the closing marker -// because what follows the block is prose the caller has not reached yet. -func firstProseInFence(lines []string, delim string) string { - for _, line := range lines { - if mdFenceClose(line, delim) { - return "" - } - if trimmed := strings.TrimSpace(line); trimmed != "" { - return trimmed - } - } - return "" -} From 8b7eb7c501f46d088e1d02ce53cc9c8e220ee712 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Fri, 11 Sep 2026 16:10:42 -0400 Subject: [PATCH 3/3] changelog: the entry carries its pull request's number, not the issue's The lane numbered its entry 889, which is the issue. The number in the filename and in `pr:` is what the released changelog links, so an entry filed under an issue number points a reader at the report rather than at the change. Co-Authored-By: Claude Opus 5 (1M context) --- ...rd-first-prose-line.md => 930-done-card-first-prose-line.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/changes/unreleased/{889-done-card-first-prose-line.md => 930-done-card-first-prose-line.md} (99%) diff --git a/docs/changes/unreleased/889-done-card-first-prose-line.md b/docs/changes/unreleased/930-done-card-first-prose-line.md similarity index 99% rename from docs/changes/unreleased/889-done-card-first-prose-line.md rename to docs/changes/unreleased/930-done-card-first-prose-line.md index d9ef144657..053c23e513 100644 --- a/docs/changes/unreleased/889-done-card-first-prose-line.md +++ b/docs/changes/unreleased/930-done-card-first-prose-line.md @@ -1,7 +1,7 @@ --- kind: fixed title: a task card quotes the report's first line that says something, not its literal first line -pr: 889 +pr: 930 surface: [chat] invalidates: - "The landed card's quoted sentence was the report's literal first line (`firstLine(node.report)` in `internal/tui3/taskdone.go`), so a report that opened with a code fence drew \"```\" as its outcome. It is the first line that says something now: leading blank lines and fence markers — ``` and ~~~, with or without a language word — are skipped, a report that is nothing but a fenced block quotes the first line inside it, and a report with no prose at all draws the stamp alone rather than empty quotation marks. `internal/tui3`'s `firstProseLine` is the one reader, and every road that fills a card's quoted half goes through it."