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
5 changes: 5 additions & 0 deletions .changeset/rename-worker-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aoagents/ao-web": minor
---

Add inline rename for worker sessions in the sidebar. Each worker row now shows a small pencil button on hover; clicking it swaps the label for an input pre-filled with the current title. Enter persists via `PATCH /api/sessions/:id`, Escape cancels, and an empty value reverts the session to its default title. The rename is written to the existing `displayName` metadata field and is now the highest-priority signal in `getSessionTitle`, so a user-chosen label always beats PR/issue titles. The session ID (`ao-N`) remains the canonical identifier — only display surfaces change. (#1647)
60 changes: 60 additions & 0 deletions packages/core/src/__tests__/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,66 @@ describe("writeMetadata + readMetadata", () => {
const meta = readMetadata(dataDir, "app-6");
expect(meta?.displayName).toBe("Refactor session manager");
});

it("serializes and reads back displayNameUserSet flag", () => {
writeMetadata(dataDir, "app-7", {
worktree: "/tmp/w",
branch: "feat/test",
status: "working",
displayName: "PR 1466 review",
displayNameUserSet: true,
});

const content = readFileSync(join(dataDir, "app-7.json"), "utf-8");
const parsed = JSON.parse(content);
expect(parsed.displayNameUserSet).toBe(true);

const meta = readMetadata(dataDir, "app-7");
expect(meta?.displayNameUserSet).toBe(true);
});

it("accepts on/off and true/false for displayNameUserSet (matches prAutoDetect)", () => {
// Defensive: storage paths that flow through unflattenFromStringRecord
// already convert "on"/"off" → boolean before write, but readMetadata
// should still tolerate the legacy string forms for parity with prAutoDetect.
for (const [stored, expected] of [
["on", true],
["off", false],
["true", true],
["false", false],
[true, true],
[false, false],
] as const) {
writeFileSync(
join(dataDir, `flag-${String(stored)}.json`),
JSON.stringify({
worktree: "/tmp/w",
branch: "feat/test",
status: "working",
displayNameUserSet: stored,
}),
"utf-8",
);
const meta = readMetadata(dataDir, `flag-${String(stored)}` as never);
expect(meta?.displayNameUserSet).toBe(expected);
}
});

it("omits displayNameUserSet when undefined and does not flag auto-derived sessions", () => {
writeMetadata(dataDir, "app-8", {
worktree: "/tmp/w",
branch: "feat/test",
status: "working",
displayName: "Auto-derived at spawn",
});

const content = readFileSync(join(dataDir, "app-8.json"), "utf-8");
const parsed = JSON.parse(content);
expect(parsed.displayNameUserSet).toBeUndefined();

const meta = readMetadata(dataDir, "app-8");
expect(meta?.displayNameUserSet).toBeUndefined();
});
});

describe("readMetadataRaw", () => {
Expand Down
14 changes: 13 additions & 1 deletion packages/core/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,16 @@ export function readMetadata(dataDir: string, sessionId: SessionId): SessionMeta
pinnedSummary: raw["pinnedSummary"] as string | undefined,
userPrompt: raw["userPrompt"] as string | undefined,
displayName: raw["displayName"] as string | undefined,
displayNameUserSet:
raw["displayNameUserSet"] === "off" ||
raw["displayNameUserSet"] === "false" ||
raw["displayNameUserSet"] === false
? false
: raw["displayNameUserSet"] === "on" ||
raw["displayNameUserSet"] === "true" ||
raw["displayNameUserSet"] === true
? true
: undefined,
};
}

Expand Down Expand Up @@ -218,7 +228,7 @@ const jsonFields = new Set([
function unflattenFromStringRecord(data: Record<string, string>): Record<string, unknown> {
const result: Record<string, unknown> = {};
const numberFields = new Set(["dashboardPort", "terminalWsPort", "directTerminalWsPort"]);
const booleanFields = new Set(["prAutoDetect"]);
const booleanFields = new Set(["prAutoDetect", "displayNameUserSet"]);

for (const [key, value] of Object.entries(data)) {
if (value === undefined || value === "") continue;
Expand Down Expand Up @@ -281,6 +291,8 @@ export function writeMetadata(
if (metadata.pinnedSummary) data["pinnedSummary"] = metadata.pinnedSummary;
if (metadata.userPrompt) data["userPrompt"] = metadata.userPrompt;
if (metadata.displayName) data["displayName"] = metadata.displayName;
if (metadata.displayNameUserSet !== undefined)
data["displayNameUserSet"] = metadata.displayNameUserSet;

atomicWriteFileSync(path, serializeMetadata(data));
}
Expand Down
23 changes: 18 additions & 5 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1773,13 +1773,26 @@ export interface SessionMetadata {
pinnedSummary?: string; // First quality summary, pinned for display stability
userPrompt?: string; // Prompt used when spawning without a tracker issue
/**
* Stable human-readable display name derived from task context at spawn time.
* Populated from issue title, user prompt, or orchestrator system prompt —
* whichever was available when the session was created. Used by the dashboard
* as a fallback above humanized branch names so sessions are identifiable
* even when PR/issue enrichment is unavailable.
* Human-readable display name for the session.
*
* Populated automatically at spawn time from the best available task context
* (issue title, user prompt, or orchestrator system prompt). Can be
* overwritten later via the dashboard rename UI — the session ID (`ao-N`)
* remains the canonical identifier; only display surfaces are affected.
*
* Whether this value should beat PR/issue titles in the dashboard depends
* on `displayNameUserSet` — auto-derived values stay below live tracker
* signals, user-set values win over them.
*/
displayName?: string;
/**
* Set to `true` when the user explicitly renamed the session via the
* dashboard. The dashboard fallback chain promotes `displayName` above
* PR/issue titles only when this flag is true, so an auto-derived spawn-time
* `displayName` doesn't shadow a live PR title for sessions the user never
* touched.
*/
displayNameUserSet?: boolean;
}

// =============================================================================
Expand Down
Loading
Loading