Skip to content
Draft
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
39 changes: 39 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,42 @@ jobs:
working_directory: packages/shared
- store_test_results:
path: ./test-results
strict_typecheck_report:
docker:
- image: cimg/node:22.22
resource_class: large
steps:
- checkout
- restore_cache:
keys:
- deps-v7-{{ checksum "pnpm-lock.yaml" }}
- deps-v7-{{ .Branch }}
- run:
name: Strict Typecheck Report (non-blocking)
command: |
mkdir -p strict-typecheck
set +e

npm run typecheck:strict --prefix packages/shared > strict-typecheck/shared.log 2>&1
SHARED_EXIT=$?
npm run typecheck:strict --prefix packages/webapp > strict-typecheck/webapp.log 2>&1
WEBAPP_EXIT=$?
npm run typecheck:strict --prefix packages/extension > strict-typecheck/extension.log 2>&1
EXTENSION_EXIT=$?

echo "shared exit code: ${SHARED_EXIT}"
echo "webapp exit code: ${WEBAPP_EXIT}"
echo "extension exit code: ${EXTENSION_EXIT}"

for file in strict-typecheck/shared.log strict-typecheck/webapp.log strict-typecheck/extension.log; do
echo "--- ${file} ---"
grep -Eo 'error TS[0-9]+' "${file}" | sort | uniq -c | sort -nr | head -10 || true
echo "total errors: $(grep -Ec 'error TS[0-9]+' "${file}" || true)"
done

exit 0
- store_artifacts:
path: strict-typecheck
build_extension:
docker:
- image: cimg/node:22.22
Expand Down Expand Up @@ -136,6 +172,9 @@ workflows:
- test_shared:
requires:
- install_deps
- strict_typecheck_report:
requires:
- install_deps
- build_extension:
requires:
- install_deps
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"scripts": {
"prepare": "corepack enable || true",
"pnpm-version": "pnpm -v",
"typecheck": "pnpm --filter @dailydotdev/shared typecheck && pnpm --filter webapp typecheck && pnpm --filter extension typecheck",
"typecheck:strict": "pnpm --filter @dailydotdev/shared typecheck:strict && pnpm --filter webapp typecheck:strict && pnpm --filter extension typecheck:strict",
"typecheck:strict:report": "pnpm --filter @dailydotdev/shared typecheck:strict || true; pnpm --filter webapp typecheck:strict || true; pnpm --filter extension typecheck:strict || true",
"test:e2e": "pnpm --filter playwright test",
"test:e2e:headed": "pnpm --filter playwright test:headed",
"test:e2e:ui": "pnpm --filter playwright test:ui"
Expand Down
2 changes: 2 additions & 0 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"build": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome webpack",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0 --fix",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
"typecheck": "tsc -p tsconfig.json --noEmit",
"typecheck:strict": "tsc -p tsconfig.strict.json --noEmit",
"pretest": "npm run lint",
"test": "jest --runInBand"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/extension/tsconfig.strict.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"strict": true
}
}
2 changes: 2 additions & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"scripts": {
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix --max-warnings 0",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
"typecheck": "tsc -p tsconfig.json --noEmit",
"typecheck:strict": "tsc -p tsconfig.strict.json --noEmit",
"pretest": "npm run lint",
"test": "jest --runInBand"
},
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/components/layout/HeaderButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAuthContext } from '../../contexts/AuthContext';
import classed from '../../lib/classed';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { OpportunityEntryButton } from '../opportunity/OpportunityEntryButton';
import { AgentStatusIndicator } from '../../features/agentStatus/components/AgentStatusIndicator';

