Skip to content

Commit 8cd200e

Browse files
Merge origin/main into codex/agent-assisted-onboarding-latest
2 parents e953c4b + 0ca8237 commit 8cd200e

4 files changed

Lines changed: 168 additions & 4 deletions

File tree

ui/src/e2e/session-management.e2e.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ function sessionRow(
3333
hasActiveRun?: boolean;
3434
status?: string;
3535
spawnedBy?: string;
36+
execNode?: string;
37+
worktree?: { branch?: string; repoRoot?: string };
3638
} = {},
3739
) {
3840
return {
@@ -969,6 +971,119 @@ describeControlUiE2e("Control UI session management mocked Gateway E2E", () => {
969971
}
970972
});
971973

974+
it("keeps raw ids out of work rows and survives rows growing subtitles in place", async () => {
975+
const baseTime = Date.parse("2026-07-01T16:00:00.000Z");
976+
const nodeHash = "11c38726acc6fac280357576c87acc6fac280357";
977+
const rows = (withWork: boolean) => {
978+
const ts = baseTime + (withWork ? 5_000 : 0);
979+
return [
980+
sessionRow("agent:main:main", "Main", ts),
981+
sessionRow(
982+
"agent:main:dashboard:0f9d5c1e-6d0f-4c9a-9d84-1c2f3a4b5c6d",
983+
"",
984+
ts - 60_000,
985+
withWork ? { execNode: nodeHash } : {},
986+
),
987+
sessionRow(
988+
"agent:main:dashboard:0f9d5c1e-6d0f-4c9a-9d84-1c2f3a4b5c6e",
989+
"",
990+
ts - 120_000,
991+
withWork
992+
? {
993+
execNode: nodeHash,
994+
worktree: { branch: "openclaw/wt-1", repoRoot: "/Users/dev/Projects/clawdbot" },
995+
}
996+
: {},
997+
),
998+
sessionRow("agent:main:node-mcp-debug-4de003fbff138fcb9239c9378b2e", "", ts - 180_000),
999+
];
1000+
};
1001+
const context = await browser.newContext({
1002+
locale: "en-US",
1003+
serviceWorkers: "block",
1004+
viewport: { height: 900, width: 1280 },
1005+
});
1006+
const page = await context.newPage();
1007+
const gateway = await installMockGateway(page, {
1008+
methodResponses: {
1009+
"sessions.list": sessionsListResponse(rows(false)),
1010+
},
1011+
// Defer every list request; the test resolves them per phase so rows
1012+
// first paint single-line and only then grow a subtitle line in place.
1013+
deferredMethods: Array.from({ length: 12 }, () => "sessions.list"),
1014+
sessionKey: "agent:main:main",
1015+
});
1016+
const resolveNextList = async (withWork: boolean) => {
1017+
try {
1018+
await gateway.resolveDeferred("sessions.list", sessionsListResponse(rows(withWork)));
1019+
} catch {
1020+
// No list request queued yet; the poll below retries.
1021+
}
1022+
};
1023+
1024+
try {
1025+
await page.goto(`${server.baseUrl}chat`);
1026+
await expect
1027+
.poll(
1028+
async () => {
1029+
await resolveNextList(false);
1030+
return page.locator(".sidebar-recent-session").count();
1031+
},
1032+
{ timeout: 15_000 },
1033+
)
1034+
.toBeGreaterThan(0);
1035+
// Rows grow a second subtitle line only after first layout: the WebKit
1036+
// overlap regression needs in-place growth, not a static two-line list.
1037+
await gateway.emitGatewayEvent("sessions.changed", {});
1038+
await expect
1039+
.poll(
1040+
async () => {
1041+
await resolveNextList(true);
1042+
return page.locator(".sidebar-recent-session__subtitle").count();
1043+
},
1044+
{ timeout: 15_000 },
1045+
)
1046+
.toBeGreaterThan(0);
1047+
1048+
// Names and subtitles never show raw node ids or raw agent keys.
1049+
const names = await trimmedTextContents(page.locator(".sidebar-recent-session__name"));
1050+
expect(names).toContain("New session");
1051+
expect(names).toContain("clawdbot ⎇ wt-1 · …0357");
1052+
expect(names).toContain("node-mcp-debug-…8b2e");
1053+
const subtitles = await trimmedTextContents(
1054+
page.locator(".sidebar-recent-session__subtitle"),
1055+
);
1056+
expect(subtitles).toContain("…0357");
1057+
for (const text of [...names, ...subtitles]) {
1058+
expect(text).not.toContain(nodeHash);
1059+
expect(text).not.toContain("agent:main:");
1060+
}
1061+
1062+
// Sections must lay out below the rows above them, not paint over them.
1063+
const overlaps = await page.evaluate(() => {
1064+
const rects = [
1065+
...document.querySelectorAll(".sidebar-recent-session, .sidebar-recent-sessions__head"),
1066+
]
1067+
.map((element) => {
1068+
const rect = element.getBoundingClientRect();
1069+
return { top: rect.top, bottom: rect.bottom };
1070+
})
1071+
.filter((rect) => rect.bottom > rect.top)
1072+
.toSorted((a, b) => a.top - b.top);
1073+
let bad = 0;
1074+
for (let index = 1; index < rects.length; index += 1) {
1075+
if (rects[index].top < rects[index - 1].bottom - 2) {
1076+
bad += 1;
1077+
}
1078+
}
1079+
return bad;
1080+
});
1081+
expect(overlaps).toBe(0);
1082+
} finally {
1083+
await context.close();
1084+
}
1085+
});
1086+
9721087
it("keeps sidebar session controls reachable on touch pointers", async () => {
9731088
const context = await browser.newContext({
9741089
hasTouch: true,

ui/src/lib/session-display.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ describe("resolveSessionDisplayName", () => {
3939
}),
4040
).toBe("clawdbot ⎇ wt-3f2a");
4141
});
42+
43+
it("names named subsessions after their slug, never the raw agent key", () => {
44+
expect(resolveSessionDisplayName("agent:main:node-proof-claude")).toBe("node-proof-claude");
45+
expect(resolveSessionDisplayName("agent:main:explicit:node-mcp-debug")).toBe("node-mcp-debug");
46+
expect(
47+
resolveSessionDisplayName(
48+
"agent:main:explicit:model-run-0f9d5c1e-6d0f-4c9a-9d84-1c2f3a4b5c6d",
49+
),
50+
).toBe("model-run-…5c6d");
51+
expect(resolveSessionDisplayName("agent:main:node-fleet-4de003fbff138fcb9239c9378b2e")).toBe(
52+
"node-fleet-…8b2e",
53+
);
54+
});
4255
});
4356

