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
19 changes: 16 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,18 @@ confirm the running service is on the merge SHA. Report once, when it is deploye
when it is merged. "Merged" is not a delivery; the reviewer cannot verify anything until
expbkt3 is actually serving the new build, and a build or install can still fail after a
green PR. Ground truth is `/home/ubuntu/.t3/expbkt3-dev/deployed-sha` matching the merge
commit, plus `t3-expbkt3.service` active. This authorisation covers `expbkmain` only —
`bkmain` still needs a human, because deploying it kills the team's live sessions.
commit, plus `t3-expbkt3.service` active. This authorisation covers `expbkmain`, and
`stage` for the Manager session below, only — `bkmain` still needs a human, because
deploying it kills the team's live sessions.

**Tushar's Manager session may merge to `stage` by its own judgement.** Tushar's Manager
session (the bk-docs `manager` skill, `tushar` child) may merge a green PR into `stage`
without a further ask, once CI is green and the change is reviewed. It then owns the
change until it is live: it verifies it running on stagebkt3.dev.beknown.live, the same
way as the `expbkmain` rule above. A merge or promotion to `bkmain` still needs Tushar.
The Manager asks once, as one item on its batched Toolyard decision card, because a
`bkmain` deploy restarts bkt3 and stops the live sessions; Tushar then picks the time.
(Tushar, 2026-10-23.)

## Building features that survive upstream merges