interface HeaderButtonsProps {
additionalButtons?: ReactNode;
Expand Down Expand Up @@ -40,6 +41,7 @@ export function HeaderButtons({
return (
<Container>
<OpportunityEntryButton />
<AgentStatusIndicator />
{additionalButtons}
<NotificationsBell />
<ProfileButton className="hidden laptop:flex" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import type { ReactElement } from 'react';
import React, { useEffect, useRef } from 'react';
import classNames from 'classnames';
import { Button, ButtonVariant } from '../../../components/buttons/Button';
import { TerminalIcon } from '../../../components/icons';
import { Tooltip } from '../../../components/tooltip/Tooltip';
import { SimpleTooltip } from '../../../components/tooltips/SimpleTooltip';
import { useInteractivePopup } from '../../../hooks/utils/useInteractivePopup';
import { useToastNotification } from '../../../hooks/useToastNotification';
import { useAgentStatus } from '../hooks/useAgentStatus';
import { AgentStatusType } from '../types';
import { AgentStatusPopup } from './AgentStatusPopup';

export function AgentStatusIndicator(): ReactElement {
const { agents, isLoading } = useAgentStatus();
const { displayToast } = useToastNotification();
const { isOpen, onUpdate, wrapHandler } = useInteractivePopup();
const previousStatusesRef = useRef<Map<string, AgentStatusType>>(new Map());
const initializedRef = useRef(false);

const activeAgents = agents.filter(
(agent) => agent.status !== AgentStatusType.Idle,
);
const waitingAgents = activeAgents.filter(
(agent) => agent.status === AgentStatusType.Waiting,
);
const completedAgents = activeAgents.filter(
(agent) => agent.status === AgentStatusType.Completed,
);
const errorAgents = activeAgents.filter(
(agent) => agent.status === AgentStatusType.Error,
);
const runningAgents = activeAgents.filter(
(agent) => agent.status === AgentStatusType.Working,
);

const hasAgents = activeAgents.length > 0;
const waitingCount = waitingAgents.length;
const errorCount = errorAgents.length;
const completedCount = completedAgents.length;
const runningCount = runningAgents.length;
const hasWaiting = waitingCount > 0;
const hasError = errorCount > 0;
const hasWorking = runningCount > 0;
const hasCompleted = completedCount > 0;

const formatCount = (count: number, copy: string): string => {
return `${count} agent${count > 1 ? 's' : ''} ${copy}`;
};

useEffect(() => {
if (!initializedRef.current) {
if (isLoading) {
return;
}

previousStatusesRef.current = new Map(
agents.map((agent) => [`${agent.name}:${agent.project}`, agent.status]),
);
initializedRef.current = true;
return;
}

const previousStatuses = previousStatusesRef.current;
const nextStatuses = new Map<string, AgentStatusType>();
const newWaitingAgents: string[] = [];
const newCompletedAgents: string[] = [];

agents.forEach((agent) => {
const agentId = `${agent.name}:${agent.project}`;
const previousStatus = previousStatuses.get(agentId);

nextStatuses.set(agentId, agent.status);

if (!previousStatus || previousStatus === agent.status) {
return;
}

if (agent.status === AgentStatusType.Waiting) {
newWaitingAgents.push(agent.name);
return;
}

if (agent.status === AgentStatusType.Completed) {
newCompletedAgents.push(agent.name);
}
});

previousStatusesRef.current = nextStatuses;

if (newWaitingAgents.length > 0) {
const firstAgent = newWaitingAgents[0];
const message =
newWaitingAgents.length === 1
? `${firstAgent} needs your input`
: `${newWaitingAgents.length} agents need your input`;

displayToast(message);
return;
}

if (newCompletedAgents.length > 0) {
const firstAgent = newCompletedAgents[0];
const message =
newCompletedAgents.length === 1
? `${firstAgent} is done`
: `${newCompletedAgents.length} agents are done`;

displayToast(message);
}
}, [agents, displayToast, isLoading]);

let dotColor = 'bg-text-disabled';
if (hasWaiting) {
dotColor = 'bg-status-warning';
} else if (hasError) {
dotColor = 'bg-status-error';
} else if (hasWorking) {
dotColor = 'bg-status-success';
} else if (hasCompleted) {
dotColor = 'bg-brand-default';
}

let tooltipContent = 'No active agents';
if (hasWaiting) {
tooltipContent = formatCount(waitingCount, 'need input');
} else if (hasError) {
tooltipContent = formatCount(errorCount, 'need attention');
} else if (hasWorking) {
tooltipContent = formatCount(runningCount, 'running');
} else if (hasAgents) {
tooltipContent = formatCount(completedCount, 'done');
}

return (
<SimpleTooltip
interactive
placement="bottom-end"
visible={isOpen}
onClickOutside={() => onUpdate(false)}
showArrow={false}
container={{
className:
'w-72 !rounded-14 border border-border-subtlest-tertiary !bg-accent-pepper-subtlest',
bgClassName: 'bg-accent-pepper-subtlest',
textClassName: 'text-text-primary',
paddingClassName: 'p-0',
}}
content={<AgentStatusPopup agents={activeAgents} />}
>
<div className="relative">
<Tooltip side="bottom" content={tooltipContent} visible={!isOpen}>
<Button
variant={ButtonVariant.Float}
className="w-10 justify-center"
icon={<TerminalIcon />}
onClick={wrapHandler(() => onUpdate(!isOpen))}
/>
</Tooltip>
{hasAgents && (
<span className="absolute -right-0.5 -top-0.5">
{hasWaiting && (
<span
className={classNames(
'absolute inset-0 h-2.5 w-2.5 rounded-full',
dotColor,
'animate-glow-ring',
)}
/>
)}
<span
className={classNames(
'relative block h-2.5 w-2.5 rounded-full border-2 border-background-default transition-colors duration-300',
dotColor,
hasWorking && !hasWaiting && !hasError && 'animate-breathe',
)}
/>
</span>
)}
{waitingCount > 0 && (
<span className="absolute -bottom-1 -right-1 min-w-4 rounded-6 bg-status-warning px-1 text-center font-bold text-text-primary typo-caption2">
<span className="relative flex items-center justify-center">
{waitingCount > 9 ? '9+' : waitingCount}
</span>
</span>
)}
</div>
</SimpleTooltip>
);
}
Loading