Files
PaperClipAI/ui/src/hooks/useAgentOrder.ts
T
DottaandPaperclip 841742fc1a [codex] Graduate experimental conference room defaults (#8628)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI has been graduating experimental conference-room task
experiences into the default issue and onboarding flows
> - Several task UI improvements were still coupled to the Conference
Room Chat experimental flag even though they are useful outside chat
itself
> - That coupling meant disabling chat also reverted unrelated defaults
such as work-mode labels, task status colors, team creation copy, and
the graduated issue thread
> - This pull request keeps chat-specific gating scoped to chat while
making the graduated task UI the default experience
> - The benefit is that operators can use the newer task workflows
without needing to enable the separate chat experiment

## Linked Issues or Issue Description

No public GitHub issue exists for this exact change.

### Problem or motivation

The Conference Room Chat experimental flag was controlling unrelated
task UI defaults, which made non-chat workflows regress when chat was
disabled.

### Proposed solution

Remove that flag from task-thread, work-mode, onboarding, status, and
team-creation presentation paths while leaving chat-specific behavior
separately gated.

### Alternatives considered

Keeping the flag as a broad umbrella until chat graduates would avoid a
behavior change, but it keeps unrelated UI improvements hidden behind
the wrong capability switch.

### Roadmap alignment

Checked `ROADMAP.md`; this is focused graduation/polish for existing UI
surfaces rather than a new roadmap-level core feature.

Related search:

- Searched open PRs and issues for `conference room chat experimental
flag`: no matches.
- Searched open PRs and issues for `graduated issue thread`: no matches.

## What Changed

- Removes Conference Room Chat flag branching from task-thread
rendering, work-mode labels, task status colors, and team creation copy.
- Makes the onboarding completion path create/reuse an onboarding
project, create the first assigned task, and send the user to the
dashboard instead of chat.
- Deletes the legacy onboarding wizard and classic task-thread files now
that the graduated flow is the default.
- Updates focused UI tests and affected E2E specs for the default task
experience.
- Adds a user-visible onboarding error if restored state is missing the
company or agent required for launch.

## Verification

- `pnpm run preflight:workspace-links && pnpm exec vitest run
ui/src/components/NewIssueDialog.test.tsx
ui/src/components/OnboardingWizardVariant.test.tsx
ui/src/components/RunChatSurface.test.tsx
ui/src/components/SidebarCompanyMenu.test.tsx
ui/src/components/StatusBadge.test.tsx ui/src/lib/agent-order.test.ts
ui/src/lib/onboarding-launch.test.ts ui/src/lib/work-mode-meta.test.ts
ui/src/pages/IssueDetail.test.tsx`
- `pnpm --filter @paperclipai/ui typecheck`
- `npx playwright test --config tests/e2e/playwright.config.ts --list
tests/e2e/conference-room-typing-intro.spec.ts
tests/e2e/planning-mode-visual-verification.spec.ts`
- Attempted targeted Playwright execution locally, but this host is
missing Chromium system libraries (`libatk1.0-0t64`, `libatspi2.0-0t64`,
`libxcomposite1`, `libxdamage1`, `libxfixes3`, `libxrandr2`, `libgbm1`,
`libasound2t64`). CI runs the specs in the proper Actions environment.

## Risks

- Medium UI behavior risk: this intentionally changes the default
experience for users who have not enabled Conference Room Chat.
- Medium onboarding risk: completion now creates/reuses a project and
creates the first task instead of only navigating.
- Low migration risk: no database schema or migration changes are
included.
- The PR avoids `pnpm-lock.yaml` and `.github/workflows` changes.

## Model Used

OpenAI Codex, GPT-5-class coding model, tool-enabled local repository
workflow with shell, git, and test execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-25 11:52:36 -05:00

110 lines
3.4 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import type { Agent } from "@paperclipai/shared";
import {
AGENT_ORDER_UPDATED_EVENT,
getAgentOrderStorageKey,
readAgentOrder,
sortAgentsByStoredOrder,
writeAgentOrder,
type AgentSidebarOrderOptions,
} from "../lib/agent-order";
type UseAgentOrderParams = {
agents: Agent[];
companyId: string | null | undefined;
userId: string | null | undefined;
};
type AgentOrderUpdatedDetail = {
storageKey: string;
orderedIds: string[];
};
function areEqual(a: string[], b: string[]) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) return false;
}
return true;
}
function buildOrderIds(agents: Agent[], orderedIds: string[], options: AgentSidebarOrderOptions) {
return sortAgentsByStoredOrder(agents, orderedIds, options).map((agent) => agent.id);
}
export function useAgentOrder({ agents, companyId, userId }: UseAgentOrderParams) {
const sortOptions = useMemo<AgentSidebarOrderOptions>(
() => ({ leadershipFirst: true }),
[],
);
const storageKey = useMemo(() => {
if (!companyId) return null;
return getAgentOrderStorageKey(companyId, userId);
}, [companyId, userId]);
const [orderedIds, setOrderedIds] = useState<string[]>(() => {
if (!storageKey) return agents.map((agent) => agent.id);
return buildOrderIds(agents, readAgentOrder(storageKey), sortOptions);
});
useEffect(() => {
const nextIds = storageKey
? buildOrderIds(agents, readAgentOrder(storageKey), sortOptions)
: agents.map((agent) => agent.id);
setOrderedIds((current) => (areEqual(current, nextIds) ? current : nextIds));
}, [agents, storageKey, sortOptions]);
useEffect(() => {
if (!storageKey) return;
const syncFromIds = (ids: string[]) => {
const nextIds = buildOrderIds(agents, ids, sortOptions);
setOrderedIds((current) => (areEqual(current, nextIds) ? current : nextIds));
};
const onStorage = (event: StorageEvent) => {
if (event.key !== storageKey) return;
syncFromIds(readAgentOrder(storageKey));
};
const onCustomEvent = (event: Event) => {
const detail = (event as CustomEvent<AgentOrderUpdatedDetail>).detail;
if (!detail || detail.storageKey !== storageKey) return;
syncFromIds(detail.orderedIds);
};
window.addEventListener("storage", onStorage);
window.addEventListener(AGENT_ORDER_UPDATED_EVENT, onCustomEvent);
return () => {
window.removeEventListener("storage", onStorage);
window.removeEventListener(AGENT_ORDER_UPDATED_EVENT, onCustomEvent);
};
}, [agents, storageKey, sortOptions]);
const orderedAgents = useMemo(
() => sortAgentsByStoredOrder(agents, orderedIds, sortOptions),
[agents, orderedIds, sortOptions],
);
const persistOrder = useCallback(
(ids: string[]) => {
const idSet = new Set(agents.map((agent) => agent.id));
const filtered = ids.filter((id) => idSet.has(id));
for (const agent of sortAgentsByStoredOrder(agents, [], sortOptions)) {
if (!filtered.includes(agent.id)) filtered.push(agent.id);
}
setOrderedIds((current) => (areEqual(current, filtered) ? current : filtered));
if (storageKey) {
writeAgentOrder(storageKey, filtered);
}
},
[agents, storageKey, sortOptions],
);
return {
orderedAgents,
orderedIds,
persistOrder,
};
}