fix(sessions): show collaboration details in CLI (#128035)

* fix(sessions): expose collaboration details in CLI

* fix(sessions): normalize shared visibility

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Peter Steinberger
2026-08-22 15:14:04 -07:00
committed by GitHub
parent b7cafbe7e8
commit cc56ea534f
2 changed files with 134 additions and 3 deletions
+33
View File
@@ -30,6 +30,11 @@ export type SessionDisplayRow = {
lastInteractionAt?: number;
label?: string;
status?: SessionEntry["status"];
visibility?: SessionEntry["visibility"];
createdActor?: SessionEntry["createdActor"];
owner?: SessionEntry["owner"];
participants?: SessionEntry["participants"];
participantCount?: SessionEntry["participantCount"];
systemSent?: boolean;
abortedLastRun?: boolean;
thinkingLevel?: string;
@@ -76,6 +81,11 @@ export function toSessionDisplayRow(key: string, entry: SessionEntry): SessionDi
lastInteractionAt: entry?.lastInteractionAt,
label: entry?.label,
status: entry?.status,
visibility: entry?.visibility ?? "shared",
createdActor: entry?.createdActor,
owner: entry?.owner,
participants: entry?.participants,
participantCount: entry?.participantCount,
systemSent: entry?.systemSent,
abortedLastRun: entry?.abortedLastRun,
thinkingLevel: entry?.thinkingLevel,
@@ -134,6 +144,10 @@ export function formatSessionModelCell(model: string | null | undefined, rich: b
return rich ? theme.info(label) : label;
}
function formatSessionActor(actor: NonNullable<SessionEntry["createdActor"]>): string {
return actor.label?.trim() || actor.id?.trim() || actor.type;
}
/** Formats compact per-session flags for table output. */
export function formatSessionFlagsCell(
row: Pick<
@@ -149,9 +163,25 @@ export function formatSessionFlagsCell(
| "abortedLastRun"
| "sessionId"
| "runtimePolicySessionKey"
| "visibility"
| "createdActor"
| "owner"
| "participants"
| "participantCount"
>,
rich: boolean,
): string {
const owner = row.owner?.actor ?? row.createdActor;
// Match the canonical session-row participant preview bound.
const participants = (row.participants ?? []).slice(0, 4).map(formatSessionActor);
const remainingParticipants = Math.max(
0,
(row.participantCount ?? participants.length) - participants.length,
);
const participantSummary =
participants.length > 0
? `${participants.join(",")}${remainingParticipants > 0 ? `,+${remainingParticipants}` : ""}`
: undefined;
const flags = [
row.thinkingLevel ? `think:${row.thinkingLevel}` : null,
row.verboseLevel ? `verbose:${row.verboseLevel}` : null,
@@ -162,6 +192,9 @@ export function formatSessionFlagsCell(
row.groupActivation ? `activation:${row.groupActivation}` : null,
row.systemSent ? "system" : null,
row.abortedLastRun ? "aborted" : null,
row.visibility ? `visibility:${row.visibility}` : null,
owner ? `owner:${formatSessionActor(owner)}` : null,
participantSummary ? `participants:${participantSummary}` : null,
row.runtimePolicySessionKey ? `policy:${row.runtimePolicySessionKey}` : null,
row.sessionId ? `id:${row.sessionId}` : null,
].filter(Boolean);
+101 -3
View File
@@ -1,5 +1,10 @@
// Sessions command tests cover listing, details, filtering, and transcript display behavior.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
assignSessionOwner,
recordSessionParticipant,
} from "../config/sessions/session-accessor.js";
import type { SessionEntry } from "../config/sessions/types.js";
import { normalizeSessionDeliveryState } from "../utils/delivery-context.shared.js";
import {
cleanupStore,
@@ -52,7 +57,7 @@ describe("sessionsCommand", () => {
const row = logs.find((line) => line.includes("agent:main:+15555550123")) ?? "";
expect(row).toBe(
"direct agent:main:+15555550123 45m ago test:opus OpenAI Codex 2.0k/200k (1%) id:abc123",
"direct agent:main:+15555550123 45m ago test:opus OpenAI Codex 2.0k/200k (1%) visibility:shared id:abc123",
);
});
@@ -110,7 +115,7 @@ describe("sessionsCommand", () => {
const row = logs.find((line) => line.includes("agent:main:main")) ?? "";
expect(row).toBe(
"direct agent:main:main 1m ago claude-opus-4-7 Claude CLI unknown/200k (?%) id:main-session",
"direct agent:main:main 1m ago claude-opus-4-7 Claude CLI unknown/200k (?%) visibility:shared id:main-session",
);
});
@@ -144,7 +149,7 @@ describe("sessionsCommand", () => {
const row = logs.find((line) => line.includes("agent:main:main")) ?? "";
expect(row).toBe(
"direct agent:main:main 1m ago claude-opus-4-7 Claude CLI unknown/200k (?%) id:main-session",
"direct agent:main:main 1m ago claude-opus-4-7 Claude CLI unknown/200k (?%) visibility:shared id:main-session",
);
});
@@ -279,6 +284,99 @@ describe("sessionsCommand", () => {
expect(group?.totalTokensFresh).toBe(false);
});
it("defaults missing collaboration visibility to shared in JSON output", async () => {
const sessionKey = "agent:main:legacy-shared";
const store = await writeStore(
{
[sessionKey]: {
sessionId: "legacy-shared-session",
updatedAt: Date.now() - 60_000,
model: "test:opus",
},
},
"sessions-default-visibility",
);
const payload = await runSessionsJson<{
sessions?: Array<{ key: string; visibility?: SessionEntry["visibility"] }>;
}>(sessionsCommand, store);
expect(payload.sessions?.find((entry) => entry.key === sessionKey)).toMatchObject({
visibility: "shared",
});
});
it("preserves collaboration metadata in JSON and human output", async () => {
const sessionKey = "agent:main:shared";
const store = await writeStore(
{
[sessionKey]: {
sessionId: "shared-session",
updatedAt: Date.now() - 60_000,
model: "test:opus",
visibility: "suggest",
createdActor: { type: "human", id: "profile-creator", label: "Creator" },
},
},
"sessions-collaboration",
);
const scope = { agentId: "main", sessionKey, storePath: store };
assignSessionOwner(scope, {
owner: { type: "human", id: "profile-owner", label: "Grace" },
assignedBy: { type: "human", id: "profile-admin", label: "Admin" },
assignedAt: Date.now() - 30_000,
});
for (const [id, label] of [
["profile-ada", "Ada"],
["profile-ben", "Ben"],
["profile-cam", "Cam"],
["profile-dee", "Dee"],
["profile-eli", "Eli"],
] as const) {
recordSessionParticipant(scope, {
actor: { type: "human", id, label },
source: "profile",
});
}
const { runtime, logs } = makeRuntime();
await sessionsCommand({ store }, runtime);
const row = logs.find((line) => line.includes(sessionKey)) ?? "";
expect(row).toContain(
"visibility:suggest owner:profile-owner participants:profile-ada,profile-ben,profile-cam,profile-dee,+1",
);
const payload = await runSessionsJson<{
sessions?: Array<
Pick<
SessionEntry,
"visibility" | "createdActor" | "owner" | "participants" | "participantCount"
> & {
key: string;
sharingRole?: unknown;
}
>;
}>(sessionsCommand, store);
const shared = payload.sessions?.find((entry) => entry.key === sessionKey);
expect(shared).toMatchObject({
visibility: "suggest",
createdActor: { type: "human", id: "profile-creator" },
owner: {
actor: { type: "human", id: "profile-owner" },
assignedBy: { type: "human", id: "profile-admin" },
assignedAt: Date.now() - 30_000,
},
participantCount: 5,
participants: [
{ type: "human", id: "profile-ada", source: "profile" },
{ type: "human", id: "profile-ben", source: "profile" },
{ type: "human", id: "profile-cam", source: "profile" },
{ type: "human", id: "profile-dee", source: "profile" },
{ type: "human", id: "profile-eli", source: "profile" },
],
});
expect(shared).not.toHaveProperty("sharingRole");
});
it("reports the SQLite database and omits the retired sessionFile field", async () => {
const store = await writeStore({
"agent:main:main": {