Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b1a4b37
cmux NEXT Home: the folded step counts use plural forms in every locale
teamleaderleo Oct 21, 2026
f4fab17
cmux NEXT Home: the live book publishes the thought's headline, so th…
teamleaderleo Oct 21, 2026
3dd99e7
cmux NEXT Home: the running indicator shows the published headline as…
teamleaderleo Oct 21, 2026
21861f7
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
a8cbd6c
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
c77db0e
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
a6a9e1d
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
ecb56fa
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
a30ff7b
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
b0a7afb
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
772f911
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
3e44a44
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
0604479
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
3777c8f
Merge branch 'feat-cmux-next' into feat-cmux-next-chief-progress-plurals
teamleaderleo Oct 21, 2026
75a8be4
fix(next): Home step counts carry English's plural forms in every locale
teamleaderleo Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ public struct HomeLiveRun: Hashable, Sendable {
/// The run's talk steps in order, joined by a blank line ("" before the first text).
public var text: String
public var activity: HomeLiveActivity?
/// The newest thought the run streams now (a thought step not yet ended), whole; nil when none.
/// The headline of the run's newest thought (``HomeThoughtHeadline``), kept after its step ends
/// until the next thought; nil when there is none.
/// Home's running indicator shows its headline as the Chief's progress narration.
public var thought: String? = nil

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ struct HomeLiveBook {
.sorted { (Self.place(run.steps[$0.element]), $0.offset) < (Self.place(run.steps[$1.element]), $1.offset) }
.compactMap { run.steps[$0.element]?.text }
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
let thought = run.lastThought.flatMap { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : $0 }
let thought = run.lastThought.flatMap(HomeThoughtHeadline.of)
return HomeLiveRun(participant: key.author, run: key.run, turn: turn(of: key),
text: talk.joined(separator: "\n\n"), activity: run.activity, thought: thought)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation

/// The headline of one of the Chief's thoughts: what its running indicator shows as progress narration.
///
/// The live book publishes this, not the whole thought, so a streamed thought token changes the
/// store's live state only when the line a person sees changes.
public enum HomeThoughtHeadline {
/// Most characters a headline keeps.
public static let limit = 72

/// A thought's headline: its last bold heading line ("**Checking the tests**", how Claude and
/// Codex title their reasoning summaries), else its last sentence, as plain text of at most
/// ``limit`` characters. Nil for a thought with no words.
public static func of(_ thought: String) -> String? {
let lines = thought.split(whereSeparator: \.isNewline).map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
let heading = lines.last { $0.count > 4 && $0.hasPrefix("**") && $0.hasSuffix("**") }
let raw: String
if let heading {
raw = String(heading.dropFirst(2).dropLast(2))
} else if let last = lines.last {
raw = lastSentence(last)
} else {
return nil
}
let plain = raw.replacingOccurrences(of: "**", with: "").replacingOccurrences(of: "`", with: "")
.trimmingCharacters(in: CharacterSet(charactersIn: "*_#> ").union(.whitespaces))
guard !plain.isEmpty else { return nil }
guard plain.count > limit else { return plain }
return String(plain.prefix(limit - 1)).trimmingCharacters(in: .whitespaces) + "…"
}

/// The last sentence of `line` (after its last ". ", "! " or "? "), or the line.
static func lastSentence(_ line: String) -> String {
let trimmed = line.trimmingCharacters(in: .whitespaces)
let body = trimmed.last.map { ".!?".contains($0) } == true ? String(trimmed.dropLast()) : trimmed
// After the last ". ", then the last "! " after it, then the last "? ": after the last end of any kind.
var rest = body
for end in [". ", "! ", "? "] {
if let range = rest.range(of: end, options: .backwards) { rest = String(rest.suffix(from: range.upperBound)) }
}
let sentence = rest.trimmingCharacters(in: .whitespaces)
return sentence.isEmpty ? trimmed : sentence
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct HomeTurnProgress: Equatable {
/// Lines the owner sends for its own writing, not a step of the turn's work.
static let notSteps: Set<String> = ["Thinking", "Writing the reply", "Working"]
/// Most characters a narration row keeps.
static let narrationLimit = 72
static var narrationLimit: Int { HomeThoughtHeadline.limit }

init(summary: String?, narration: String?, current: String) {
self.summary = summary
Expand All @@ -54,7 +54,8 @@ struct HomeTurnProgress: Equatable {
}
let finished = steps.filter { $0.end != nil }.map(\.line).filter { !Self.notSteps.contains($0) }
let summary = Self.summary(finished)
var narration = style == .narrated ? thought.flatMap(Self.headline) : nil
// `thought` is already the headline (the live book publishes `HomeThoughtHeadline.of`).
var narration = style == .narrated ? thought : nil
var current = line
if let told = narration, activity.line == "Thinking" {
// Thinking now: its headline is the running row, not a second row over "Thinking".
Expand Down Expand Up @@ -126,38 +127,6 @@ struct HomeTurnProgress: Equatable {

// MARK: Narration

/// A thought's headline: its last bold heading line ("**Checking the tests**", how Claude and
/// Codex title their reasoning summaries), else its last sentence, as plain text of at most
/// ``narrationLimit`` characters. Nil for a thought with no words.
static func headline(_ thought: String) -> String? {
let lines = thought.split(whereSeparator: \.isNewline).map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
let heading = lines.last { $0.count > 4 && $0.hasPrefix("**") && $0.hasSuffix("**") }
let raw: String
if let heading {
raw = String(heading.dropFirst(2).dropLast(2))
} else if let last = lines.last {
raw = lastSentence(last)
} else {
return nil
}
let plain = raw.replacingOccurrences(of: "**", with: "").replacingOccurrences(of: "`", with: "")
.trimmingCharacters(in: CharacterSet(charactersIn: "*_#> ").union(.whitespaces))
guard !plain.isEmpty else { return nil }
guard plain.count > narrationLimit else { return plain }
return String(plain.prefix(narrationLimit - 1)).trimmingCharacters(in: .whitespaces) + "…"
}

/// The last sentence of `line` (after its last ". ", "! " or "? "), or the line.
static func lastSentence(_ line: String) -> String {
let trimmed = line.trimmingCharacters(in: .whitespaces)
let body = trimmed.last.map { ".!?".contains($0) } == true ? String(trimmed.dropLast()) : trimmed
// After the last ". ", then the last "! " after it, then the last "? ": after the last end of any kind.
var rest = body
for end in [". ", "! ", "? "] {
if let range = rest.range(of: end, options: .backwards) { rest = String(rest.suffix(from: range.upperBound)) }
}
let sentence = rest.trimmingCharacters(in: .whitespaces)
return sentence.isEmpty ? trimmed : sentence
}
/// A thought's headline (``HomeThoughtHeadline``): the store already publishes it this way.
static func headline(_ thought: String) -> String? { HomeThoughtHeadline.of(thought) }
}
Loading