diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d64a1bcc..c27ee080 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -42,14 +42,32 @@ jobs:
VERSION="${GITHUB_REF#refs/tags/v}"
pnpm pkg set version="$VERSION"
+ - name: Verify Sentry release env
+ if: startsWith(github.ref, 'refs/tags/v')
+ env:
+ SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
+ SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
+ run: node ./scripts/ci/verify-sentry-release.cjs prebuild
+
- 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_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')
+ env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- SENTRY_ORG: quant-jump-pro
- SENTRY_PROJECT: electron
- run: pnpm build
+ SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
+ SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
+ run: node ./scripts/ci/verify-sentry-release.cjs postbuild
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
@@ -282,14 +300,32 @@ jobs:
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')
+ env:
+ SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
+ SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
+ run: node ./scripts/ci/verify-sentry-release.cjs prebuild
+
- 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_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')
+ env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- SENTRY_ORG: quant-jump-pro
- SENTRY_PROJECT: electron
- run: pnpm build
+ SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
+ SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
+ run: node ./scripts/ci/verify-sentry-release.cjs postbuild
- name: Verify packaged inputs (macOS ${{ matrix.arch }})
run: |
@@ -381,14 +417,32 @@ jobs:
node ./scripts/stage-runtime.mjs --platform win32-x64
}
+ - name: Verify Sentry release env (Windows)
+ if: startsWith(github.ref, 'refs/tags/v')
+ env:
+ SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
+ SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
+ run: node ./scripts/ci/verify-sentry-release.cjs prebuild
+
- 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_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')
+ env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- SENTRY_ORG: quant-jump-pro
- SENTRY_PROJECT: electron
- run: pnpm build
+ SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
+ SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
+ run: node ./scripts/ci/verify-sentry-release.cjs postbuild
- name: Verify packaged inputs (Windows)
shell: bash
@@ -483,14 +537,32 @@ jobs:
node ./scripts/stage-runtime.mjs --platform linux-x64
fi
+ - name: Verify Sentry release env (Linux)
+ if: startsWith(github.ref, 'refs/tags/v')
+ env:
+ SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
+ SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
+ run: node ./scripts/ci/verify-sentry-release.cjs prebuild
+
- 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_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')
+ env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- SENTRY_ORG: quant-jump-pro
- SENTRY_PROJECT: electron
- run: pnpm build
+ SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
+ SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
+ run: node ./scripts/ci/verify-sentry-release.cjs postbuild
- name: Verify packaged inputs (Linux)
run: |
diff --git a/.gitignore b/.gitignore
index c0876432..16f34a19 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,3 +65,4 @@ remotion/*
# Local reference captures
/agent-teams-reference-fix-*.png
+/.tmp-*
diff --git a/electron.vite.config.ts b/electron.vite.config.ts
index 9b69acad..1c6486c5 100644
--- a/electron.vite.config.ts
+++ b/electron.vite.config.ts
@@ -44,30 +44,42 @@ function nativeModuleStub(): Plugin {
}
}
-// Sentry source map upload — only active in CI when SENTRY_AUTH_TOKEN is set.
-const sentryPlugins = process.env.SENTRY_AUTH_TOKEN
- ? [
- sentryVitePlugin({
- org: process.env.SENTRY_ORG ?? 'quant-jump-pro',
- project: process.env.SENTRY_PROJECT ?? 'electron',
- authToken: process.env.SENTRY_AUTH_TOKEN,
- release: { name: `agent-teams-ai@${pkg.version}` },
- sourcemaps: {
- filesToDeleteAfterUpload: ['./out/renderer/**/*.map', './dist-electron/**/*.map'],
- },
- }),
- ]
- : []
+const sentrySourceMapTargets = {
+ main: {
+ assets: ['./dist-electron/main/**/*.{js,cjs,mjs,map}'],
+ filesToDeleteAfterUpload: ['./dist-electron/main/**/*.map'],
+ },
+ renderer: {
+ assets: ['./out/renderer/**/*.{js,cjs,mjs,map}'],
+ filesToDeleteAfterUpload: ['./out/renderer/**/*.map'],
+ },
+} as const
+
+// Sentry source map upload - only active in CI when SENTRY_AUTH_TOKEN is set.
+function createSentryPlugins(target: keyof typeof sentrySourceMapTargets): Plugin[] {
+ if (!process.env.SENTRY_AUTH_TOKEN) return []
+
+ return [
+ sentryVitePlugin({
+ org: process.env.SENTRY_ORG ?? 'quant-jump-pro',
+ project: process.env.SENTRY_PROJECT ?? 'electron',
+ authToken: process.env.SENTRY_AUTH_TOKEN,
+ telemetry: false,
+ release: { name: `agent-teams-ai@${pkg.version}` },
+ sourcemaps: sentrySourceMapTargets[target],
+ }) as Plugin,
+ ]
+}
export default defineConfig({
main: {
plugins: [
nativeModuleStub(),
- ...sentryPlugins,
+ ...createSentryPlugins('main'),
],
define: {
__APP_VERSION__: JSON.stringify(pkg.version),
- // Inject DSN at compile time — process.env.SENTRY_DSN is NOT available
+ // Inject DSN at compile time - process.env.SENTRY_DSN is NOT available
// at runtime in packaged Electron apps (only during CI build).
'process.env.SENTRY_DSN': JSON.stringify(process.env.SENTRY_DSN ?? ''),
},
@@ -148,10 +160,14 @@ export default defineConfig({
'@renderer': resolve(__dirname, 'src/renderer'),
'@shared': resolve(__dirname, 'src/shared'),
'@main': resolve(__dirname, 'src/main'),
+ '@radix-ui/react-compose-refs': resolve(
+ __dirname,
+ 'src/renderer/vendor/radixComposeRefs.ts'
+ ),
'@claude-teams/agent-graph': resolve(__dirname, 'packages/agent-graph/src/index.ts')
}
},
- plugins: [react(), ...sentryPlugins],
+ plugins: [react(), ...createSentryPlugins('renderer')],
build: {
sourcemap: 'hidden',
rollupOptions: {
diff --git a/landing/assets/styles/cyberpunk-hero.scss b/landing/assets/styles/cyberpunk-hero.scss
index 41b6bdf0..cc3ebccd 100644
--- a/landing/assets/styles/cyberpunk-hero.scss
+++ b/landing/assets/styles/cyberpunk-hero.scss
@@ -1259,17 +1259,17 @@
.cyber-feature-rail__reviewer-bubble {
--reviewer-bubble-center-shift: 3px;
+ --robot-bubble-position: absolute;
+ --robot-bubble-min-width: 112px;
+ --robot-bubble-max-width: 184px;
+ --robot-bubble-min-height: 46px;
+ --robot-bubble-font-size: 0.64rem;
+ --robot-bubble-padding: 8px 14px 16px;
left: auto;
top: auto;
right: calc(var(--reviewer-robot-width) / 2);
bottom: calc(100% + 10px);
- z-index: 6;
- width: max-content;
- max-width: 158px;
- white-space: normal;
- overflow-wrap: anywhere;
- text-wrap: balance;
transform: translateX(calc(50% + var(--reviewer-bubble-center-shift))) translate3d(0, 0, 0) rotate(-4deg);
transform-origin: center bottom;
animation: cyberFeatureReviewerSpeechFloat 2.8s ease-in-out 0.45s infinite;
diff --git a/landing/components/common/RobotSpeechBubble.vue b/landing/components/common/RobotSpeechBubble.vue
new file mode 100644
index 00000000..f8974ab5
--- /dev/null
+++ b/landing/components/common/RobotSpeechBubble.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/landing/components/hero/CyberHeroFeatureStrip.vue b/landing/components/hero/CyberHeroFeatureStrip.vue
index f29ca237..33e75cd5 100644
--- a/landing/components/hero/CyberHeroFeatureStrip.vue
+++ b/landing/components/hero/CyberHeroFeatureStrip.vue
@@ -67,13 +67,13 @@ const reviewerBubbleText = computed(() => {
aria-hidden="true"
>
-
{{ reviewerBubbleText }}
-
+
{{ heroReviewerFeatureCard.label }}
diff --git a/landing/components/layout/AppFooter.vue b/landing/components/layout/AppFooter.vue
index 17222bfc..396acddd 100644
--- a/landing/components/layout/AppFooter.vue
+++ b/landing/components/layout/AppFooter.vue
@@ -14,20 +14,9 @@ const docsHref = computed(() => {
)}
- {effectiveCliStatus.providers.length > 0 && (
+ {visibleEffectiveProviders.length > 0 && (
- {effectiveCliStatus.providers.map((provider) => (
+ {visibleEffectiveProviders.map((provider) => (
{
const statusText = effectiveShowSkeleton
? 'Checking...'
: formatProviderStatusText(provider);
+ const modelCatalogLoading =
+ provider.modelCatalogRefreshState === 'loading';
const connectionModeSummary = getProviderConnectionModeSummary(provider);
const credentialSummary = getProviderCredentialSummary(provider);
const disconnectAction = getProviderDisconnectAction(provider);
@@ -575,7 +585,10 @@ export const CliStatusSection = (): React.JSX.Element | null => {
{connectionModeSummary}
) : null}
{credentialSummary ? {credentialSummary} : null}
- {provider.models.length === 0 && (
+ {provider.models.length === 0 && modelCatalogLoading ? (
+ Loading models...
+ ) : null}
+ {provider.models.length === 0 && !modelCatalogLoading && (
Models unavailable for this runtime build
)}
diff --git a/src/renderer/components/sidebar/GlobalTaskList.tsx b/src/renderer/components/sidebar/GlobalTaskList.tsx
index 0af6fab8..de877535 100644
--- a/src/renderer/components/sidebar/GlobalTaskList.tsx
+++ b/src/renderer/components/sidebar/GlobalTaskList.tsx
@@ -1,5 +1,6 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { api, isElectronMode } from '@renderer/api';
import { confirm } from '@renderer/components/common/ConfirmDialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useCollapsedGroups } from '@renderer/hooks/useCollapsedGroups';
@@ -7,6 +8,7 @@ import { useTaskLocalState } from '@renderer/hooks/useTaskLocalState';
import { cn } from '@renderer/lib/utils';
import { markTaskUnread } from '@renderer/services/commentReadStorage';
import { useStore } from '@renderer/store';
+import { getCurrentProvisioningProgressForTeam } from '@renderer/store/slices/teamSlice';
import { normalizePath } from '@renderer/utils/pathNormalize';
import { projectColor } from '@renderer/utils/projectColor';
import {
@@ -16,6 +18,7 @@ import {
NO_PROJECT_KEY,
sortTasksByFreshness,
} from '@renderer/utils/taskGrouping';
+import { resolveTeamStatus } from '@renderer/utils/teamListStatus';
import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
import {
Archive,
@@ -191,6 +194,9 @@ export const GlobalTaskList = memo(function GlobalTaskList({
viewMode,
repositoryGroups,
teams,
+ provisioningRuns,
+ currentProvisioningRunIdByTeam,
+ leadActivityByTeam,
} = useStore(
useShallow((s) => ({
globalTasks: s.globalTasks,
@@ -202,6 +208,9 @@ export const GlobalTaskList = memo(function GlobalTaskList({
viewMode: s.viewMode,
repositoryGroups: s.repositoryGroups,
teams: s.teams,
+ provisioningRuns: s.provisioningRuns,
+ currentProvisioningRunIdByTeam: s.currentProvisioningRunIdByTeam,
+ leadActivityByTeam: s.leadActivityByTeam,
}))
);
@@ -217,6 +226,8 @@ export const GlobalTaskList = memo(function GlobalTaskList({
const [sortPopoverOpen, setSortPopoverOpen] = useState(false);
const [showArchived, setShowArchived] = useState(false);
const [renamingTaskKey, setRenamingTaskKey] = useState
(null);
+ const [aliveTeams, setAliveTeams] = useState([]);
+ const [aliveTeamsInitialized, setAliveTeamsInitialized] = useState(false);
const [projectRequestedVisibleCountByKey, setProjectRequestedVisibleCountByKey] = useState<
Record
>({});
@@ -224,6 +235,21 @@ export const GlobalTaskList = memo(function GlobalTaskList({
const hasFetchedRef = useRef(false);
const readState = useReadStateSnapshot();
const taskLocalState = useTaskLocalState();
+ const electronMode = isElectronMode();
+
+ const provisioningState = useMemo(
+ () => ({ currentProvisioningRunIdByTeam, provisioningRuns }),
+ [currentProvisioningRunIdByTeam, provisioningRuns]
+ );
+
+ const fetchAliveTeams = useCallback(async (): Promise => {
+ if (!electronMode || !api.teams?.aliveList) return null;
+ try {
+ return await api.teams.aliveList();
+ } catch {
+ return null;
+ }
+ }, [electronMode]);
// --- New-task animation tracking (same pattern as ChatHistory) ---
const knownTaskIdsRef = useRef>(new Set());
@@ -262,6 +288,70 @@ export const GlobalTaskList = memo(function GlobalTaskList({
[newTaskIds]
);
+ useEffect(() => {
+ let cancelled = false;
+ void fetchAliveTeams().then((list) => {
+ if (!cancelled && list) {
+ setAliveTeams(list);
+ setAliveTeamsInitialized(true);
+ }
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [fetchAliveTeams, teams]);
+
+ const readyProgressRefreshKey = useMemo(() => {
+ return Object.entries(currentProvisioningRunIdByTeam)
+ .map(([teamName, runId]) => {
+ if (!runId) return null;
+ const progress = provisioningRuns[runId];
+ return progress?.state === 'ready'
+ ? `${teamName}:${progress.runId}:${progress.updatedAt}`
+ : null;
+ })
+ .filter((item): item is string => Boolean(item))
+ .join('|');
+ }, [currentProvisioningRunIdByTeam, provisioningRuns]);
+
+ useEffect(() => {
+ if (!readyProgressRefreshKey) return;
+ let cancelled = false;
+ void fetchAliveTeams().then((list) => {
+ if (!cancelled && list) {
+ setAliveTeams(list);
+ setAliveTeamsInitialized(true);
+ }
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [fetchAliveTeams, readyProgressRefreshKey]);
+
+ const offlineTeamNames = useMemo(() => {
+ const result = new Set();
+ if (aliveTeamsInitialized) {
+ for (const team of teams) {
+ const status = resolveTeamStatus(
+ team,
+ team.teamName,
+ aliveTeams,
+ getCurrentProvisioningProgressForTeam(provisioningState, team.teamName),
+ leadActivityByTeam
+ );
+ if (status === 'offline') {
+ result.add(team.teamName);
+ }
+ }
+ }
+ for (const [teamName, activity] of Object.entries(leadActivityByTeam)) {
+ if (activity === 'offline') {
+ result.add(teamName);
+ }
+ }
+ return result;
+ }, [aliveTeams, aliveTeamsInitialized, leadActivityByTeam, provisioningState, teams]);
+
const setGroupingMode = (mode: TaskGroupingMode): void => {
setGroupingModeState(mode);
saveGroupingMode(mode);
@@ -561,6 +651,7 @@ export const GlobalTaskList = memo(function GlobalTaskList({
();
+const COMPACT_HIDDEN_INTERVAL_SCOPE_WARNINGS = new Set([
+ 'Task boundaries missing - scoped by workIntervals timestamps.',
+ 'Task start boundary missing - scoped by persisted workIntervals timestamps.',
+]);
function getChangeSetFiles(changeSet: TaskChangeSetV2 | null): FileChangeSummary[] {
if (!Array.isArray(changeSet?.files)) {
@@ -111,6 +115,23 @@ function getTaskSummaryBadge(changeSet: TaskChangeSetV2 | null): string | undefi
return undefined;
}
+function isWorkIntervalScopedFileChange(changeSet: TaskChangeSetV2): boolean {
+ const reason = changeSet.scope?.confidence?.reason;
+ return (
+ getChangeSetFiles(changeSet).length > 0 &&
+ changeSet.confidence === 'medium' &&
+ typeof reason === 'string' &&
+ reason.toLowerCase().includes('workinterval')
+ );
+}
+
+function shouldHideCompactDiagnostic(changeSet: TaskChangeSetV2, message: string): boolean {
+ return (
+ isWorkIntervalScopedFileChange(changeSet) &&
+ COMPACT_HIDDEN_INTERVAL_SCOPE_WARNINGS.has(message.trim())
+ );
+}
+
function getTaskChangeDiagnosticMessages(changeSet: TaskChangeSetV2): string[] {
const status = classifyTaskChangeReviewability(changeSet);
if (status.reviewability === 'unknown' || status.reviewability === 'none') {
@@ -120,7 +141,13 @@ function getTaskChangeDiagnosticMessages(changeSet: TaskChangeSetV2): string[] {
status.diagnostics.length > 0
? status.diagnostics.map((diagnostic) => diagnostic.message)
: getChangeSetWarnings(changeSet);
- return [...new Set(messages.filter((message) => message.trim().length > 0))];
+ return [
+ ...new Set(
+ messages.filter(
+ (message) => message.trim().length > 0 && !shouldHideCompactDiagnostic(changeSet, message)
+ )
+ ),
+ ];
}
export const TeamChangesSection = memo(function TeamChangesSection({
diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx
index 0400db05..294ad496 100644
--- a/src/renderer/components/team/TeamDetailView.tsx
+++ b/src/renderer/components/team/TeamDetailView.tsx
@@ -1397,6 +1397,7 @@ 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(null);
const [updatingRoleLoading, setUpdatingRoleLoading] = useState(false);
@@ -2877,20 +2878,35 @@ export const TeamDetailView = memo(function TeamDetailView({
icon={}
badge={activeTeammateCount === 0 ? 'Solo' : activeTeammateCount}
defaultOpen
+ afterBadge={
+
+ }
action={
-
-
+
+
+
+ Memory
+
+
+
+ CPU
+
}
contentWrapperClassName="-mx-[calc(1rem-5px)] w-[calc(100%+2rem-10px)]"
@@ -2907,6 +2923,8 @@ export const TeamDetailView = memo(function TeamDetailView({
isTeamAlive={data.isAlive}
isTeamProvisioning={isTeamProvisioning}
launchParams={launchParams}
+ runtimeTelemetryVisible={runtimeTelemetryPreviewVisible}
+ onRuntimeTelemetryHoverChange={setRuntimeTelemetryPreviewVisible}
onMemberClick={handleSelectMember}
onSendMessage={handleSendMessageToMember}
onAssignTask={handleAssignTaskToMember}
diff --git a/src/renderer/components/team/__tests__/useTeamChangesSummaries.test.tsx b/src/renderer/components/team/__tests__/useTeamChangesSummaries.test.tsx
index 1ca80c8a..44780ee4 100644
--- a/src/renderer/components/team/__tests__/useTeamChangesSummaries.test.tsx
+++ b/src/renderer/components/team/__tests__/useTeamChangesSummaries.test.tsx
@@ -234,6 +234,40 @@ function lowConfidenceFileResponse(): TeamTaskChangeSummariesResponse {
});
}
+function intervalScopedFileResponse(): TeamTaskChangeSummariesResponse {
+ return response({
+ ...changeSet(),
+ confidence: 'medium',
+ files: [
+ fileChange({
+ filePath: '/repo/791/calculator.js',
+ relativePath: '791/calculator.js',
+ }),
+ ],
+ totalFiles: 1,
+ totalLinesAdded: 162,
+ scope: {
+ ...changeSet().scope,
+ confidence: {
+ tier: 2,
+ label: 'medium',
+ reason: 'Scoped by persisted task workIntervals (timestamp-based)',
+ },
+ },
+ warnings: ['Task start boundary missing - scoped by persisted workIntervals timestamps.'],
+ });
+}
+
+function warningFileResponse(): TeamTaskChangeSummariesResponse {
+ return response({
+ ...changeSet(),
+ files: [fileChange()],
+ totalFiles: 1,
+ totalLinesAdded: 1,
+ warnings: ['Unexpected ledger warning.'],
+ });
+}
+
function invalidFileSummaryResponse(): TeamTaskChangeSummariesResponse {
return response({
...changeSet(),
@@ -751,6 +785,114 @@ describe('useTeamChangesSummaries', () => {
}
});
+ it('hides work-interval scoping advisories in the compact Changes list when files are present', async () => {
+ hoisted.getTeamTaskChangeSummaries.mockResolvedValue(intervalScopedFileResponse());
+
+ const scrollIntoViewDescriptor = Object.getOwnPropertyDescriptor(
+ Element.prototype,
+ 'scrollIntoView'
+ );
+ Object.defineProperty(Element.prototype, 'scrollIntoView', {
+ configurable: true,
+ value: vi.fn(),
+ });
+ try {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ root = createRoot(container);
+
+ await act(async () => {
+ root?.render(
+ React.createElement(
+ TooltipProvider,
+ null,
+ React.createElement(TeamChangesSection, {
+ teamName: 'team-a',
+ tasks: [task({ status: 'completed', owner: 'jack' })],
+ onOpenTask: vi.fn(),
+ onViewChanges: vi.fn(),
+ })
+ )
+ );
+ });
+
+ const expandButton = container.querySelector
(
+ 'button[aria-label="Expand section"]'
+ );
+ expect(expandButton).not.toBeNull();
+
+ await act(async () => {
+ expandButton?.click();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(container.textContent).toContain('791/calculator.js');
+ expect(container.textContent).not.toContain(
+ 'Task start boundary missing - scoped by persisted workIntervals timestamps.'
+ );
+ } finally {
+ if (scrollIntoViewDescriptor) {
+ Object.defineProperty(Element.prototype, 'scrollIntoView', scrollIntoViewDescriptor);
+ } else {
+ delete (Element.prototype as { scrollIntoView?: Element['scrollIntoView'] }).scrollIntoView;
+ }
+ }
+ });
+
+ it('keeps unrelated file warnings visible in the compact Changes list', async () => {
+ hoisted.getTeamTaskChangeSummaries.mockResolvedValue(warningFileResponse());
+
+ const scrollIntoViewDescriptor = Object.getOwnPropertyDescriptor(
+ Element.prototype,
+ 'scrollIntoView'
+ );
+ Object.defineProperty(Element.prototype, 'scrollIntoView', {
+ configurable: true,
+ value: vi.fn(),
+ });
+ try {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ root = createRoot(container);
+
+ await act(async () => {
+ root?.render(
+ React.createElement(
+ TooltipProvider,
+ null,
+ React.createElement(TeamChangesSection, {
+ teamName: 'team-a',
+ tasks: [task({ status: 'completed' })],
+ onOpenTask: vi.fn(),
+ onViewChanges: vi.fn(),
+ })
+ )
+ );
+ });
+
+ const expandButton = container.querySelector(
+ 'button[aria-label="Expand section"]'
+ );
+ expect(expandButton).not.toBeNull();
+
+ await act(async () => {
+ expandButton?.click();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(container.textContent).toContain('src/app.ts');
+ expect(container.textContent).toContain('Unexpected ledger warning.');
+ } finally {
+ if (scrollIntoViewDescriptor) {
+ Object.defineProperty(Element.prototype, 'scrollIntoView', scrollIntoViewDescriptor);
+ } else {
+ delete (Element.prototype as { scrollIntoView?: Element['scrollIntoView'] }).scrollIntoView;
+ }
+ }
+ });
+
it('does not clear completed task presence from an uncertain empty summary', async () => {
hoisted.getTeamTaskChangeSummaries.mockResolvedValue(response(quietNoLogChangeSet()));
diff --git a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx
index dc8d38b4..a0be7dfe 100644
--- a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx
+++ b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx
@@ -147,6 +147,7 @@ type ProvisioningDetailSummary =
| 'Selected model unavailable'
| 'Selected model verification timed out'
| 'Selected model check failed'
+ | 'Selected model verification deferred'
| 'Selected model ping not confirmed'
| 'Ready with notes'
| 'Needs attention';
@@ -163,6 +164,7 @@ function isFormattedModelDetail(lower: string): boolean {
lower.includes(' - compatible, deep verification pending') ||
lower.includes(' - unavailable') ||
lower.includes(' - check failed') ||
+ lower.includes(' - verification deferred') ||
lower.includes(' - ping not confirmed')
);
}
@@ -244,6 +246,9 @@ function summarizeDetail(
if (isSelectedModelDetail(lower) && lower.includes('could not be verified')) {
return 'Selected model check failed';
}
+ if (isSelectedModelDetail(lower) && lower.includes('verification deferred')) {
+ return 'Selected model verification deferred';
+ }
if (lower.includes(' - verified')) {
return 'Selected model verified';
}
@@ -259,6 +264,9 @@ function summarizeDetail(
if (lower.includes(' - check failed -')) {
return 'Selected model check failed';
}
+ if (lower.includes(' - verification deferred')) {
+ return 'Selected model verification deferred';
+ }
if (lower.includes(' - ping not confirmed')) {
return 'Selected model ping not confirmed';
}
@@ -279,6 +287,7 @@ function getModelDetailSummary(details: string[]): string | null {
let unavailableCount = 0;
let timedOutCount = 0;
let checkFailedCount = 0;
+ let deferredCount = 0;
let pingNotConfirmedCount = 0;
let checkingCount = 0;
@@ -327,6 +336,13 @@ function getModelDetailSummary(details: string[]): string | null {
checkFailedCount += 1;
continue;
}
+ if (
+ lower.includes(' - verification deferred') ||
+ (isSelectedModelDetail(lower) && lower.includes('verification deferred'))
+ ) {
+ deferredCount += 1;
+ continue;
+ }
if (lower.includes(' - ping not confirmed')) {
pingNotConfirmedCount += 1;
continue;
@@ -346,6 +362,9 @@ function getModelDetailSummary(details: string[]): string | null {
if (timedOutCount > 0) {
parts.push(`${timedOutCount} model${timedOutCount === 1 ? '' : 's'} timed out`);
}
+ if (deferredCount > 0) {
+ parts.push(`${deferredCount} verification deferred`);
+ }
if (pingNotConfirmedCount > 0) {
parts.push(`${pingNotConfirmedCount} ping not confirmed`);
}
diff --git a/src/renderer/components/team/dialogs/TeamModelSelector.tsx b/src/renderer/components/team/dialogs/TeamModelSelector.tsx
index 7bb88de6..605a364e 100644
--- a/src/renderer/components/team/dialogs/TeamModelSelector.tsx
+++ b/src/renderer/components/team/dialogs/TeamModelSelector.tsx
@@ -513,6 +513,48 @@ const OpenCodeVirtualizedModelGrid = ({
);
};
+const OpenCodeModelCatalogLoadingSkeleton = (): React.JSX.Element => (
+
+
+
+
+ Loading OpenCode models...
+
+
+
+ {[0, 1, 2].map((index) => (
+
+ ))}
+
+
+);
+
export interface TeamModelSelectorProps {
providerId: TeamProviderId;
onProviderChange: (providerId: TeamProviderId) => void;
@@ -957,11 +999,18 @@ export const TeamModelSelector: React.FC = ({
const visibleConcreteModelOptionCount =
visibleModelOptions.length - visibleDefaultModelOptions.length;
const concreteModelOptionCount = modelOptions.filter((option) => option.value.trim()).length;
- const shouldShowModelSearch = concreteModelOptionCount > 8;
+ const shouldShowOpenCodeCatalogLoading =
+ effectiveProviderId === 'opencode' &&
+ runtimeProviderStatus?.modelCatalogRefreshState === 'loading' &&
+ runtimeProviderStatus.modelCatalog?.providerId !== 'opencode' &&
+ (runtimeProviderStatus.models.length === 0 ||
+ runtimeProviderStatus.models.every((model) => model.trim() === 'opencode/big-pickle'));
+ const shouldShowModelSearch = !shouldShowOpenCodeCatalogLoading && concreteModelOptionCount > 8;
const trimmedModelQuery = modelQuery.trim();
const shouldConstrainModelListHeight = visibleModelOptions.length > 8;
const shouldVirtualizeOpenCodeModels =
effectiveProviderId === 'opencode' &&
+ !shouldShowOpenCodeCatalogLoading &&
visibleConcreteModelOptionCount > OPENCODE_MODEL_VIRTUALIZATION_THRESHOLD;
const getModelAdvisoryBadgeLabel = (reason: string | null): string =>
reason?.toLowerCase().includes('ping not confirmed') ? 'Ping not confirmed' : 'Note';
@@ -1270,8 +1319,9 @@ export const TeamModelSelector: React.FC = ({
/>
) : null}
- {(effectiveProviderId === 'opencode' && openCodeSourceOptions.length > 1) ||
- hasRecommendedOpenCodeModels ? (
+ {!shouldShowOpenCodeCatalogLoading &&
+ ((effectiveProviderId === 'opencode' && openCodeSourceOptions.length > 1) ||
+ hasRecommendedOpenCodeModels) ? (
{effectiveProviderId === 'opencode' && openCodeSourceOptions.length > 1 ? (
= ({
) : null}
{effectiveProviderId === 'opencode' ? (
- shouldVirtualizeOpenCodeModels ? (
+ shouldShowOpenCodeCatalogLoading ? (
+
+ {visibleDefaultModelOptions.length > 0 ? (
+
+ {visibleDefaultModelOptions.map(renderModelOption)}
+
+ ) : null}
+
+
+ ) : shouldVirtualizeOpenCodeModels ? (
= ({
{visibleModelOptions.map(renderModelOption)}
)}
- {visibleModelOptions.length === 0 ? (
+ {visibleModelOptions.length === 0 && !shouldShowOpenCodeCatalogLoading ? (
{trimmedModelQuery
? 'No models match this search.'
diff --git a/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts b/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts
index bc744fc6..5940f46a 100644
--- a/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts
+++ b/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts
@@ -169,6 +169,7 @@ function stripSelectedModelPrefix(modelId: string, message: string): string {
const patterns = [
new RegExp(`^Selected model ${escapeRegExp(modelId)} is unavailable\\.\\s*`, 'i'),
new RegExp(`^Selected model ${escapeRegExp(modelId)} could not be verified\\.\\s*`, 'i'),
+ new RegExp(`^Selected model ${escapeRegExp(modelId)} verification deferred\\.\\s*`, 'i'),
new RegExp(`^Selected model ${escapeRegExp(modelId)} verified for launch\\.\\s*`, 'i'),
new RegExp(`^Selected model ${escapeRegExp(modelId)} is available for launch\\.\\s*`, 'i'),
new RegExp(
@@ -420,6 +421,17 @@ function buildModelFailureLine(
return reason ? `${label} - ${kind} - ${reason}` : `${label} - ${kind}`;
}
+function buildModelVerificationDeferredLine(
+ providerId: TeamProviderId,
+ modelId: string,
+ reason: string | null
+): string {
+ const label = getModelLabel(providerId, modelId);
+ return reason
+ ? `${label} - verification deferred - ${reason}`
+ : `${label} - verification deferred`;
+}
+
function createRuntimeDetailLines(result: TeamProvisioningPrepareResult): string[] {
return uniquePrepareLines([...(result.details ?? []), ...(result.warnings ?? [])]);
}
@@ -574,6 +586,18 @@ function resolveModelResultFromBatch(
};
}
+ const hasVerificationDeferredLine = modelScopedEntries.some((entry) =>
+ /selected model .* verification deferred\./i.test(entry)
+ );
+ if (hasVerificationDeferredLine) {
+ const line = buildModelVerificationDeferredLine(providerId, modelId, scopedReason);
+ return {
+ status: 'notes',
+ line,
+ warningLine: line,
+ };
+ }
+
const hasUnavailableLine = modelScopedEntries.some((entry) =>
/selected model .* is unavailable\./i.test(entry)
);
diff --git a/src/renderer/components/team/members/CurrentTaskIndicator.tsx b/src/renderer/components/team/members/CurrentTaskIndicator.tsx
index 220ce60c..5c7d7255 100644
--- a/src/renderer/components/team/members/CurrentTaskIndicator.tsx
+++ b/src/renderer/components/team/members/CurrentTaskIndicator.tsx
@@ -105,7 +105,11 @@ export const CurrentTaskIndicator = memo(
return (
-
+
{activityLabel}