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
65 changes: 65 additions & 0 deletions apps/web/src/performance.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { EventId, ProjectId, TurnId, type OrchestrationThreadActivity } from "@t3tools/contracts";
import {
getLatestThreadForProject,
sortActiveThreadsByOrderKey,
sortPinnedThreadsByOrderKey,
sortThreads,
} from "@t3tools/client-runtime/state/thread-sort";
import { formatHourShort, formatRelativeHourShort } from "@t3tools/shared/usageFormat";
import { bench, describe } from "vite-plus/test";

import { deriveActivePlanState } from "./session-logic";

const projectId = ProjectId.make("benchmark-project");
const turnId = TurnId.make("benchmark-turn");
const start = Date.parse("2026-08-11T00:00:00.000Z");
const threads = Array.from({ length: 1_000 }, (_, index) => {
const timestamp = new Date(start + ((index * 997) % 1_000) * 60_000).toISOString();
return {
id: `thread-${index}`,
projectId,
archivedAt: null,
createdAt: timestamp,
updatedAt: timestamp,
latestUserMessageAt: timestamp,
unsettledAt: null,
};
});
const activities: OrchestrationThreadActivity[] = Array.from({ length: 500 }, (_, index) => ({
id: EventId.make(`activity-${index}`),
turnId,
sequence: index,
createdAt: new Date(start + index * 1_000).toISOString(),
kind: index % 100 === 0 ? "turn.plan.updated" : "tool.completed",
summary: "Benchmark activity",
tone: "info",
payload: index % 100 === 0 ? { plan: [{ step: "Run checks", status: "inProgress" }] } : {},
}));
const hours = Array.from({ length: 24 }, (_, index) =>
new Date(start + index * 3_600_000).toISOString(),
);
const referenceTime = "2026-08-12T00:00:00.000Z";

