Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { groupDecisions, shouldShowArchitectureLink, KIND_ORDER } from '../components/decisionsPanelUtils';
import { groupDecisions, shouldShowArchitectureLink, KIND_ORDER, KIND_TITLES, KIND_ICON } from '../components/decisionsPanelUtils';

// This module's vitest env is 'node' (no jsdom), so <DecisionsPanel> itself can't be rendered here —
// see admin/__tests__/project-avatar.test.ts for the same constraint. Instead we pin the pure
Expand Down Expand Up @@ -39,6 +39,24 @@ describe('groupDecisions', () => {
});
});

// Regression guard for issue #66: every kind that can be grouped must have both a title and an
// icon defined, or the panel would render a blank header / missing icon for that kind's rows.
describe('KIND_TITLES / KIND_ICON completeness', () => {
it('defines a non-empty title and icon spec for every kind in KIND_ORDER', () => {
for (const kind of KIND_ORDER) {
expect(typeof KIND_TITLES[kind]).toBe('string');
expect(KIND_TITLES[kind].length).toBeGreaterThan(0);

const iconSpec = KIND_ICON[kind];
expect(iconSpec).toBeDefined();
expect(typeof iconSpec.icon).toBe('string');
expect(iconSpec.icon.length).toBeGreaterThan(0);
expect(typeof iconSpec.className).toBe('string');
expect(iconSpec.className.length).toBeGreaterThan(0);
}
});
});

