feat(ui): reconcile backup UI changes with current routing and interaction features

This commit is contained in:
Dotta
2026-03-02 16:44:03 -06:00
parent 83be94361c
commit 8ee063c4e5
69 changed files with 1591 additions and 666 deletions

View File

@@ -13,13 +13,17 @@ import { companiesApi } from "../api/companies";
import { ApiError } from "../api/client";
import { queryKeys } from "../lib/queryKeys";
type CompanySelectionSource = "manual" | "route_sync" | "bootstrap";
type CompanySelectionOptions = { source?: CompanySelectionSource };
interface CompanyContextValue {
companies: Company[];
selectedCompanyId: string | null;
selectedCompany: Company | null;
selectionSource: CompanySelectionSource;
loading: boolean;
error: Error | null;
setSelectedCompanyId: (companyId: string) => void;
setSelectedCompanyId: (companyId: string, options?: CompanySelectionOptions) => void;
reloadCompanies: () => Promise<void>;
createCompany: (data: {
name: string;
@@ -34,24 +38,8 @@ const CompanyContext = createContext<CompanyContextValue | null>(null);
export function CompanyProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient();
const [selectedCompanyId, setSelectedCompanyIdState] = useState<string | null>(
() => {
// Check URL param first (supports "open in new tab" from company rail)
const urlParams = new URLSearchParams(window.location.search);
const companyParam = urlParams.get("company");
if (companyParam) {
localStorage.setItem(STORAGE_KEY, companyParam);
// Clean up the URL param
urlParams.delete("company");
const newSearch = urlParams.toString();
const newUrl =
window.location.pathname + (newSearch ? `?${newSearch}` : "");
window.history.replaceState({}, "", newUrl);
return companyParam;
}
return localStorage.getItem(STORAGE_KEY);
}
);
const [selectionSource, setSelectionSource] = useState<CompanySelectionSource>("bootstrap");
const [selectedCompanyId, setSelectedCompanyIdState] = useState<string | null>(() => localStorage.getItem(STORAGE_KEY));
const { data: companies = [], isLoading, error } = useQuery({
queryKey: queryKeys.companies.all,
@@ -83,11 +71,13 @@ export function CompanyProvider({ children }: { children: ReactNode }) {
const next = selectableCompanies[0]!.id;
setSelectedCompanyIdState(next);
setSelectionSource("bootstrap");
localStorage.setItem(STORAGE_KEY, next);
}, [companies, selectedCompanyId, sidebarCompanies]);
const setSelectedCompanyId = useCallback((companyId: string) => {
const setSelectedCompanyId = useCallback((companyId: string, options?: CompanySelectionOptions) => {
setSelectedCompanyIdState(companyId);
setSelectionSource(options?.source ?? "manual");
localStorage.setItem(STORAGE_KEY, companyId);
}, []);
@@ -121,6 +111,7 @@ export function CompanyProvider({ children }: { children: ReactNode }) {
companies,
selectedCompanyId,
selectedCompany,
selectionSource,
loading: isLoading,
error: error as Error | null,
setSelectedCompanyId,
@@ -131,6 +122,7 @@ export function CompanyProvider({ children }: { children: ReactNode }) {
companies,
selectedCompanyId,
selectedCompany,
selectionSource,
isLoading,
error,
setSelectedCompanyId,

View File

@@ -125,7 +125,7 @@ function resolveIssueToastContext(
}
const ISSUE_TOAST_ACTIONS = new Set(["issue.created", "issue.updated", "issue.comment_added"]);
const AGENT_TOAST_STATUSES = new Set(["running", "idle", "error"]);
const AGENT_TOAST_STATUSES = new Set(["running", "error"]);
const TERMINAL_RUN_STATUSES = new Set(["succeeded", "failed", "timed_out", "cancelled"]);
function describeIssueUpdate(details: Record<string, unknown> | null): string | null {
@@ -178,6 +178,10 @@ function buildActivityToast(
}
if (action === "issue.updated") {
if (details?.reopened === true && readString(details.source) === "comment") {
// Reopen-via-comment emits a paired comment event; show one combined toast on the comment event.
return null;
}
const changeDesc = describeIssueUpdate(details);
const body = changeDesc
? issue.title
@@ -197,13 +201,26 @@ function buildActivityToast(
const commentId = readString(details?.commentId);
const bodySnippet = readString(details?.bodySnippet);
const reopened = details?.reopened === true;
const reopenedFrom = readString(details?.reopenedFrom);
const reopenedLabel = reopened
? reopenedFrom
? `reopened from ${reopenedFrom.replace(/_/g, " ")}`
: "reopened"
: null;
const title = reopened ? `${actor} reopened and commented on ${issue.ref}` : `${actor} commented on ${issue.ref}`;
const body = bodySnippet
? reopenedLabel
? `${reopenedLabel} - ${bodySnippet.replace(/^#+\s*/m, "").replace(/\n/g, " ")}`
: bodySnippet.replace(/^#+\s*/m, "").replace(/\n/g, " ")
: reopenedLabel
? issue.title
? `${reopenedLabel} - ${issue.title}`
: reopenedLabel
: issue.title ?? undefined;
return {
title: `${actor} commented on ${issue.ref}`,
body: bodySnippet
? truncate(bodySnippet.replace(/^#+\s*/m, "").replace(/\n/g, " "), 96)
: issue.title
? truncate(issue.title, 96)
: undefined,
title,
body: body ? truncate(body, 96) : undefined,
tone: "info",
action: { label: `View ${issue.ref}`, href: issue.href },
dedupeKey: `activity:${action}:${entityId}:${commentId ?? "na"}`,
@@ -220,14 +237,12 @@ function buildAgentStatusToast(
const status = readString(payload.status);
if (!agentId || !status || !AGENT_TOAST_STATUSES.has(status)) return null;
const tone = status === "error" ? "error" : status === "idle" ? "success" : "info";
const tone = status === "error" ? "error" : "info";
const name = nameOf(agentId) ?? `Agent ${shortId(agentId)}`;
const title =
status === "running"
? `${name} started`
: status === "idle"
? `${name} is idle`
: `${name} errored`;
: `${name} errored`;
const agents = queryClient.getQueryData<Agent[]>(queryKeys.agents.list(companyId));
const agent = agents?.find((a) => a.id === agentId);