authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-07 02:00:36-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-07 02:00:36-07:00
log6e42231cb1da130069cbc694f9da4a185067a81f
tree1bbfcc62d7523148122295a3046edf9a3384efe3
parentbdf5aed665a91f2e1755e6a8ef72cd29627fa937
signature Signed by PGP key B5690EEEBB952194

Extend Done display to 15 minutes and show up to 5 Live Activity banner rows (#3761)


4 files changed, 25 insertions(+), 10 deletions(-)

infra/relay/src/agentActivity/AgentActivityPublisher.test.ts+13-4
...@@ -736,7 +736,7 @@ describe("makeAggregateState", () => {...@@ -736,7 +736,7 @@ describe("makeAggregateState", () => {
736 ...state,736 ...state,
737 threadId: "thread-done" as RelayAgentActivityState["threadId"],737 threadId: "thread-done" as RelayAgentActivityState["threadId"],
738 phase: "completed",738 phase: "completed",
739 updatedAt: "1970-01-01T00:50:00.000Z",739 updatedAt: "1970-01-01T00:44:00.000Z",
740 };740 };
741 const aggregate = AgentActivityPublisher.makeAggregateState({741 const aggregate = AgentActivityPublisher.makeAggregateState({
742 activeStates: [active, staleCompleted],742 activeStates: [active, staleCompleted],
...@@ -769,7 +769,7 @@ describe("makeAggregateState", () => {...@@ -769,7 +769,7 @@ describe("makeAggregateState", () => {
769 });769 });
770 expect(770 expect(
771 AgentActivityPublisher.makeAggregateState({771 AgentActivityPublisher.makeAggregateState({
772 activeStates: [{ ...lingeringCompleted, updatedAt: "1970-01-01T00:50:00.000Z" }],772 activeStates: [{ ...lingeringCompleted, updatedAt: "1970-01-01T00:44:00.000Z" }],
773 terminalState: null,773 terminalState: null,
774 nowMs: hourMs,774 nowMs: hourMs,
775 }),775 }),
...@@ -789,16 +789,25 @@ describe("makeAggregateState", () => {...@@ -789,16 +789,25 @@ describe("makeAggregateState", () => {
789 updatedAt: "1970-01-01T00:59:00.000Z",789 updatedAt: "1970-01-01T00:59:00.000Z",
790 };790 };
791 const aggregate = AgentActivityPublisher.makeAggregateState({791 const aggregate = AgentActivityPublisher.makeAggregateState({
792 activeStates: [mkActive("a-1"), mkActive("a-2"), mkActive("a-3"), justCompleted],792 activeStates: [
793 mkActive("a-1"),
794 mkActive("a-2"),
795 mkActive("a-3"),
796 mkActive("a-4"),
797 mkActive("a-5"),
798 justCompleted,
799 ],
793 terminalState: null,800 terminalState: null,
794 nowMs: hourMs,801 nowMs: hourMs,
795 });802 });
796803
797 expect(aggregate?.activeCount).toBe(3);804 expect(aggregate?.activeCount).toBe(5);
798 expect(aggregate?.activities).toMatchObject([805 expect(aggregate?.activities).toMatchObject([
799 { threadId: "a-1" },806 { threadId: "a-1" },
800 { threadId: "a-2" },807 { threadId: "a-2" },
801 { threadId: "a-3" },808 { threadId: "a-3" },
809 { threadId: "a-4" },
810 { threadId: "a-5" },
802 ]);811 ]);
803 });812 });
804});813});
infra/relay/src/agentActivity/AgentActivityPublisher.ts+7-3
...@@ -13,6 +13,7 @@ import * as Option from "effect/Option";...@@ -13,6 +13,7 @@ import * as Option from "effect/Option";
13import {13import {
14 isExpiredAgentActivityState,14 isExpiredAgentActivityState,
15 isTerminalPhase,15 isTerminalPhase,
16 MAX_ACTIVITY_ROWS,
16 sanitizeAgentActivityAggregateState,17 sanitizeAgentActivityAggregateState,
17} from "./agentActivityPayloads.ts";18} from "./agentActivityPayloads.ts";
1819
...@@ -228,7 +229,7 @@ function terminalAggregateState(state: RelayAgentActivityState): RelayAgentActiv...@@ -228,7 +229,7 @@ function terminalAggregateState(state: RelayAgentActivityState): RelayAgentActiv
228// How long a finished thread keeps its Done/Failed row in the aggregate while229// How long a finished thread keeps its Done/Failed row in the aggregate while
229// other agents are still active. Long enough to be seen on the lock screen,230// other agents are still active. Long enough to be seen on the lock screen,
230// short enough that the activity list stays about live work.231// short enough that the activity list stays about live work.
231export const TERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MS = 5 * 60 * 1_000;232export const TERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MS = 15 * 60 * 1_000;
232233
233function isRecentTerminalState(state: RelayAgentActivityState, nowMs: number): boolean {234function isRecentTerminalState(state: RelayAgentActivityState, nowMs: number): boolean {
234 if (!isTerminalPhase(state)) {235 if (!isTerminalPhase(state)) {
...@@ -273,7 +274,7 @@ export function makeAggregateState(input: {...@@ -273,7 +274,7 @@ export function makeAggregateState(input: {
273 subtitle: newest.phase === "failed" ? "Agent work failed" : "Agent work completed",274 subtitle: newest.phase === "failed" ? "Agent work failed" : "Agent work completed",
274 activeCount: 0,275 activeCount: 0,
275 updatedAt: newest.updatedAt,276 updatedAt: newest.updatedAt,
276 activities: recentTerminal.slice(0, 3).map(aggregateRowForState),277 activities: recentTerminal.slice(0, MAX_ACTIVITY_ROWS).map(aggregateRowForState),
277 });278 });
278 }279 }
279 // Recently finished threads ride along after the active ones (display slots280 // Recently finished threads ride along after the active ones (display slots
...@@ -282,7 +283,10 @@ export function makeAggregateState(input: {...@@ -282,7 +283,10 @@ export function makeAggregateState(input: {
282 const recentTerminalStates = input.activeStates283 const recentTerminalStates = input.activeStates
283 .filter((state) => isRecentTerminalState(state, input.nowMs))284 .filter((state) => isRecentTerminalState(state, input.nowMs))
284 .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));285 .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
285 const displayedStates = [...activeStates.slice(0, 3), ...recentTerminalStates].slice(0, 3);286 const displayedStates = [
287 ...activeStates.slice(0, MAX_ACTIVITY_ROWS),
288 ...recentTerminalStates,
289 ].slice(0, MAX_ACTIVITY_ROWS);
286 const updatedAt = [...activeStates, ...recentTerminalStates].reduce((latest, state) =>290 const updatedAt = [...activeStates, ...recentTerminalStates].reduce((latest, state) =>
287 state.updatedAt.localeCompare(latest.updatedAt) > 0 ? state : latest,291 state.updatedAt.localeCompare(latest.updatedAt) > 0 ? state : latest,
288 ).updatedAt;292 ).updatedAt;
infra/relay/src/agentActivity/ApnsDeliveries.test.ts+2-2
...@@ -373,7 +373,7 @@ describe("ApnsDeliveries", () => {...@@ -373,7 +373,7 @@ describe("ApnsDeliveries", () => {
373 ...aggregate,373 ...aggregate,
374 title: longTitle,374 title: longTitle,
375 subtitle: longTitle,375 subtitle: longTitle,
376 activities: [0, 1, 2, 3].map((index) =>376 activities: [0, 1, 2, 3, 4, 5].map((index) =>
377 Object.assign({}, aggregate.activities[0]!, {377 Object.assign({}, aggregate.activities[0]!, {
378 projectTitle: longTitle,378 projectTitle: longTitle,
379 threadTitle: longTitle,379 threadTitle: longTitle,
...@@ -396,7 +396,7 @@ describe("ApnsDeliveries", () => {...@@ -396,7 +396,7 @@ describe("ApnsDeliveries", () => {
396 const payloadAggregate = queuedJobs[0]?.payload.aggregate;396 const payloadAggregate = queuedJobs[0]?.payload.aggregate;
397 expect(payloadAggregate?.title.length).toBeLessThanOrEqual(120);397 expect(payloadAggregate?.title.length).toBeLessThanOrEqual(120);
398 expect(payloadAggregate?.subtitle.length).toBeLessThanOrEqual(120);398 expect(payloadAggregate?.subtitle.length).toBeLessThanOrEqual(120);
399 expect(payloadAggregate?.activities).toHaveLength(3);399 expect(payloadAggregate?.activities).toHaveLength(5);
400 expect(payloadAggregate?.activities[0]?.projectTitle.length).toBeLessThanOrEqual(120);400 expect(payloadAggregate?.activities[0]?.projectTitle.length).toBeLessThanOrEqual(120);
401 expect(payloadAggregate?.activities[0]?.status.length).toBeLessThanOrEqual(40);401 expect(payloadAggregate?.activities[0]?.status.length).toBeLessThanOrEqual(40);
402 expect(payloadAggregate?.activities[0]?.deepLink).toBe("/");402 expect(payloadAggregate?.activities[0]?.deepLink).toBe("/");
infra/relay/src/agentActivity/agentActivityPayloads.ts+3-1
...@@ -43,7 +43,9 @@ export function isExpiredAgentActivityState(...@@ -43,7 +43,9 @@ export function isExpiredAgentActivityState(
43const MAX_SUMMARY_TEXT_LENGTH = 120;43const MAX_SUMMARY_TEXT_LENGTH = 120;
44const MAX_STATUS_TEXT_LENGTH = 40;44const MAX_STATUS_TEXT_LENGTH = 40;
45const MAX_DEEP_LINK_LENGTH = 512;45const MAX_DEEP_LINK_LENGTH = 512;
46const MAX_ACTIVITY_ROWS = 3;46// The Live Activity banner (lock screen / Notification Center) renders up to
47// five rows; the expanded Dynamic Island shows the top three of these.
48export const MAX_ACTIVITY_ROWS = 5;
4749
48function truncateText(value: string, maxLength: number): string {50function truncateText(value: string, maxLength: number): string {
49 const trimmed = value.trim();51 const trimmed = value.trim();