describe('shouldShowArchitectureLink', () => {
it('shows the link for awaiting_architecture with a resolved commit url', () => {
expect(shouldShowArchitectureLink('awaiting_architecture', { architecture_commit_url: 'https://github.com/x/y/commit/abc' })).toBe(true);
Expand Down
25 changes: 24 additions & 1 deletion modules/software-engineer/admin/components/DecisionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,31 @@
* decision yet (classifyDecision()-only label) keeps the old click-to-open-run behavior.
*/
import React, { useCallback, useEffect, useState } from 'react';
import {
DocumentTextIcon,
BuildingLibraryIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
XCircleIcon,
Cog6ToothIcon,
} from '@heroicons/react/24/outline';
import { supabase } from '@/lib/supabase';
import { fmtCost, formatDuration } from './overview-filters';
import { ProjectAvatar } from './ProjectAvatar';
import { runLabel } from './RunListSection';
import { KIND_TITLES, groupDecisions, shouldShowArchitectureLink } from './decisionsPanelUtils';
import { KIND_ICON, KIND_TITLES, groupDecisions, shouldShowArchitectureLink } from './decisionsPanelUtils';

// Maps the string icon names in KIND_ICON (decisionsPanelUtils.ts) to their actual components —
// kept here rather than in decisionsPanelUtils.ts so that pure module stays free of the heroicons
// import for its `node`-environment test target.
const ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: string }>> = {
DocumentTextIcon,
BuildingLibraryIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
XCircleIcon,
Cog6ToothIcon,
};

// Absolute API base on deployed admins (nginx serves the SPA only — no /api proxy); '' locally → Vite proxy.
const API = `${(import.meta as unknown as { env: Record<string, string | undefined> }).env.VITE_API_URL ?? ''}/api/modules/software-engineer/admin`;
Expand Down Expand Up @@ -222,11 +242,14 @@ export default function DecisionsPanel({ projectFilter, onOpenRun }: {
{rows.map((d) => {
const showProposalLink = shouldShowArchitectureLink(kind, d);
const interactive = Boolean(d.decisionId) && !d.answered;
const iconSpec = KIND_ICON[kind];
const KindIcon = iconSpec && ICON_COMPONENTS[iconSpec.icon];
const label = (
<div
className="flex items-center gap-2 min-w-0 cursor-pointer"
onClick={() => onOpenRun?.(d.id)}
>
{KindIcon && <KindIcon className={`size-4 shrink-0 ${iconSpec.className}`} />}
<ProjectAvatar emoji={d.project?.avatar_emoji} className="size-4 shrink-0" />
<div className="min-w-0">
<div className="truncate text-sm text-[var(--gray-12)]">{runLabel(d)}</div>
Expand Down
9 changes: 5 additions & 4 deletions modules/software-engineer/admin/components/OverviewView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ export default function OverviewView({ onGoToSetup, onOpenRuns, onOpenRun }: {

return (
<div className="space-y-6">
{/* Every run parked waiting on a human, disambiguated + plain-language, with a deep link to
act (issue #49; moved to top of page per issue #66 so the panel an admin opens this page
FOR is the first thing they see). Renders nothing when nothing is gated. */}
<DecisionsPanel projectFilter={projectFilter} onOpenRun={onOpenRun} />

{/* One panel per env profile — each with its own status + full PR-set
builder; each hides itself when its control channel is absent. The
lfx panel is the Environments section: primary card + one card per
Expand Down Expand Up @@ -260,10 +265,6 @@ export default function OverviewView({ onGoToSetup, onOpenRuns, onOpenRun }: {
is the page operators watch; renders nothing when there is nothing to review. */}
<PendingApprovals projects={projects} />

{/* Every run parked waiting on a human, disambiguated + plain-language, with a deep link to
act (issue #49). Renders nothing when nothing is gated. */}
<DecisionsPanel projectFilter={projectFilter} onOpenRun={onOpenRun} />

{loading && !data ? (
<div className="flex justify-center p-12"><LoadingSpinner /></div>
) : (totals.runs ?? 0) === 0 ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,25 @@
export const KIND_ORDER = ['awaiting_spec', 'awaiting_architecture', 'ready_to_submit', 'review_blocked', 'pr_closed_partial', 'config_blocked'] as const;

export const KIND_TITLES: Record<string, string> = {
awaiting_spec: 'Awaiting spec approval',
awaiting_architecture: 'Architecture review',
ready_to_submit: 'Ready to submit',
review_blocked: 'Review blocked',
pr_closed_partial: 'PR closed without merging',
config_blocked: 'Blocked on configuration',
awaiting_spec: 'Spec needs your approval',
awaiting_architecture: 'Architecture proposal needs your review',
ready_to_submit: 'Pull request ready to submit',
review_blocked: 'Spec rejected by reviewer',
pr_closed_partial: 'Pull request closed without merging',
config_blocked: 'Blocked — needs a config fix',
};

// Per-kind icon (issue #66 §1) — decoration alongside the existing text label (title + row text),
// never a substitute for it, per this module's own colour-blind-safe convention. The icon is stored
// as a string name (not imported here) so this file keeps its deliberate zero-heroicons footprint
// for the `node`-environment test target; DecisionsPanel.tsx owns the name → component lookup.
export const KIND_ICON: Record<string, { icon: string; className: string }> = {
awaiting_spec: { icon: 'DocumentTextIcon', className: 'text-blue-600' },
awaiting_architecture: { icon: 'BuildingLibraryIcon', className: 'text-purple-600' },
ready_to_submit: { icon: 'CheckCircleIcon', className: 'text-green-600' },
review_blocked: { icon: 'ExclamationTriangleIcon', className: 'text-amber-600' },
pr_closed_partial: { icon: 'XCircleIcon', className: 'text-red-600' },
config_blocked: { icon: 'Cog6ToothIcon', className: 'text-[var(--gray-9)]' },
};

/** Buckets decisions by kind and orders the buckets per KIND_ORDER, dropping empty kinds. */
Expand Down
2 changes: 1 addition & 1 deletion modules/software-engineer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const softwareEngineerModule: GatewazeModule = {
name: 'Software Engineer',
description:
'Autonomous engineering agent: labelled GitHub issues → spec → adversarial review → implement → verify → PR/merge, driven by in-process Claude Code sessions. Per-brand repos + credentials; live agent monitoring.',
version: '0.1.0',
version: '0.2.0',

features: ['software-engineer'],

Expand Down
Loading