Expand Down Expand Up @@ -223,7 +233,10 @@ For authorized mobile verification, a missing or outdated native client is a bui
- Never make a PR unless the developer explicitly asks you to do so.
- Merging a green PR into `expbkmain` needs no further ask, but it is not finished at
merge: follow it through the deploy and report only once expbkt3 is serving it. See
"Beknown fork and deployments". Merging to `bkmain` still needs a human.
"Beknown fork and deployments". Tushar's Manager session may also merge a green,
reviewed PR into `stage` by its own judgement, then verify it on stagebkt3. Merging to
`bkmain` still needs a human (for the Manager: one item on its batched Toolyard
decision card).
- Conventional commit titles, plain language: `fix(web): new threads no longer spike CPU`.
- Body: the problem in a sentence or two, then how you fixed it. End with the model and harness that did the work.
- UI changes need before/after images. Motion or timing needs a short video.
Expand Down
68 changes: 43 additions & 25 deletions apps/web/src/components/PhaseGroupedSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ import {
PHASE_SIDEBAR_SECTION_ACTION_BUTTON_CLASS_NAME,
PHASE_SIDEBAR_SECTION_MANAGED_HEADER_CLASS_NAME,
phaseSidebarCustomGroupAccentStyle,
// T3-CUSTOM(expbkt3): a nested row's own group chip in Custom mode.
phaseSidebarNestedRowShowsCustomGroup,
phaseSidebarSectionActionsClassName,
phaseSidebarSectionHeaderClassName,
} from "./sidebar/PhaseGroupedSidebar.logic";
Expand Down Expand Up @@ -1113,7 +1115,11 @@ interface PhaseThreadRowProps {
// `groupActions` absent = the host cannot store custom groups.
readonly groupActions?: PhaseThreadRowGroupActions;
readonly customGroupId?: string | null;
/** The group's display label, shown as a chip outside Custom mode only. */
/**
* The group's display label, shown as a chip outside Custom mode. In Custom
* mode only on Running shelf rows and on nested rows filed in another group
* than their parent.
*/
readonly customGroupLabel?: string | null;
/** The group's colour (a hex value) from the shared registry, tinting that chip. */
readonly customGroupColor?: string | null;
Expand Down Expand Up @@ -1602,27 +1608,26 @@ const PhaseThreadRow = memo(function PhaseThreadRow(props: PhaseThreadRowProps)
// T3-CUSTOM(expbkt3): custom groups. Always offered, whichever mode is
// showing — placing a session is cheap, and the group is waiting when the
// user switches to Custom.
// A nested row is placed with its parent, so offering to move it alone
// would show a tick for a group it never appears in.
const groupItems =
groupActions && treeParentKey == null
? [
{
id: "move-to-group",
label: "Move to group",
children: [
...groupActions.groups.map((group) => ({
id: `group:${group.id}`,
label: customGroupId === group.id ? `${group.label} ✓` : group.label,
})),
...(customGroupId != null
? [{ id: "group:none", label: "Remove from group" }]
: []),
{ id: "group:new", label: "New group…" },
],
},
]
: [];
// Offered on nested rows too. A nested row still renders under its parent
// in Custom mode (a subtree is placed by its root), but it now shows its
// own group chip when that group differs from its parent's, so the move
// visibly took effect. The group also applies when the row is detached.
const groupItems = groupActions
? [
{
id: "move-to-group",
label: "Move to group",
children: [
...groupActions.groups.map((group) => ({
id: `group:${group.id}`,
label: customGroupId === group.id ? `${group.label} ✓` : group.label,
})),
...(customGroupId != null ? [{ id: "group:none", label: "Remove from group" }] : []),
{ id: "group:new", label: "New group…" },
],
},
]
: [];
// Side-by-side sessions. First in the menu because it is the one item that
// starts work rather than tidying up after it, and because the whole point
// is opening a second session without leaving the one you are reading.
Expand Down Expand Up @@ -4094,7 +4099,8 @@ export function PhaseGroupedSidebar() {
// clean while silently leaving every treeXxx prop undefined, which reads as
// "the feature is off" rather than as a build error.
tree?: PhaseThreadRowTreeProps,
// T3-CUSTOM(expbkt3): Running shelf rows left their group, so they name it.
// T3-CUSTOM(expbkt3): Running shelf rows left their group, so they name it,
// and so does a nested row filed in another group than its parent.
showGroup = false,
) => {
const key = scopedThreadKey(scopeThreadRef(row.thread.environmentId, row.thread.id));
Expand Down Expand Up @@ -4150,8 +4156,16 @@ export function PhaseGroupedSidebar() {
// T3-CUSTOM(expbkt3): custom groups.
{...(groupActions ? { groupActions } : {})}
customGroupId={phaseSidebarCustomGroupIdForRow(row)}
// T3-CUSTOM(expbkt3): in Custom mode a chip names the group as the
// registry spells it (the section header's label), not the row's own spelling.
customGroupLabel={
grouping.groupBy === "custom" && !showGroup ? null : (row.customGroup ?? null)
grouping.groupBy !== "custom"
? (row.customGroup ?? null)
: showGroup
? (customGroupLabels.get(phaseSidebarCustomGroupIdForRow(row) ?? "") ??
row.customGroup ??
null)
: null
}
customGroupColor={customGroupColors.get(phaseSidebarCustomGroupIdForRow(row) ?? "") ?? null}
linearIssueStatuses={linearStatusesForRow(row)}
Expand Down Expand Up @@ -4191,7 +4205,11 @@ export function PhaseGroupedSidebar() {
aria-label={`Sessions started by ${node.row.thread.title}`}
className="mt-0.5 space-y-0.5 border-l border-sidebar-border pl-1"
>
{node.children.map((child) => renderTreeNode(child))}
{/* T3-CUSTOM(expbkt3): a child filed in another group than its
parent names that group, since it renders in the parent's section. */}
{node.children.map((child) =>
renderTreeNode(child, phaseSidebarNestedRowShowsCustomGroup(child.row, node.row)),
)}
</ul>
</li>
) : null}
Expand Down
48 changes: 48 additions & 0 deletions apps/web/src/components/sidebar/PhaseGroupedSidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ import {
resolvePhaseSidebarWorkBadge,
sanitizePhaseSidebarFilters,
sanitizePhaseSidebarSort,
// T3-CUSTOM(expbkt3): a nested row's own group chip in Custom mode.
phaseSidebarNestedRowShowsCustomGroup,
type PhaseSidebarRow,
} from "./PhaseGroupedSidebar.logic";

Expand Down Expand Up @@ -2159,3 +2161,49 @@ describe("phase sidebar pinning", () => {
});
});
// T3-CUSTOM(expbkt3): END

// T3-CUSTOM(expbkt3): BEGIN — a nested row's own group chip in Custom mode.
describe("phaseSidebarNestedRowShowsCustomGroup", () => {
it("shows the chip when the child's group differs from its parent's", () => {
expect(
phaseSidebarNestedRowShowsCustomGroup({ customGroup: "Today" }, { customGroup: "Manager" }),
).toBe(true);
});

it("shows the chip when the parent has no group and the child has one", () => {
expect(
phaseSidebarNestedRowShowsCustomGroup({ customGroup: "Today" }, { customGroup: null }),
).toBe(true);
});

it("hides the chip when the child is in its parent's group", () => {
expect(
phaseSidebarNestedRowShowsCustomGroup({ customGroup: "Today" }, { customGroup: "Today" }),
).toBe(false);
});

it("compares groups by their normalised id, not by the label's case", () => {
expect(
phaseSidebarNestedRowShowsCustomGroup({ customGroup: "today" }, { customGroup: "Today" }),
).toBe(false);
});

it("compares groups after trimming whitespace and folding case", () => {
expect(
phaseSidebarNestedRowShowsCustomGroup({ customGroup: " Today " }, { customGroup: "today" }),
).toBe(false);
});

it("hides the chip when neither the child nor its parent has a group", () => {
expect(
phaseSidebarNestedRowShowsCustomGroup({ customGroup: null }, { customGroup: null }),
).toBe(false);
});

it("hides the chip when the child has no group", () => {
expect(
phaseSidebarNestedRowShowsCustomGroup({ customGroup: null }, { customGroup: "Today" }),
).toBe(false);
});
});
// T3-CUSTOM(expbkt3): END
26 changes: 26 additions & 0 deletions apps/web/src/components/sidebar/PhaseGroupedSidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
export * from "@t3tools/client-runtime/state/phase-sidebar";

import type { PhaseSidebarPhaseId } from "@t3tools/client-runtime/state/phase-sidebar";
// T3-CUSTOM(expbkt3): a nested row's own group chip in Custom mode.
import {
phaseSidebarCustomGroupIdForRow,
type PhaseSidebarRow,
} from "@t3tools/client-runtime/state/phase-sidebar";
// T3-CUSTOM(expbkt3): a custom group's colour is an inline style (a hex value).
import type { CSSProperties } from "react";
// T3-CUSTOM(expbkt3): Linear tags on a session.
Expand Down Expand Up @@ -367,3 +372,24 @@ export function resolvePhaseSidebarShelfRows<Row>(input: {
routedRow !== undefined && routedIndex >= page.length ? [...page, routedRow] : page;
return { rendered, hiddenCount: rows.length - rendered.length, containsRoutedThread };
}

// T3-CUSTOM(expbkt3): BEGIN — a nested row's own group chip in Custom mode.
/**
* Whether a NESTED row shows its own custom-group chip in Custom mode.
*
* Custom mode places a whole subtree by its root, so a child renders under its
* parent in the parent's section, whatever its own group is. A top-level row
* needs no chip there (the section header names its group), but a child filed
* in a different group would hide that group completely. So a child shows its
* own chip only when it has a group and that group differs from its direct
* parent's. Groups compare by their normalised id, so "Today" and "today" are
* the same group.
*/
export function phaseSidebarNestedRowShowsCustomGroup(
row: Pick<PhaseSidebarRow, "customGroup">,
parent: Pick<PhaseSidebarRow, "customGroup">,
): boolean {
const ownGroupId = phaseSidebarCustomGroupIdForRow(row);
return ownGroupId !== null && ownGroupId !== phaseSidebarCustomGroupIdForRow(parent);
}
// T3-CUSTOM(expbkt3): END
7 changes: 7 additions & 0 deletions docs/user/thread-sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,13 @@ thread sees the same group, on every device. To file a thread, open its menu
and choose **Move to group**, pick an existing group or **New group…**, or
**Remove from group**.

<!-- T3-CUSTOM(expbkt3): a nested session's own group chip. -->

In Custom mode, a session nested under a parent session stays under that
parent, in the parent's group. You can still move the nested session to a
group of its own. When its group differs from its parent's, the row shows a
chip with its own group's name.

<!-- T3-CUSTOM(expbkt3): XFN-59 shared group registry, colours and header actions. -->

The groups themselves are shared too. Each environment keeps a list of its
Expand Down
Loading