chore(release): support manual draft builds

This commit is contained in:
777genius 2026-05-18 02:04:13 +03:00
parent 4a8cec9dc2
commit 7742c528ad
6 changed files with 195 additions and 161 deletions

View file

@ -5,14 +5,29 @@ on:
tags:
- 'v*'
workflow_dispatch:
inputs:
release_tag:
description: Release tag to build into a draft, for example v2.0.0
required: true
type: string
publish_release:
description: Publish the release after all assets are uploaded
required: true
default: false
type: boolean
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
group: release-${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || inputs.release_tag || github.ref }}
cancel-in-progress: false
env:
IS_RELEASE_BUILD: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
RELEASE_TAG: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || inputs.release_tag }}
PUBLISH_RELEASE: ${{ startsWith(github.ref, 'refs/tags/v') || inputs.publish_release }}
jobs:
build:
runs-on: ubuntu-latest
@ -36,14 +51,26 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set version from tag
if: startsWith(github.ref, 'refs/tags/v')
- name: Validate release metadata
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
set -euo pipefail
case "${RELEASE_TAG:-}" in
v[0-9]*) ;;
*)
echo "release_tag must start with v and include a numeric version, got '${RELEASE_TAG:-<empty>}'" >&2
exit 1
;;
esac
- name: Set version from release tag
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
run: |
VERSION="${RELEASE_TAG#v}"
pnpm pkg set version="$VERSION"
- name: Verify Sentry release env
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -54,14 +81,14 @@ jobs:
- name: Build app
env:
NODE_OPTIONS: '--max-old-space-size=8192'
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_DSN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: pnpm build
- name: Verify Sentry source map upload
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -70,16 +97,28 @@ jobs:
run: node ./scripts/ci/verify-sentry-release.cjs postbuild
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG:-$TAG}"
existing_is_draft="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft' 2>/dev/null || true)"
if [ -n "$existing_is_draft" ]; then
if [ "${PUBLISH_RELEASE}" != "true" ] && [ "$existing_is_draft" != "true" ]; then
echo "Release $TAG already exists and is not a draft; refusing to overwrite it from draft mode." >&2
exit 1
fi
echo "Release $TAG already exists, skipping creation"
exit 0
fi
gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
--title "$TAG" \
--generate-notes \
--draft=true 2>/dev/null || echo "Release $TAG already exists, skipping creation"
--draft=true
- name: Upload dist artifact
uses: actions/upload-artifact@v7
@ -98,30 +137,55 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- name: Validate release metadata
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
run: |
set -euo pipefail
case "${RELEASE_TAG:-}" in
v[0-9]*) ;;
*)
echo "release_tag must start with v and include a numeric version, got '${RELEASE_TAG:-<empty>}'" >&2
exit 1
;;
esac
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG:-$TAG}"
existing_is_draft="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft' 2>/dev/null || true)"
if [ -n "$existing_is_draft" ]; then
if [ "${PUBLISH_RELEASE}" != "true" ] && [ "$existing_is_draft" != "true" ]; then
echo "Release $TAG already exists and is not a draft; refusing to overwrite it from draft mode." >&2
exit 1
fi
echo "Release $TAG already exists, skipping creation"
exit 0
fi
gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
--title "$TAG" \
--generate-notes \
--draft=true 2>/dev/null || echo "Release $TAG already exists, skipping creation"
--draft=true
- name: Skip runtime asset preparation for manual builds
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
run: echo "Runtime asset preparation is only needed for tagged releases."
- name: Skip runtime asset preparation for non-release builds
if: ${{ env.IS_RELEASE_BUILD != 'true' }}
run: echo "Runtime asset preparation is only needed for release builds."
- name: Check runtime assets
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
id: runtime-assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG:-$TAG}"
existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)"
missing=0
while IFS= read -r asset; do
@ -144,7 +208,7 @@ jobs:
exit 1
fi
TARGET_TAG="${GITHUB_REF#refs/tags/}"
TARGET_TAG="${RELEASE_TAG}"
SOURCE_REPO="$(node ./scripts/runtime-lock.mjs source-repository)"
SOURCE_REF="$(node ./scripts/runtime-lock.mjs source-ref)"
RUNTIME_VERSION="$(node ./scripts/runtime-lock.mjs version)"
@ -215,6 +279,7 @@ jobs:
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG:-$TAG}"
for attempt in $(seq 1 12); do
existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)"
all_found=1
@ -281,10 +346,10 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set version from tag
if: startsWith(github.ref, 'refs/tags/v')
- name: Set version from release tag
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
VERSION="${RELEASE_TAG#v}"
pnpm pkg set version="$VERSION"
- name: Stage bundled runtime (macOS ${{ matrix.arch }})
@ -293,15 +358,15 @@ jobs:
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
TAG="${GITHUB_REF#refs/tags/}"
if [[ "${IS_RELEASE_BUILD}" == "true" ]]; then
TAG="${RELEASE_TAG}"
node ./scripts/stage-runtime.mjs --platform "darwin-${{ matrix.arch }}" --release-tag "$TAG"
else
node ./scripts/stage-runtime.mjs --platform "darwin-${{ matrix.arch }}"
fi
- name: Verify Sentry release env (macOS ${{ matrix.arch }})
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -312,14 +377,14 @@ jobs:
- name: Build app (macOS ${{ matrix.arch }})
env:
NODE_OPTIONS: '--max-old-space-size=8192'
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_DSN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: pnpm build
- name: Verify Sentry source map upload (macOS ${{ matrix.arch }})
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -352,11 +417,11 @@ jobs:
run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/mac-${{ matrix.arch }}/Agent Teams UI.app" darwin
- name: Upload assets to release
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG}"
for f in release/*.dmg release/*.zip release/*.blockmap; do
[ -f "$f" ] || continue
echo "Uploading: $f"
@ -397,11 +462,11 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set version from tag
if: startsWith(github.ref, 'refs/tags/v')
- name: Set version from release tag
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
shell: bash
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
VERSION="${RELEASE_TAG#v}"
pnpm pkg set version="$VERSION"
- name: Stage bundled runtime (Windows)
@ -410,15 +475,15 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$ErrorActionPreference = "Stop"
if ($env:GITHUB_REF -like 'refs/tags/v*') {
$tag = $env:GITHUB_REF.Replace('refs/tags/', '')
if ($env:IS_RELEASE_BUILD -eq 'true') {
$tag = $env:RELEASE_TAG
node ./scripts/stage-runtime.mjs --platform win32-x64 --release-tag $tag
} else {
node ./scripts/stage-runtime.mjs --platform win32-x64
}
- name: Verify Sentry release env (Windows)
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -429,14 +494,14 @@ jobs:
- name: Build app (Windows)
env:
NODE_OPTIONS: '--max-old-space-size=8192'
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_DSN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: pnpm build
- name: Verify Sentry source map upload (Windows)
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -467,12 +532,12 @@ jobs:
run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/win-unpacked" win32
- name: Upload assets to release
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG}"
for f in release/*.exe release/*.blockmap; do
[ -f "$f" ] || continue
echo "Uploading: $f"
@ -518,10 +583,10 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set version from tag
if: startsWith(github.ref, 'refs/tags/v')
- name: Set version from release tag
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
VERSION="${RELEASE_TAG#v}"
pnpm pkg set version="$VERSION"
- name: Stage bundled runtime (Linux)
@ -530,15 +595,15 @@ jobs:
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
TAG="${GITHUB_REF#refs/tags/}"
if [[ "${IS_RELEASE_BUILD}" == "true" ]]; then
TAG="${RELEASE_TAG}"
node ./scripts/stage-runtime.mjs --platform linux-x64 --release-tag "$TAG"
else
node ./scripts/stage-runtime.mjs --platform linux-x64
fi
- name: Verify Sentry release env (Linux)
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -549,14 +614,14 @@ jobs:
- name: Build app (Linux)
env:
NODE_OPTIONS: '--max-old-space-size=8192'
SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_DSN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_DSN || '' }}
SENTRY_AUTH_TOKEN: ${{ (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && secrets.SENTRY_AUTH_TOKEN || '' }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: pnpm build
- name: Verify Sentry source map upload (Linux)
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
@ -584,12 +649,12 @@ jobs:
run: xvfb-run -a node ./scripts/electron-builder/smokePackagedApp.cjs "release/linux-unpacked" linux
- name: Upload assets to release
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -x
TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG}"
ls -la release/ || true
for f in release/*.AppImage release/*.deb release/*.rpm release/*.pacman release/*.blockmap; do
[ -f "$f" ] || continue
@ -602,7 +667,7 @@ jobs:
needs: [release-mac, release-win, release-linux]
runs-on: ubuntu-latest
timeout-minutes: 30
if: startsWith(github.ref, 'refs/tags/v')
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
steps:
- name: Upload stable-named assets for /latest/download links
@ -610,7 +675,8 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF#refs/tags/v}"
VERSION="${RELEASE_TAG#v}"
TAG="${RELEASE_TAG}"
REPO="${GITHUB_REPOSITORY}"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
@ -629,13 +695,13 @@ jobs:
for STABLE_NAME in "${!FILES[@]}"; do
VERSIONED_NAME="${FILES[$STABLE_NAME]}"
echo "Downloading ${VERSIONED_NAME} -> ${STABLE_NAME}"
gh release download "v${VERSION}" \
gh release download "${TAG}" \
--repo "$REPO" \
--pattern "${VERSIONED_NAME}" \
--dir "$TMP_DIR" \
--clobber
cp "${TMP_DIR}/${VERSIONED_NAME}" "${TMP_DIR}/${STABLE_NAME}"
gh release upload "v${VERSION}" "${TMP_DIR}/${STABLE_NAME}" --repo "$REPO" --clobber
gh release upload "${TAG}" "${TMP_DIR}/${STABLE_NAME}" --repo "$REPO" --clobber
done
- name: Publish canonical updater metadata
@ -643,8 +709,8 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF#refs/tags/v}"
TAG="v${VERSION}"
VERSION="${RELEASE_TAG#v}"
TAG="${RELEASE_TAG}"
REPO="${GITHUB_REPOSITORY}"
RELEASE_DATE="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
TMP_DIR="$(mktemp -d)"
@ -724,9 +790,14 @@ jobs:
gh release upload "${TAG}" latest.yml latest-linux.yml latest-mac.yml --repo "${REPO}" --clobber
- name: Publish release
if: ${{ startsWith(github.ref, 'refs/tags/v') || inputs.publish_release }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
TAG="${RELEASE_TAG}"
gh release edit "${TAG}" --repo "${GITHUB_REPOSITORY}" --draft=false --latest
- name: Keep release as draft
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.publish_release }}
run: echo "Draft release ${RELEASE_TAG} is ready. It was not published because publish_release=false."

View file

@ -1397,7 +1397,6 @@ export const TeamDetailView = memo(function TeamDetailView({
});
const [creatingTask, setCreatingTask] = useState(false);
const [addMemberDialogOpen, setAddMemberDialogOpen] = useState(false);
const [runtimeTelemetryPreviewVisible, setRuntimeTelemetryPreviewVisible] = useState(false);
const [addingMemberLoading, setAddingMemberLoading] = useState(false);
const [removeMemberConfirm, setRemoveMemberConfirm] = useState<string | null>(null);
const [updatingRoleLoading, setUpdatingRoleLoading] = useState(false);
@ -2872,68 +2871,63 @@ export const TeamDetailView = memo(function TeamDetailView({
</div>
) : null}
<CollapsibleTeamSection
sectionId="team"
title="Team"
icon={<Users size={14} />}
badge={activeTeammateCount === 0 ? 'Solo' : activeTeammateCount}
defaultOpen
afterBadge={
<Button
variant="ghost"
size="sm"
className="pointer-events-auto h-6 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
onClick={(e) => {
e.stopPropagation();
setAddMemberDialogOpen(true);
}}
>
<UserPlus size={12} />
Add
</Button>
}
action={
<div
className={cn(
'flex items-center gap-3 pr-3 text-[11px] font-medium leading-none text-[var(--color-text-muted)] transition-opacity duration-150',
runtimeTelemetryPreviewVisible ? 'opacity-100' : 'opacity-0'
)}
>
<span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-emerald-500 shadow-[0_0_6px_rgba(34,197,94,0.3)]" />
Memory
</span>
<span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-blue-500 shadow-[0_0_6px_rgba(59,130,246,0.3)]" />
CPU
</span>
<div className="runtime-telemetry-hover-scope">
<CollapsibleTeamSection
sectionId="team"
title="Team"
icon={<Users size={14} />}
badge={activeTeammateCount === 0 ? 'Solo' : activeTeammateCount}
defaultOpen
afterBadge={
<Button
variant="ghost"
size="sm"
className="pointer-events-auto h-6 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
onClick={(e) => {
e.stopPropagation();
setAddMemberDialogOpen(true);
}}
>
<UserPlus size={12} />
Add
</Button>
}
action={
<div className="runtime-telemetry-legend flex items-center gap-3 pr-3 text-[11px] font-medium leading-none text-[var(--color-text-muted)] opacity-0 transition-opacity duration-150">
<span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-emerald-500 shadow-[0_0_6px_rgba(34,197,94,0.3)]" />
Memory
</span>
<span className="flex items-center gap-1.5">
<span className="size-2 rounded-full bg-blue-500 shadow-[0_0_6px_rgba(59,130,246,0.3)]" />
CPU
</span>
</div>
}
contentWrapperClassName="-mx-[calc(1rem-5px)] w-[calc(100%+2rem-10px)]"
>
<div className="px-[calc(1rem-5px)]">
<TeamMemberListBridge
teamName={teamName}
members={membersWithLiveBranches}
expectedTeammateCount={activeTeammateCount}
memberTaskCounts={memberTaskCounts}
taskMap={taskMap}
pendingRepliesByMember={pendingRepliesByMember}
isRosterLoading={loading}
isTeamAlive={data.isAlive}
isTeamProvisioning={isTeamProvisioning}
launchParams={launchParams}
onMemberClick={handleSelectMember}
onSendMessage={handleSendMessageToMember}
onAssignTask={handleAssignTaskToMember}
onOpenTask={handleOpenTaskById}
onRestartMember={handleRestartMember}
onSkipMemberForLaunch={handleSkipMemberForLaunch}
/>
</div>
}
contentWrapperClassName="-mx-[calc(1rem-5px)] w-[calc(100%+2rem-10px)]"
>
<div className="px-[calc(1rem-5px)]">
<TeamMemberListBridge
teamName={teamName}
members={membersWithLiveBranches}
expectedTeammateCount={activeTeammateCount}
memberTaskCounts={memberTaskCounts}
taskMap={taskMap}
pendingRepliesByMember={pendingRepliesByMember}
isRosterLoading={loading}
isTeamAlive={data.isAlive}
isTeamProvisioning={isTeamProvisioning}
launchParams={launchParams}
runtimeTelemetryVisible={runtimeTelemetryPreviewVisible}
onRuntimeTelemetryHoverChange={setRuntimeTelemetryPreviewVisible}
onMemberClick={handleSelectMember}
onSendMessage={handleSendMessageToMember}
onAssignTask={handleAssignTaskToMember}
onOpenTask={handleOpenTaskById}
onRestartMember={handleRestartMember}
onSkipMemberForLaunch={handleSkipMemberForLaunch}
/>
</div>
</CollapsibleTeamSection>
</CollapsibleTeamSection>
</div>
<CollapsibleTeamSection
sectionId="sessions"

View file

@ -90,7 +90,6 @@ interface MemberCardProps {
spawnLaunchState?: MemberLaunchState;
spawnRuntimeAlive?: boolean;
isLaunchSettling?: boolean;
runtimeTelemetryVisible?: boolean;
runtimeTelemetryScale?: RuntimeTelemetryScale;
onOpenTask?: () => void;
onOpenReviewTask?: () => void;
@ -502,11 +501,9 @@ function buildRuntimeTelemetryPaths(
const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({
runtimeEntry,
visible,
scale,
}: {
runtimeEntry?: TeamAgentRuntimeEntry;
visible: boolean;
scale?: RuntimeTelemetryScale;
}): React.JSX.Element | null {
const paths = useMemo(
@ -521,10 +518,7 @@ const MemberRuntimeTelemetryStrip = memo(function MemberRuntimeTelemetryStrip({
<div
aria-hidden="true"
data-testid="member-runtime-telemetry-strip"
className={cn(
'pointer-events-none absolute inset-x-0 bottom-0 z-0 h-5 overflow-hidden rounded-b transition-opacity duration-150',
visible ? 'opacity-100' : 'opacity-0'
)}
className="runtime-telemetry-strip pointer-events-none absolute inset-x-0 bottom-0 z-0 h-5 overflow-hidden rounded-b opacity-0 transition-opacity duration-150"
style={{
WebkitMaskImage:
'linear-gradient(to right, transparent 0, black 44px, black calc(100% - 44px), transparent 100%)',
@ -602,7 +596,6 @@ export const MemberCard = memo(function MemberCard({
spawnLaunchState,
spawnRuntimeAlive,
isLaunchSettling,
runtimeTelemetryVisible = false,
runtimeTelemetryScale,
onOpenTask,
onOpenReviewTask,
@ -696,7 +689,7 @@ export const MemberCard = memo(function MemberCard({
? `Reviewing task: #${deriveTaskDisplayId(reviewTask.id)}`
: undefined;
const runtimeTelemetryTitle = buildRuntimeTelemetryTitle(runtimeEntry);
const showRuntimeTelemetryTooltip = runtimeTelemetryVisible && Boolean(runtimeTelemetryTitle);
const showRuntimeTelemetryTooltip = Boolean(runtimeTelemetryTitle);
const rowTitle = showRuntimeTelemetryTooltip ? undefined : activityTitle;
const runtimeTelemetryTooltipTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [runtimeTelemetryTooltipOpen, setRuntimeTelemetryTooltipOpen] = useState(false);
@ -920,11 +913,7 @@ export const MemberCard = memo(function MemberCard({
}}
>
{!isRemoved ? (
<MemberRuntimeTelemetryStrip
runtimeEntry={runtimeEntry}
visible={runtimeTelemetryVisible}
scale={runtimeTelemetryScale}
/>
<MemberRuntimeTelemetryStrip runtimeEntry={runtimeEntry} scale={runtimeTelemetryScale} />
) : null}
<div className="pointer-events-none absolute inset-0 z-10 rounded transition-colors group-hover:bg-white/5" />
<div className="relative z-20 flex items-center gap-2.5">

View file

@ -45,8 +45,6 @@ interface MemberListProps {
isTeamProvisioning?: boolean;
leadActivity?: LeadActivityState;
launchParams?: TeamLaunchParams;
runtimeTelemetryVisible?: boolean;
onRuntimeTelemetryHoverChange?: (visible: boolean) => void;
onMemberClick?: (member: ResolvedTeamMember) => void;
onSendMessage?: (member: ResolvedTeamMember) => void;
onAssignTask?: (member: ResolvedTeamMember) => void;
@ -464,8 +462,6 @@ function areMemberListPropsEqual(
prev.isTeamAlive === next.isTeamAlive &&
prev.isTeamProvisioning === next.isTeamProvisioning &&
prev.leadActivity === next.leadActivity &&
prev.runtimeTelemetryVisible === next.runtimeTelemetryVisible &&
prev.onRuntimeTelemetryHoverChange === next.onRuntimeTelemetryHoverChange &&
prev.onRestartMember === next.onRestartMember &&
prev.onSkipMemberForLaunch === next.onSkipMemberForLaunch &&
areLaunchParamsEquivalent(prev.launchParams, next.launchParams)
@ -502,7 +498,6 @@ interface MemberCardRowProps {
isTeamProvisioning?: boolean;
leadActivity?: LeadActivityState;
isLaunchSettling?: boolean;
runtimeTelemetryVisible?: boolean;
runtimeTelemetryScale?: RuntimeTelemetryScale;
onOpenTask?: (taskId: string) => void;
onMemberClick?: (member: ResolvedTeamMember) => void;
@ -538,7 +533,6 @@ const MemberCardRow = memo(function MemberCardRow({
isTeamProvisioning,
leadActivity,
isLaunchSettling,
runtimeTelemetryVisible,
runtimeTelemetryScale,
onOpenTask,
onMemberClick,
@ -589,7 +583,6 @@ const MemberCardRow = memo(function MemberCardRow({
spawnLaunchState={spawnLaunchState}
spawnRuntimeAlive={spawnRuntimeAlive}
isLaunchSettling={isLaunchSettling}
runtimeTelemetryVisible={runtimeTelemetryVisible}
runtimeTelemetryScale={runtimeTelemetryScale}
onOpenTask={currentTask ? handleOpenTask : undefined}
onOpenReviewTask={reviewTask ? handleOpenReviewTask : undefined}
@ -714,8 +707,6 @@ export const MemberList = memo(function MemberList({
isTeamProvisioning,
leadActivity,
launchParams,
runtimeTelemetryVisible = false,
onRuntimeTelemetryHoverChange,
onMemberClick,
onSendMessage,
onAssignTask,
@ -742,14 +733,6 @@ export const MemberList = memo(function MemberList({
return () => observer.disconnect();
}, [handleResize]);
const handleRuntimeTelemetryMouseEnter = useCallback(() => {
onRuntimeTelemetryHoverChange?.(true);
}, [onRuntimeTelemetryHoverChange]);
const handleRuntimeTelemetryMouseLeave = useCallback(() => {
onRuntimeTelemetryHoverChange?.(false);
}, [onRuntimeTelemetryHoverChange]);
const gridClass = isWide ? 'grid grid-cols-2 gap-1' : 'grid grid-cols-1 gap-1';
const activeMembers = useMemo(
() =>
@ -941,12 +924,7 @@ export const MemberList = memo(function MemberList({
}
return (
<div
ref={containerRef}
className="flex flex-col gap-1"
onMouseEnter={handleRuntimeTelemetryMouseEnter}
onMouseLeave={handleRuntimeTelemetryMouseLeave}
>
<div ref={containerRef} className="runtime-telemetry-list flex flex-col gap-1">
<div className={gridClass}>
{activeMembers.map((member) => {
const spawnEntry = memberSpawnStatuses?.get(member.name);
@ -1017,7 +995,6 @@ export const MemberList = memo(function MemberList({
isTeamProvisioning={isTeamProvisioning}
leadActivity={leadActivity}
isLaunchSettling={isLaunchSettling}
runtimeTelemetryVisible={runtimeTelemetryVisible}
runtimeTelemetryScale={runtimeTelemetryScale}
onOpenTask={onOpenTask}
onMemberClick={onMemberClick}
@ -1063,7 +1040,6 @@ export const MemberList = memo(function MemberList({
isTeamProvisioning={isTeamProvisioning}
leadActivity={leadActivity}
isLaunchSettling={false}
runtimeTelemetryVisible={runtimeTelemetryVisible}
runtimeTelemetryScale={runtimeTelemetryScale}
onOpenTask={onOpenTask}
onMemberClick={onMemberClick}

View file

@ -1644,6 +1644,13 @@ a[href],
opacity: 0.96;
}
.runtime-telemetry-list:hover .runtime-telemetry-strip,
.runtime-telemetry-list:focus-within .runtime-telemetry-strip,
.runtime-telemetry-hover-scope:has(.runtime-telemetry-list:hover) .runtime-telemetry-legend,
.runtime-telemetry-hover-scope:has(.runtime-telemetry-list:focus-within) .runtime-telemetry-legend {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.kanban-comment-badge-pulse,
.message-composer-orbit-path,

View file

@ -800,7 +800,6 @@ describe('MemberCard starting-state visuals', () => {
],
updatedAt: '2026-04-24T12:00:05.000Z',
},
runtimeTelemetryVisible: true,
runtimeTelemetryScale: {
cpuCapPercent: 100,
memoryCapBytes: 512 * 1024 * 1024,
@ -861,7 +860,6 @@ describe('MemberCard starting-state visuals', () => {
resourceHistory: 'not-an-array',
updatedAt: '2026-04-24T12:00:05.000Z',
} as any,
runtimeTelemetryVisible: true,
isTeamAlive: true,
isTeamProvisioning: false,
})
@ -911,7 +909,6 @@ describe('MemberCard starting-state visuals', () => {
],
updatedAt: '2026-04-24T12:00:05.000Z',
} as any,
runtimeTelemetryVisible: true,
isTeamAlive: true,
isTeamProvisioning: false,
})