diff --git a/AGENTS.md b/AGENTS.md index 408b8d72808ba..124a87fb76cb9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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. diff --git a/apps/web/src/components/PhaseGroupedSidebar.tsx b/apps/web/src/components/PhaseGroupedSidebar.tsx index 0c1e5f67e9d5b..eb24fd3563097 100644 --- a/apps/web/src/components/PhaseGroupedSidebar.tsx +++ b/apps/web/src/components/PhaseGroupedSidebar.tsx @@ -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"; @@ -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; @@ -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. @@ -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)); @@ -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)} @@ -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)), + )} ) : null} diff --git a/apps/web/src/components/sidebar/PhaseGroupedSidebar.logic.test.ts b/apps/web/src/components/sidebar/PhaseGroupedSidebar.logic.test.ts index 705d5ce40c14e..bdd25ddfbae90 100644 --- a/apps/web/src/components/sidebar/PhaseGroupedSidebar.logic.test.ts +++ b/apps/web/src/components/sidebar/PhaseGroupedSidebar.logic.test.ts @@ -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"; @@ -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 diff --git a/apps/web/src/components/sidebar/PhaseGroupedSidebar.logic.ts b/apps/web/src/components/sidebar/PhaseGroupedSidebar.logic.ts index 14f7eb2e610d0..19ccda979a0ea 100644 --- a/apps/web/src/components/sidebar/PhaseGroupedSidebar.logic.ts +++ b/apps/web/src/components/sidebar/PhaseGroupedSidebar.logic.ts @@ -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. @@ -367,3 +372,24 @@ export function resolvePhaseSidebarShelfRows(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, + parent: Pick, +): boolean { + const ownGroupId = phaseSidebarCustomGroupIdForRow(row); + return ownGroupId !== null && ownGroupId !== phaseSidebarCustomGroupIdForRow(parent); +} +// T3-CUSTOM(expbkt3): END diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index 74485d0f961fc..00919a15bfe58 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -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**. + + +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. + The groups themselves are shared too. Each environment keeps a list of its