Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -135,6 +135,14 @@ public nonisolated struct AgentTurnStates: Hashable, Sendable {
!generated(from: summary["harness"] as? String), !generated(from: summary["family"] as? String) {
return name
}
if !includingPrompt {
// acpmux publishes the first prompt as `title` before its title
// request completes. Only a source that names a real chat title
// may feed durable workspace auto-naming; an absent source is an
// older daemon and is intentionally treated as unknown.
let source = summary["titleSource"] as? String
guard let source, ["harness", "generated", "adopted"].contains(source) else { return nil }
}
if let title = summary["title"] as? String, !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return promptGist(title)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
public import CmuxNextDesignTokens

/// The workspace auto-name prototypes (hq#1957; Debug Settings
/// `workspaces.autoName.variant`, DEV and NIGHTLY only; Release always uses
/// `live`). One is deleted once Leo picks after dogfood.
/// The workspace auto-name variants (hq#1957; Debug Settings
/// `workspaces.autoName.variant`, DEV and NIGHTLY only). Persisted first-chat
/// titles are the default; live labels remain available for comparison.
public nonisolated enum WorkspaceAutoNameVariant: String, Sendable, CaseIterable, TunableChoice {
/// A: the sidebar row shows the name its front tab gives, and follows that
/// tab as it changes; the daemon keeps the default name.
/// The sidebar row shows the name its front tab gives, and follows that tab
/// as it changes; the daemon keeps the default name.
case live
/// B: as `live`, and the first name the front tab gives is written to the
/// daemon once (Rename Workspace's command), so the CLI, window title and
/// search see it.
/// The first generated chat title is written to the daemon once, so the
/// CLI, window title and search see it. Manual and agent renames win.
case persist

public var tunableTitle: String {
Expand All @@ -27,7 +26,7 @@ public nonisolated struct WorkspaceAutoNameTunables {
public static let variant = Tunable<WorkspaceAutoNameVariant>.choice(
"workspaces.autoName.variant", .sidebar, "Workspace auto-name",
help: "How a workspace you never named takes its front tab's name. Applies at once.",
default: .live, code: "WorkspaceAutoNameTunables.variant")
default: .persist, code: "WorkspaceAutoNameTunables.variant")

public static var all: [TunableDescriptor] { [variant.descriptor] }
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ struct AgentTabNamingTests {
"a name the user gave still wins")
}

@Test func generatedTitleDoesNotTreatPromptFallbackAsGenerated() {
let prompt = [
"sessionId": "p1", "name": "claude-1", "harness": "claude",
"title": "Fix the sidebar drag", "titleSource": "prompt",
] as [String: Any]
#expect(AgentTurnStates.title(prompt, includingPrompt: false) == nil)

var generated = prompt
generated["titleSource"] = "generated"
#expect(AgentTurnStates.title(generated, includingPrompt: false) == "Fix the sidebar drag")

var renamed = prompt
renamed["titleSource"] = "named"
#expect(AgentTurnStates.title(renamed, includingPrompt: false) == nil)
}

@Test func anUntitledChatReadsAsItsWorkspacesTask() {
let turns = AgentTurnStateStore()
let ref = AgentSessionRef(host: Self.chiefHost, session: "c9", harness: "claude")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ struct WorkspaceAutoNameTests {
private static let live = WorkspaceAutoName(enabled: true, variant: .live)
private static let persist = WorkspaceAutoName(enabled: true, variant: .persist)

@Test func theDefaultVariantPersistsTheFirstChatTitle() {
#expect(WorkspaceAutoNameTunables.variant.defaultValue == .persist)
}

private static func terminal(title: String? = nil, cwd: String? = nil, userName: String? = nil) -> WorkspaceAutoName.Front {
.terminal(NewWorkspaceName.Tab(kind: .terminal, userName: userName, title: title, pageTitle: nil, url: nil, cwd: cwd))
}
Expand Down
7 changes: 7 additions & 0 deletions cmux-tui/crates/acpmux/spec/acpmux-protocol.json
Original file line number Diff line number Diff line change
Expand Up @@ -3372,6 +3372,13 @@
"null"
]
},
"titleSource": {
"description": "Who supplied `title`: `prompt` is the first-prompt fallback, while\n`harness`, `generated` and `adopted` are real chat titles. Older peers\nmay omit this field.",
"type": [
"string",
"null"
]
},
"tools": {
"description": "The chat's tools (`hub/tools.rs`).",
"items": {
Expand Down
1 change: 1 addition & 0 deletions cmux-tui/crates/acpmux/src/hub/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ impl Hub {
depth: m.subsession.as_ref().map_or(0, |l| l.depth),
children: self.subsessions.open_count(&m.id),
title: m.title.clone(),
title_source: m.title_source.clone(),
last_prompt: m.last_prompt.clone(),
preview: m.preview.as_deref().map(|p| short_text(p, 160)),
last_message: last_message(&m),
Expand Down
6 changes: 6 additions & 0 deletions cmux-tui/crates/acpmux/src/protocol/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ pub struct SessionSummary {
/// Its sub-sessions that are not closed.
pub children: u32,
pub title: Option<String>,
/// Who supplied `title`: `prompt` is the first-prompt fallback, while
/// `harness`, `generated` and `adopted` are real chat titles. Older peers
/// may omit this field.
#[serde(default)]
#[schemars(default)]
pub title_source: Option<String>,
pub last_prompt: Option<String>,
pub preview: Option<String>,
/// One sidebar line; null before the first prompt.
Expand Down
6 changes: 6 additions & 0 deletions webviews/src/agent-session/acpmux/protocol/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,12 @@ export type SessionSummary = {
/** Tags that have not expired. */
tags: Record<string, string>;
title?: (string | null);
/**
* Who supplied `title`: `prompt` is the first-prompt fallback, while
* `harness`, `generated` and `adopted` are real chat titles. Older peers
* may omit this field.
*/
titleSource?: (string | null);
/** The chat's tools (`hub/tools.rs`). */
tools: ToolSummary[];
/** `_acpmux/set_tools` can change them. */
Expand Down
Loading