4457
describe("resolveSessionWorkSubtitle", () => {
@@ -53,10 +66,22 @@ describe("resolveSessionWorkSubtitle", () => {
5366
worktree: { branch: "feature/x", repoRoot: "/repo/clawdbot" },
5467
execNode: "macbook",
5568
}),
56-
).toBe("macbook · clawdbot ⎇ feature/x");
69+
).toBe("clawdbot ⎇ feature/x · macbook");
5770
expect(resolveSessionWorkSubtitle({ execNode: "macbook" })).toBe("macbook");
5871
expect(resolveSessionWorkSubtitle({})).toBeUndefined();
5972
});
73+
74+
it("shortens opaque node ids instead of rendering raw hashes", () => {
75+
expect(
76+
resolveSessionWorkSubtitle({ execNode: "11c38726acc6fac280357576c87acc6fac280357" }),
77+
).toBe("…0357");
78+
expect(
79+
resolveSessionWorkSubtitle({
80+
worktree: { branch: "openclaw/wt-1", repoRoot: "/repo/clawdbot" },
81+
execNode: "11c38726acc6fac280357576c87acc6fac280357",
82+
}),
83+
).toBe("clawdbot ⎇ wt-1 · …0357");
84+
});
6085
});
6186

6287
describe("resolveChannelSessionInfo", () => {

ui/src/lib/session-display.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ function shortenPeerId(identifier: string): string {
2525
return trimmed.length <= 10 ? trimmed : `…${trimmed.slice(-6)}`;
2626
}
2727

28+
// Long hex/uuid runs inside keys and node ids are machine ids, not names;
29+
// keep a short recognizable tail so rows never fill with opaque hashes.
30+
const OPAQUE_ID_RUN_RE =
31+
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{10,}/gi;
32+
33+
function shortenOpaqueIdRuns(text: string): string {
34+
return text.replace(OPAQUE_ID_RUN_RE, (match) => `…${match.slice(-4)}`);
35+
}
36+
2837
const WORKTREE_BRANCH_PREFIX = "openclaw/";
2938

3039
const CHANNEL_SESSION_KEY_RE = /^agent:[^:]+:([^:]+)(?::[^:]+)?:(?:direct|group|channel|thread):/;
@@ -58,14 +67,17 @@ type SessionWorktreeDisplayRow = {
5867
export function resolveSessionWorkSubtitle(row: SessionWorktreeDisplayRow): string | undefined {
5968
const repoRoot = normalizeOptionalString(row.worktree?.repoRoot);
6069
const branch = normalizeOptionalString(row.worktree?.branch);
61-
const node = normalizeOptionalString(row.execNode);
70+
// execNode is often a raw node id (long hex); never render it in full.
71+
const rawNode = normalizeOptionalString(row.execNode);
72+
const node = rawNode ? shortenOpaqueIdRuns(rawNode) : undefined;
6273
const repoName = repoRoot ? (repoRoot.split(/[\\/]/).findLast(Boolean) ?? repoRoot) : undefined;
6374
const shortBranch = branch?.startsWith(WORKTREE_BRANCH_PREFIX)
6475
? branch.slice(WORKTREE_BRANCH_PREFIX.length)
6576
: branch;
6677
const checkout = repoName ? (shortBranch ? `${repoName}${shortBranch}` : repoName) : undefined;
6778
if (checkout && node) {
68-
return `${node} · ${checkout}`;
79+
// Checkout first: it names the work; the node is routing detail.
80+
return `${checkout} · ${node}`;
6981
}
7082
return checkout ?? node;
7183
}
@@ -142,6 +154,14 @@ function parseSessionKey(key: string): SessionKeyInfo {
142154
return { prefix: "", fallbackName: "New session" };
143155
}
144156

157+
// Remaining agent keys are named subsessions (CLI --session-id and friends):
158+
// drop the agent:<id>: routing boilerplate and shorten opaque id runs so the
159+
// slug reads as a name instead of a raw key.
160+
const agentKeyMatch = key.match(/^agent:[^:]+:(?:explicit:)?(.+)$/);
161+
if (agentKeyMatch) {
162+
return { prefix: "", fallbackName: shortenOpaqueIdRuns(agentKeyMatch[1]) };
163+
}
164+
145165
// Unknown: return key as-is.
146166
return { prefix: "", fallbackName: key };
147167
}

ui/src/styles/layout.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1098,8 +1098,12 @@ html.openclaw-native-macos .shell-nav-expand {
10981098
opacity: 0.85;
10991099
}
11001100

1101+
/* Flex, not grid: WebKit does not re-run a grid flex-item's intrinsic sizing
1102+
when rows stream in or grow a second subtitle line after first layout, so a
1103+
grid list keeps a stale height and the next section paints over its rows. */
11011104
.sidebar-recent-sessions__list {
1102-
display: grid;
1105+
display: flex;
1106+
flex-direction: column;
11031107
gap: 2px;
11041108
min-height: 12px;
11051109
}

0 commit comments

Comments
 (0)