describe("client performance", () => {
bench("sort 1000 threads by recent activity", () => {
sortThreads(threads, "updated_at");
});
bench("sort 1000 active threads", () => {
sortActiveThreadsByOrderKey(threads);
});
bench("sort 1000 keyless pinned threads", () => {
sortPinnedThreadsByOrderKey(threads);
});
bench("select latest project thread from 1000 threads", () => {
getLatestThreadForProject(threads, projectId, "updated_at");
});
bench("derive plan from 500 activities with 5 plan updates", () => {
deriveActivePlanState(activities, turnId);
});
bench("format 24 hourly usage labels and tooltips", () => {
hours.map((hour) => [
formatHourShort(hour, "America/New_York"),
formatRelativeHourShort(hour, referenceTime, "America/New_York"),
]);
});
});
27 changes: 27 additions & 0 deletions apps/web/src/session-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@ function makeActivity(overrides: {
}

describe("deriveActivePlanState", () => {
it("orders plan snapshots by sequence while ignoring unrelated activities", () => {
const activities = Object.freeze([
makeActivity({
id: "completed",
kind: "turn.plan.updated",
turnId: "turn-1",
sequence: 3,
createdAt: "2026-02-23T00:00:05.000Z",
payload: { plan: [{ step: "Check", status: "completed" }] },
}),
makeActivity({ sequence: 4, kind: "tool.completed" }),
makeActivity({
id: "started",
kind: "turn.plan.updated",
turnId: "turn-1",
sequence: 1,
payload: { plan: [{ step: "Check", status: "inProgress" }] },
}),
makeActivity({ sequence: 2, kind: "context-window.updated" }),
]);
expect(deriveActivePlanState(activities, TurnId.make("turn-1"))?.steps).toEqual([
{ step: "Check", status: "completed", durationMs: 5_000 },
]);
expect(activities[0]?.id).toBe("completed");
expect(deriveActivePlanState([makeActivity({ kind: "tool.completed" })], undefined)).toBeNull();
});

it("returns the latest plan update for the active turn", () => {
const activities: OrchestrationThreadActivity[] = [
makeActivity({
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/session-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,9 @@ export function deriveActivePlanState(
activities: ReadonlyArray<OrchestrationThreadActivity>,
latestTurnId: TurnId | undefined,
): ActivePlanState | null {
const ordered = [...activities].toSorted(compareActivitiesByOrder);
const allPlanActivities = ordered.filter((activity) => activity.kind === "turn.plan.updated");
const allPlanActivities = activities
.filter((activity) => activity.kind === "turn.plan.updated")
.sort(compareActivitiesByOrder);
// Prefer plan from the current turn; fall back to the most recent plan from any turn
// so that TodoWrite tasks persist across follow-up messages.
const latest = Option.firstSomeOf([
Expand Down
45 changes: 45 additions & 0 deletions packages/client-runtime/src/state/threadSort.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ProjectId } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import {
generateSpreadPinOrderKeys,
getLatestThreadForProject,
pinOrderKeyBetween,
planPinnedMove,
planPinnedReorder,
Expand Down Expand Up @@ -58,6 +60,22 @@ describe("resolveSettledThreadTimestamp", () => {
});

describe("sortThreads", () => {
it.each(["created_at", "updated_at"] as const)(
"preserves references, input order and descending id ties for %s",
(sortOrder) => {
const threads = Object.freeze([
makeThread({ id: "a" }),
makeThread({ id: "z" }),
makeThread({ id: "invalid-a", createdAt: "invalid", updatedAt: "invalid" }),
makeThread({ id: "invalid-z", createdAt: "invalid", updatedAt: "invalid" }),
]);
const sorted = sortThreads(threads, sortOrder);
expect(sorted).toEqual([threads[1], threads[0], threads[3], threads[2]]);
expect(sorted[0]).toBe(threads[1]);
expect(threads[0]?.id).toBe("a");
},
);

it("falls back to updatedAt and createdAt when latestUserMessageAt is invalid and there are no messages", () => {
const sorted = sortThreads(
[
Expand Down Expand Up @@ -112,6 +130,33 @@ describe("sortThreads", () => {
});
});

describe("getLatestThreadForProject", () => {
it.each(["created_at", "updated_at"] as const)(
"matches the first sorted eligible thread for %s",
(sortOrder) => {
const projectId = ProjectId.make("project");
const threads = [
{ ...makeThread({ id: "a" }), projectId, archivedAt: null },
{ ...makeThread({ id: "z" }), projectId, archivedAt: null },
{ ...makeThread({ id: "zz" }), projectId, archivedAt: "2026-03-10T00:00:00Z" },
{ ...makeThread({ id: "zzz" }), projectId: ProjectId.make("other"), archivedAt: null },
];
expect(getLatestThreadForProject(threads, projectId, sortOrder)).toBe(threads[1]);
expect(getLatestThreadForProject([], projectId, sortOrder)).toBeNull();
expect(getLatestThreadForProject(threads, ProjectId.make("missing"), sortOrder)).toBeNull();
const invalid = threads.slice(0, 2).map((thread) => ({
...thread,
createdAt: "invalid",
updatedAt: "invalid",
}));
expect(getLatestThreadForProject(invalid, projectId, sortOrder)).toBe(invalid[1]);
expect(
getLatestThreadForProject([threads[1]!, { ...threads[1]! }], projectId, sortOrder),
).toBe(threads[1]);
},
);
});

describe("planPinnedReorder with hidden rows", () => {
it("keeps hidden slots available when inserting between visible neighbors", () => {
const midpoint = pinOrderKeyBetween("f", "t")!;
Expand Down
70 changes: 40 additions & 30 deletions packages/client-runtime/src/state/threadSort.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { OrchestrationThreadShell, ProjectId } from "@t3tools/contracts";
import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings";
import * as Arr from "effect/Array";
import * as Order from "effect/Order";

export interface ThreadSortInput {
readonly createdAt: string;
Expand Down Expand Up @@ -121,19 +119,15 @@ export function sortThreads<T extends { readonly id: string } & ThreadSortInput>
threads: readonly T[],
sortOrder: SidebarThreadSortOrder,
): T[] {
return Arr.sort(
threads,
Order.mapInput(
Order.Struct({
timestamp: Order.flip(Order.Number),
id: Order.flip(Order.String),
}),
(thread: T) => ({
timestamp: getThreadSortTimestamp(thread, sortOrder),
id: thread.id,
}),
),
);
if (threads.length < 2) return [...threads];
return threads
.map((thread) => ({ thread, timestamp: getThreadSortTimestamp(thread, sortOrder) }))
.sort(
(left, right) =>
right.timestamp - left.timestamp ||
(left.thread.id < right.thread.id ? 1 : left.thread.id > right.thread.id ? -1 : 0),
)
.map(({ thread }) => thread);
}

export function getLatestThreadForProject<
Expand All @@ -143,12 +137,21 @@ export function getLatestThreadForProject<
readonly archivedAt: string | null;
} & ThreadSortInput,
>(threads: readonly T[], projectId: ProjectId, sortOrder: SidebarThreadSortOrder): T | null {
return (
sortThreads(
threads.filter((thread) => thread.projectId === projectId && thread.archivedAt === null),
sortOrder,
)[0] ?? null
);
let latest: T | null = null;
let latestTimestamp = Number.NEGATIVE_INFINITY;
for (const thread of threads) {
if (thread.projectId !== projectId || thread.archivedAt !== null) continue;
const timestamp = getThreadSortTimestamp(thread, sortOrder);
if (
latest === null ||
timestamp > latestTimestamp ||
(timestamp === latestTimestamp && thread.id > latest.id)
) {
latest = thread;
latestTimestamp = timestamp;
}
}
return latest;
}

// ── Pinned reorder: fractional index keys ──────────────────────────────
Expand Down Expand Up @@ -290,6 +293,7 @@ export function sortPinnedThreadsByOrderKey<
readonly environmentId?: string | undefined;
},
>(threads: readonly T[]): T[] {
if (threads.length < 2) return [...threads];
const keyed: T[] = [];
const keyless: T[] = [];
for (const thread of threads) {
Expand All @@ -303,14 +307,13 @@ export function sortPinnedThreadsByOrderKey<
const rightKey = right.pinOrderKey!;
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : identityTiebreak(left, right);
});
keyless.sort((left, right) => {
const leftMs = Date.parse(left.createdAt);
const rightMs = Date.parse(right.createdAt);
return (
(Number.isNaN(rightMs) ? 0 : rightMs) - (Number.isNaN(leftMs) ? 0 : leftMs) ||
identityTiebreak(left, right)
);
});
const timestamps = new Map(
keyless.map((thread) => [thread, toSortableTimestamp(thread.createdAt) ?? 0]),
);
keyless.sort(
(left, right) =>
timestamps.get(right)! - timestamps.get(left)! || identityTiebreak(left, right),
);
return [...keyed, ...keyless];
}

Expand All @@ -325,6 +328,13 @@ export function sortActiveThreadsByOrderKey<
readonly environmentId?: string | undefined;
},
>(threads: readonly T[]): T[] {
if (threads.length < 2) return [...threads];
const timestamps = new Map<T, number>();
for (const thread of threads) {
if (thread.activeOrderKey == null) {
timestamps.set(thread, activeThreadAnchorTimestampMs(thread));
}
}
return [...threads].sort((left, right) => {
const leftKey = left.activeOrderKey;
const rightKey = right.activeOrderKey;
Expand All @@ -334,7 +344,7 @@ export function sortActiveThreadsByOrderKey<
if (leftKey != null && rightKey != null) {
order = leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
} else {
order = activeThreadAnchorTimestampMs(right) - activeThreadAnchorTimestampMs(left);
order = timestamps.get(right)! - timestamps.get(left)!;
}
return (
order ||
Expand Down
24 changes: 24 additions & 0 deletions packages/shared/src/usageFormat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,30 @@ import {
} from "./usageFormat.ts";

describe("hourly usage formatting", () => {
it("keeps requested zones separate when formatting repeated calls", () => {
const instant = "2026-08-11T12:37:00.000Z";
for (const zone of ["UTC", "America/New_York", "Asia/Kathmandu", "UTC"]) {
expect(formatHourShort(instant, zone)).toBe(
new Intl.DateTimeFormat("en-US", { timeZone: zone, hour: "numeric" }).format(
new Date(instant),
),
);
}
expect(() => formatHourShort(instant, "Etc/Unknown")).toThrow(RangeError);
expect(formatHourShort("invalid", "UTC")).toBe("invalid");
});

it("uses the current system zone when no zone is supplied", () => {
try {
vi.stubEnv("TZ", "UTC");
expect(formatHourShort("2026-08-11T12:37:00.000Z")).toBe("12 PM");
vi.stubEnv("TZ", "America/New_York");
expect(formatHourShort("2026-08-11T12:37:00.000Z")).toBe("8 AM");
} finally {
vi.unstubAllEnvs();
}
});

it("enumerates 24 fixed buckets across a rolling window", () => {
const hours = enumerateHourStarts("2026-08-10T12:37:00.000Z", "2026-08-11T12:37:00.000Z");

Expand Down
27 changes: 22 additions & 5 deletions packages/shared/src/usageFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ export function enumerateDays(sinceDay: string, untilDay: string): readonly stri

const HOUR_MS = 60 * 60 * 1000;

const dateTimeFormatters = new Map<string, Intl.DateTimeFormat>();

function dateTimeFormatter(
locale: string,
options: Intl.DateTimeFormatOptions,
): Intl.DateTimeFormat {
if (options.timeZone === undefined) return new Intl.DateTimeFormat(locale, options);
const key = JSON.stringify([locale, options]);
let formatter = dateTimeFormatters.get(key);
if (formatter === undefined) {
formatter = new Intl.DateTimeFormat(locale, options);
if (dateTimeFormatters.size >= 16) dateTimeFormatters.clear();
dateTimeFormatters.set(key, formatter);
}
return formatter;
}

/** Every fixed-duration bucket start in an hourly rolling window. */
export function enumerateHourStarts(sinceTime: string, untilTime: string): readonly string[] {
const starts: string[] = [];
Expand All @@ -105,11 +122,11 @@ export function formatHourShort(hourStart: string, timeZone?: string): string {
const instant = new Date(hourStart);
if (Number.isNaN(instant.getTime())) return hourStart;
const options = timeZone === undefined ? {} : { timeZone };
const hourFormat = new Intl.DateTimeFormat("en-US", {
const hourFormat = dateTimeFormatter("en-US", {
...options,
hour: "numeric",
});
const wallHourFormat = new Intl.DateTimeFormat("en-CA", {
const wallHourFormat = dateTimeFormatter("en-CA", {
...options,
year: "numeric",
month: "2-digit",
Expand All @@ -123,7 +140,7 @@ export function formatHourShort(hourStart: string, timeZone?: string): string {
);

if (!isRepeatedHour) return hourFormat.format(instant);
return new Intl.DateTimeFormat("en-US", {
return dateTimeFormatter("en-US", {
...(timeZone === undefined ? {} : { timeZone }),
hour: "numeric",
timeZoneName: "short",
Expand All @@ -134,7 +151,7 @@ export function formatHourShort(hourStart: string, timeZone?: string): string {
export function formatDateTimeShort(instant: string, timeZone?: string): string {
const date = new Date(instant);
if (Number.isNaN(date.getTime())) return instant;
return new Intl.DateTimeFormat("en-US", {
return dateTimeFormatter("en-US", {
...(timeZone === undefined ? {} : { timeZone }),
month: "short",
day: "numeric",
Expand All @@ -154,7 +171,7 @@ export function formatRelativeHourShort(
return formatDateTimeShort(hourStart, timeZone);
}

const dayFormat = new Intl.DateTimeFormat("en-CA", {
const dayFormat = dateTimeFormatter("en-CA", {
...(timeZone === undefined ? {} : { timeZone }),
year: "numeric",
month: "2-digit",
Expand Down
Loading