Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ DASHBOARD_PORT=4444
# Set to true in a production build only when the surface is ready to expose.
# VITE_CODEUX_FEATURE_NODES=true
# VITE_CODEUX_FEATURE_CUSTOM_DASHBOARDS=true
# VITE_CODEUX_FEATURE_CHAT_NODES_WORKFLOW_QUICK_ACTION=true
# VITE_CODEUX_FEATURE_CHAT_CUSTOM_DASHBOARD_QUICK_ACTION=true
19 changes: 10 additions & 9 deletions dashboard/src/v2/BrowserPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { CollapsiblePanel } from "./components/ui/CollapsiblePanel.js";
import { PageContainer } from "./components/layout/PageContainer.js";
import { PageHeader } from "./components/layout/PageHeader.js";
import { getSafeUrl } from "./lib/safe-url.js";
import { AvantgardeSelect } from "./components/ui/AvantgardeSelect.js";

const PREVIEW_MESSAGE_TYPE = "sprint-preview:state";
const PREVIEW_NAVIGATION_TYPE = "sprint-preview:navigate";
Expand Down Expand Up @@ -1015,24 +1016,24 @@ export const BrowserPage: FunctionComponent = () => {
<p className="mt-1 text-xs leading-relaxed text-slate-500 dark:text-slate-400">
Override Docker access for {visibleSelectedSession?.sprintName || "the selected preview container"}.
</p>
<select
<AvantgardeSelect
id="preview-container-docker-access"
aria-label="Selected container Docker access policy"
value={visibleSelectedSession?.dockerAccessOverride === null || visibleSelectedSession?.dockerAccessOverride === undefined
? "inherit"
: visibleSelectedSession.dockerAccessOverride ? "enabled" : "disabled"}
disabled={!visibleSelectedSession || savingDockerAccessScope !== null}
aria-busy={savingDockerAccessScope === "session"}
onChange={(event) => {
const value = (event.currentTarget as HTMLSelectElement).value;
onChange={(value) => {
void handleSetSessionDockerAccess(value === "inherit" ? null : value === "enabled");
}}
className="mt-3 h-10 w-full rounded-xl border border-black/[0.1] bg-white px-3 text-sm font-semibold text-slate-700 outline-none transition focus:border-signal-500 focus:ring-2 focus:ring-signal-500/20 disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/[0.1] dark:bg-slate-900 dark:text-slate-200"
>
<option value="inherit">Use project default ({projectDockerAccessEnabled ? "enabled" : "disabled"})</option>
<option value="enabled">Enable for this container</option>
<option value="disabled">Disable for this container</option>
</select>
className="mt-3 w-full"
options={[
{ value: "inherit", label: `Use project default (${projectDockerAccessEnabled ? "enabled" : "disabled"})` },
{ value: "enabled", label: "Enable for this container" },
{ value: "disabled", label: "Disable for this container" },
]}
/>
</div>

<div className="flex gap-2 rounded-2xl border border-status-amber/30 bg-status-amber/10 px-3 py-3 text-xs leading-relaxed text-amber-900 dark:text-amber-200" role="note">
Expand Down
19 changes: 8 additions & 11 deletions dashboard/src/v2/FileBrowserPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { FileTree } from "./components/file-browser/FileTree.js";
import { FileViewer } from "./components/file-browser/FileViewer.js";
import { ChangesList } from "./components/file-browser/ChangesList.js";
import { DiffViewer } from "./components/file-browser/DiffViewer.js";
import { AvantgardeSelect } from "./components/ui/AvantgardeSelect.js";

type BrowserMode = "files" | "changes";
type FileBrowserAction = "rebuild" | "stop";
Expand Down Expand Up @@ -726,22 +727,18 @@ const LaunchPanel: FunctionComponent<LaunchPanelProps> = ({

<div class="flex w-full flex-col items-stretch gap-2 sm:flex-row">
<label class="sr-only" htmlFor="file-browser-launch-sprint">Sprint to browse</label>
<select
<AvantgardeSelect
id="file-browser-launch-sprint"
value={launchSprintId}
onChange={(event) => onLaunchSprintChange((event.currentTarget as HTMLSelectElement).value)}
onChange={onLaunchSprintChange}
disabled={launching}
aria-describedby={launchDisabledReason ? "file-browser-launch-status" : undefined}
title={launching ? "The file browser container is starting." : "Choose the sprint to browse"}
class="h-11 flex-1 rounded-2xl border border-black/[0.08] bg-white/85 px-4 text-sm font-medium text-slate-700 outline-none transition focus:border-signal-500/40 disabled:cursor-not-allowed disabled:border-black/[0.06] disabled:bg-black/[0.03] disabled:text-slate-400 disabled:opacity-100 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-slate-200 dark:disabled:bg-white/[0.02] dark:disabled:text-slate-500"
>
{sprints.length === 0 && <option value="">No sprints available</option>}
{sprints.map((sprint) => (
<option key={sprint.id} value={sprint.id}>
{sprint.name}
</option>
))}
</select>
className="flex-1"
options={sprints.length === 0
? [{ value: "", label: "No sprints available" }]
: sprints.map((sprint) => ({ value: sprint.id, label: sprint.name }))}
/>
<button
type="button"
onClick={onLaunch}
Expand Down
30 changes: 15 additions & 15 deletions dashboard/src/v2/KnowledgePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useProjectData } from "./context/project-data.js";
import { listEmbeddingModels } from "./lib/memory-api.js";
import { fetchAgentPresets } from "./lib/agent-preset-api.js";
import type { AgentPreset, Source } from "./types.js";
import { AvantgardeSelect } from "./components/ui/AvantgardeSelect.js";
import {
fetchKnowledgeDocuments,
addPastedDocument,
Expand Down Expand Up @@ -423,14 +424,16 @@ const KnowledgeSearchBox: FunctionComponent<{ projectId: string; agentPresets: A
className="w-full rounded-xl border border-black/[0.08] bg-white/70 py-2.5 pl-9 pr-3 text-sm text-slate-700 outline-none focus:border-signal-500/40 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-slate-200"
/>
</div>
<select
<AvantgardeSelect
value={agentId}
onChange={(e) => setAgentId((e.target as HTMLSelectElement).value)}
className="rounded-xl border border-black/[0.08] bg-white/70 px-3 py-2.5 text-sm font-semibold text-slate-600 outline-none dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-slate-300"
>
<option value="">Whole library</option>
{agentPresets.map((preset) => <option key={preset.id} value={preset.id}>{preset.name}'s docs</option>)}
</select>
onChange={setAgentId}
className="min-w-[12rem]"
aria-label="Knowledge owner"
options={[
{ value: "", label: "Whole library" },
...agentPresets.map((preset) => ({ value: preset.id, label: `${preset.name}'s docs` })),
]}
/>
<button type="button" onClick={run} disabled={searching || !query.trim()} className="inline-flex items-center gap-2 rounded-xl bg-signal-500/90 px-4 py-2.5 text-sm font-bold text-white hover:bg-signal-400 disabled:opacity-50 dark:text-void-900">
{searching ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" strokeWidth={2.5} />}
Search
Expand Down Expand Up @@ -616,15 +619,12 @@ const ProjectKnowledgeModal: FunctionComponent<{
return (
<ModalShell title="Import from project" onClose={onClose}>
<div className="flex flex-col gap-4">
<select
<AvantgardeSelect
value={sourceProjectId}
onChange={(e) => setSourceProjectId((e.target as HTMLSelectElement).value)}
className="rounded-xl border border-black/[0.08] bg-white/70 px-3 py-2.5 text-sm font-semibold text-slate-600 outline-none dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-slate-300"
>
{sourceProjects.map((project) => (
<option key={project.id} value={project.id}>{project.name}</option>
))}
</select>
onChange={setSourceProjectId}
aria-label="Project to import knowledge from"
options={sourceProjects.map((project) => ({ value: project.id, label: project.name }))}
/>

<div className="flex max-h-72 flex-col gap-2 overflow-y-auto rounded-2xl border border-black/[0.06] bg-black/[0.02] p-2 dark:border-white/[0.06] dark:bg-white/[0.02]">
{loadingDocs ? (
Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/v2/LiveSessionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ export const LiveSessionPage: FunctionComponent = () => {
</div>
</div>
) : (
taskCardItems.map(({ key, task, phase, taskTiming, events, invocations, ciPresentation, isRerunning, isForceCompleting, forceCompleteError, dispatchInfo }) => (
taskCardItems.map(({ key, task, phase, taskTiming, events, invocations, ciPresentation, humanIntervention, isRerunning, isForceCompleting, forceCompleteError, dispatchInfo }) => (
<LiveTaskCard
key={key}
task={task}
Expand All @@ -477,6 +477,7 @@ export const LiveSessionPage: FunctionComponent = () => {
events={events}
invocations={invocations}
ciPresentation={ciPresentation}
humanIntervention={humanIntervention}
onRerun={handleRerun}
onEdit={handleEditTask}
onForceComplete={handleForceCompleteTask}
Expand Down
15 changes: 8 additions & 7 deletions dashboard/src/v2/SchedulerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -979,14 +979,15 @@ export const SchedulerPage: FunctionComponent = () => {
{targetType === "memory_remediation" && (
<label className="block">
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-slate-400">Long-term remediation mode</span>
<select
<AvantgardeSelect
value={memoryRemediationMode}
onChange={(event) => setMemoryRemediationMode(event.currentTarget.value === "ai" ? "ai" : "deterministic")}
className={`mt-2 min-h-[44px] w-full ${SCHEDULER_FIELD_CLASS}`}
>
<option value="deterministic">Deterministic cleanup</option>
<option value="ai">AI-routed review</option>
</select>
onChange={(value) => setMemoryRemediationMode(value === "ai" ? "ai" : "deterministic")}
className="mt-2 w-full"
options={[
{ value: "deterministic", label: "Deterministic cleanup" },
{ value: "ai", label: "AI-routed review" },
]}
/>
<p className="mt-2 text-xs leading-relaxed text-slate-400">
Runs project-scoped long-term memory cleanup for CI-failure memories and exact duplicates. AI mode reviews candidates through the Remediation route.
</p>
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/v2/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ export const SettingsPage: FunctionComponent = () => {
aria-label="Settings category panel"
aria-busy={activeSaving || loading || resettingProject ? "true" : undefined}
data-motion-contract="enterExit"
className="flex min-w-0 flex-col gap-5 lg:col-start-2 lg:row-start-2"
className="flex min-w-0 self-start flex-col justify-start gap-5 lg:col-start-2 lg:row-start-2"
>
<div className="flex flex-col gap-3">
{loading ? (
Expand Down
5 changes: 4 additions & 1 deletion dashboard/src/v2/components/LiveTaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { MARKDOWN_PROSE_CLASS } from "./ui/MarkdownEditorField.js";
import { TaskStagePills } from "./SprintStatsDeck.js";
import { RuntimeEventFeed } from "./RuntimeEventFeed.js";
import { renderMarkdown } from "../../lib/markdown.js";
import type { Subtask, ExecutionRuntimeEventSummary, ExecutionInvocationRecord } from "../../types.js";
import type { Subtask, ExecutionAttentionItemSummary, ExecutionRuntimeEventSummary, ExecutionInvocationRecord } from "../../types.js";
import {
getTaskCfg,
} from "../lib/live-session-config.js";
Expand Down Expand Up @@ -50,6 +50,7 @@ export interface LiveTaskCardProps {
events?: ExecutionRuntimeEventSummary[];
invocations?: ExecutionInvocationRecord[];
ciPresentation?: CiStatusPresentation | null;
humanIntervention?: ExecutionAttentionItemSummary | null;
onRerun: (id: string, options?: RerunOptions) => void;
onEdit: (task: Subtask) => void;
onForceComplete: (task: Subtask) => void;
Expand All @@ -73,6 +74,7 @@ const LiveTaskCard: FunctionComponent<LiveTaskCardProps> = memo(({
events,
invocations = [],
ciPresentation = null,
humanIntervention = null,
onRerun,
onEdit,
onForceComplete,
Expand Down Expand Up @@ -292,6 +294,7 @@ const LiveTaskCard: FunctionComponent<LiveTaskCardProps> = memo(({
status={taskPhase}
review={task.latestReview}
ciPresentation={ciPresentation}
humanIntervention={humanIntervention}
compact
align="right"
/>
Expand Down
17 changes: 16 additions & 1 deletion dashboard/src/v2/components/TasksList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { SprintStreamRow } from "./ui/SprintStreamRow.js";
import { FilterStrip } from "./ui/FilterStrip.js";
import { SkeletonRow } from "./layout/SkeletonLoader.js";
import { EmptyState } from "./ui/EmptyState.js";
import { deriveActiveSprintIds, filterTasksToActiveSprints } from "../lib/overview-streams.js";
import {
deriveActiveSprintIds,
deriveOverviewTaskHumanInterventions,
deriveOverviewTaskCiPresentations,
filterTasksToActiveSprints,
} from "../lib/overview-streams.js";
import { useOverviewStreamActions } from "../hooks/use-overview-stream-actions.js";
import { useListReorder } from "../lib/motion/use-list-reorder.js";
import { useProgressiveList } from "../hooks/use-progressive-list.js";
Expand Down Expand Up @@ -35,6 +40,14 @@ export const TasksList: FunctionComponent<{ pageData: ReturnType<typeof import("
const sprintById = useMemo(() => new Map(sprints.map((sprint) => [sprint.id, sprint])), [sprints]);
const activeSprintIds = useMemo(() => deriveActiveSprintIds(sprints), [sprints]);
const activeTasks = useMemo(() => filterTasksToActiveSprints(tasks, activeSprintIds), [tasks, activeSprintIds]);
const taskCiPresentations = useMemo(
() => deriveOverviewTaskCiPresentations(activeTasks, execution),
[activeTasks, execution],
);
const taskHumanInterventions = useMemo(
() => deriveOverviewTaskHumanInterventions(activeTasks, execution),
[activeTasks, execution],
);

const filteredTasks = useMemo(() => activeTasks.filter(task => {
if (activeFilter === "All Tasks") return true;
Expand Down Expand Up @@ -131,6 +144,8 @@ export const TasksList: FunctionComponent<{ pageData: ReturnType<typeof import("
<TaskRow
task={task}
state={streamActions.getTaskState(task)}
ciPresentation={taskCiPresentations.get(task.recordId) ?? null}
humanIntervention={taskHumanInterventions.get(task.recordId) ?? null}
onPlayStop={() => streamActions.playStopTask(task)}
/>
</div>
Expand Down
23 changes: 8 additions & 15 deletions dashboard/src/v2/components/browser/LaunchContainerPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { FunctionComponent } from "preact";
import { Play, Loader2 } from "lucide-preact";
import type { Sprint } from "../../types.js";
import { AvantgardeSelect } from "../ui/AvantgardeSelect.js";
import { buildInteractionTransition } from "../../lib/motion/tokens.js";

interface LaunchContainerPanelProps {
Expand Down Expand Up @@ -64,27 +65,19 @@ export const LaunchContainerPanel: FunctionComponent<LaunchContainerPanelProps>
{statusMessage}
</div>
<label htmlFor="launch-container-sprint" className="sr-only">Sprint to preview</label>
<select
<AvantgardeSelect
id="launch-container-sprint"
value={launchSprintId}
onChange={(event) => onLaunchSprintChange((event.currentTarget as HTMLSelectElement).value)}
onChange={onLaunchSprintChange}
disabled={selectUnavailable}
aria-disabled={selectUnavailable}
aria-busy={launchBusy}
aria-describedby="launch-container-status"
title={selectUnavailable ? statusMessage : "Choose the sprint to preview"}
className={`w-full rounded-2xl border border-black/[0.08] bg-white/85 px-3 py-2.5 text-sm text-slate-700 outline-none transition focus:border-signal-500/40 dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-slate-200 ${
selectUnavailable ? "cursor-not-allowed opacity-50" : ""
}`}
style={{ transition: controlTransition }}
>
{sprints.length === 0 && <option value="">No sprints available</option>}
{sprints.map((sprint) => (
<option key={sprint.id} value={sprint.id}>
{sprint.name}
</option>
))}
</select>
className="w-full"
options={sprints.length === 0
? [{ value: "", label: "No sprints available" }]
: sprints.map((sprint) => ({ value: sprint.id, label: sprint.name }))}
/>

<button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ describe('ChatPage Accessibility', () => {
expect(window.location.search).not.toContain("draft=");
});

it('presents and dispatches the full eligible 3D chat quick action set without changing the composer', async () => {
it('presents and dispatches the default eligible 3D chat quick action set without changing the composer', async () => {
const user = userEvent.setup();
mocks.reducedMotion.value = true;
const handleSend = vi.fn(() => Promise.resolve());
Expand Down Expand Up @@ -564,7 +564,7 @@ describe('ChatPage Accessibility', () => {
const labels = [
"Create Web App", "Create Desktop App", "Create Onlineshop", "Create Portfolio", "Create Game",
"Status Report", "Sprint Progress", "What’s Failing?", "Plan Next Steps",
"Add Nodes Workflow", "Add Dashboard", "Create Skill", "List Skills",
"Create Skill", "List Skills",
];
expect(screen.getByRole("group", { name: "Project quick actions" })).toBeInTheDocument();
expect(screen.getByRole("group", { name: "Create quick actions" })).toBeInTheDocument();
Expand All @@ -573,6 +573,8 @@ describe('ChatPage Accessibility', () => {
expect(document.querySelector("[data-quick-action-group='insight']")).toHaveClass("md:flex", "md:flex-wrap");
expect(document.querySelector("[data-quick-action-group='workflow']")).toHaveClass("md:flex", "md:flex-wrap");
labels.forEach((label) => expect(screen.getByRole("button", { name: label })).toBeInTheDocument());
expect(screen.queryByRole("button", { name: "Add Nodes Workflow" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Add Dashboard" })).not.toBeInTheDocument();
const iconTiles = Array.from(document.querySelectorAll<HTMLElement>("[data-quick-action-icon]"));
expect(iconTiles).toHaveLength(labels.length);
expect(new Set(iconTiles.map((tile) => tile.className))).toHaveLength(labels.length);
Expand Down
Loading
Loading