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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ branches.
like `ERR_PNPM_OUTDATED_LOCKFILE` / "specifiers in the lockfile don't match package.json".
Prefer regenerating the lockfile over repeatedly choosing ours/theirs on `pnpm-lock.yaml` during
multi-commit rebases of `fork/changes`.
- **Per-layer `vp check` after stack rebase (required):** when rebasing or rewriting the stack,
run root **`vp check` on each layer and fix all failures before rebasing the next layer**. Order:
`fork/tim` → `fork/candidates` → `fork/changes` → each overlay → compose `fork/integration`.
Do not push a "green later" tip and stack on top of it. Same rule for feature PRs after
`pnpm fork:stack update`: rebase, then `vp check`, then push/merge. Full detail:
[docs/fork-stack.md](./docs/fork-stack.md) ("Per-layer `vp check` after stack rebase").

## Pull requests (required handoff)

Expand Down
27 changes: 27 additions & 0 deletions docs/fork-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,33 @@ gh workflow run fork-ci.yml --repo patroza/t3code --ref fork/integration
Prefer one deliberate lockfile regeneration at the end of a multi-commit `fork/changes` rebase over
resolving the lockfile at every intermediate conflict.

### Per-layer `vp check` after stack rebase (required)

When you manually rebase or rewrite the stack, **do not advance to the next layer until the current
layer is clean**. After each layer is rebased onto its parent, install/lock is consistent, and
conflicts are resolved:

1. Check out that layer's tip.
2. Run root **`vp check`** (the same formatter/linter gate Fork CI uses).
3. Fix every failure on **that layer** (format, lint, lockfile, type-level breakage the check
surfaces). Commit and force-with-lease push the layer if needed.
4. Only then rebase the **next** layer onto the fixed parent.

Layer order for this gate:

```text
main (upstream mirror — skip product fixes; do not hand-edit)
→ fork/tim
→ fork/candidates
→ fork/changes
→ each integration overlay (desktop, discord, vscode) onto fork/changes
→ fork/integration (compose last)
```

Skipping `vp check` and stacking "fix it later" commits is how lockfile and lint failures cascade
into every PR and block merge. Feature PRs (e.g. based on `fork/changes`) get the same treatment
after `pnpm fork:stack update`: rebase onto the fixed parent, then `vp check` before push/merge.

`register` is used during the one-time cutover and only when intentionally building an advanced,
dependent integration chain:

Expand Down
28 changes: 23 additions & 5 deletions packages/client-runtime/src/state/threadRecencyGroups.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,27 @@ import {
THREAD_RECENCY_BUCKET_LABELS,
} from "./threadRecencyGroups.ts";

/** Local calendar fixture; Date APIs are intentional for bucket tests. */
function localDate(
year: number,
monthIndex: number,
day: number,
hours = 0,
minutes = 0,
seconds = 0,
): Date {
// @effect-diagnostics-next-line globalDate:off
return new Date(year, monthIndex, day, hours, minutes, seconds);
}

function dateFromMs(ms: number): Date {
// @effect-diagnostics-next-line globalDate:off
return new Date(ms);
}

describe("getThreadRecencyBucketId", () => {
// Fixed local afternoon so last-hour and earlier-today both fit in the day.
const now = new Date(2026, 2, 15, 14, 30, 0); // 2026-03-15 14:30 local
const now = localDate(2026, 2, 15, 14, 30, 0); // 2026-03-15 14:30 local

it("splits today into last hour vs earlier today", () => {
const startToday = startOfLocalDay(now).getTime();
Expand Down Expand Up @@ -40,7 +58,7 @@ describe("getThreadRecencyBucketId", () => {
});

describe("groupThreadsByRecency", () => {
const now = new Date(2026, 2, 15, 14, 30, 0);
const now = localDate(2026, 2, 15, 14, 30, 0);
const startToday = startOfLocalDay(now).getTime();
const nowMs = now.getTime();

Expand Down Expand Up @@ -104,10 +122,10 @@ describe("shouldShowRecencySectionHeaders", () => {

describe("groupSortedThreadsByRecency", () => {
it("groups using activity timestamps from ThreadSortInput", () => {
const now = new Date(2026, 2, 15, 14, 30, 0);
const now = localDate(2026, 2, 15, 14, 30, 0);
const startToday = startOfLocalDay(now);
const lastHourIso = new Date(now.getTime() - 5 * 60_000).toISOString();
const olderIso = new Date(startToday.getTime() - 40 * 24 * 60 * 60 * 1000).toISOString();
const lastHourIso = dateFromMs(now.getTime() - 5 * 60_000).toISOString();
const olderIso = dateFromMs(startToday.getTime() - 40 * 24 * 60 * 60 * 1000).toISOString();

const groups = groupSortedThreadsByRecency(
[
Expand Down
30 changes: 26 additions & 4 deletions packages/client-runtime/src/state/threadRecencyGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,30 @@ export const THREAD_RECENCY_BUCKET_LABELS: Record<ThreadRecencyBucketId, string>
const MS_PER_HOUR = 60 * 60 * 1000;
const MS_PER_DAY = 24 * MS_PER_HOUR;

function makeLocalDate(
year: number,
monthIndex: number,
day: number,
hours = 0,
minutes = 0,
seconds = 0,
): Date {
// @effect-diagnostics-next-line globalDate:off
return new Date(year, monthIndex, day, hours, minutes, seconds);
}

function makeDateFromEpochMs(ms: number): Date {
// @effect-diagnostics-next-line globalDate:off
return new Date(ms);
}

function makeNow(): Date {
// @effect-diagnostics-next-line globalDate:off
return new Date();
}

export function startOfLocalDay(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
return makeLocalDate(date.getFullYear(), date.getMonth(), date.getDate());
}

/**
Expand All @@ -44,7 +66,7 @@ export function startOfLocalDay(date: Date): Date {
*/
export function getThreadRecencyBucketId(
timestampMs: number,
now: Date = new Date(),
now: Date = makeNow(),
): ThreadRecencyBucketId {
if (!Number.isFinite(timestampMs)) {
return "older";
Expand Down Expand Up @@ -103,7 +125,7 @@ export function shouldShowRecencySectionHeaders(
export function groupThreadsByRecency<T>(
threads: readonly T[],
getTimestampMs: (thread: T) => number,
now: Date = new Date(),
now: Date = makeNow(),
): ReadonlyArray<ThreadRecencyGroup<T>> {
const buckets = new Map<ThreadRecencyBucketId, T[]>();
for (const id of THREAD_RECENCY_BUCKET_ORDER) {
Expand Down Expand Up @@ -134,7 +156,7 @@ export function groupThreadsByRecency<T>(
*/
export function groupSortedThreadsByRecency<T extends { readonly id: string } & ThreadSortInput>(
threads: readonly T[],
now: Date = new Date(),
now: Date = makeNow(),
): ReadonlyArray<ThreadRecencyGroup<T>> {
return groupThreadsByRecency(
threads,
Expand Down
Loading