-
Assignee
-
- {agents.map((agent) => (
-
- ))}
-
+
+
Assignee
+
+
+ {currentUserId && (
+
+ )}
+ {(agents ?? []).map((agent) => (
+
+ ))}
- )}
+
{labels && labels.length > 0 && (
@@ -683,6 +725,13 @@ export function IssuesList({
>
{issue.assigneeAgentId && agentName(issue.assigneeAgentId) ? (
+ ) : issue.assigneeUserId ? (
+
+
+
+
+ {formatAssigneeUserLabel(issue.assigneeUserId, currentUserId) ?? "User"}
+
) : (
@@ -701,7 +750,7 @@ export function IssuesList({
>
setAssigneeSearch(e.target.value)}
autoFocus
@@ -710,16 +759,32 @@ export function IssuesList({
+ {currentUserId && (
+
+ )}
{(agents ?? [])
.filter((agent) => {
if (!assigneeSearch.trim()) return true;
@@ -737,7 +802,7 @@ export function IssuesList({
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
- assignIssue(issue.id, agent.id);
+ assignIssue(issue.id, agent.id, null);
}}
>
diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx
index a90efa9a..e484b265 100644
--- a/ui/src/components/Layout.tsx
+++ b/ui/src/components/Layout.tsx
@@ -22,6 +22,7 @@ import { useTheme } from "../context/ThemeContext";
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
import { healthApi } from "../api/health";
+import { shouldSyncCompanySelectionFromRoute } from "../lib/company-selection";
import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils";
import { NotFoundPage } from "../pages/NotFound";
@@ -36,6 +37,7 @@ export function Layout() {
loading: companiesLoading,
selectedCompany,
selectedCompanyId,
+ selectionSource,
setSelectedCompanyId,
} = useCompany();
const { theme, toggleTheme } = useTheme();
@@ -88,7 +90,13 @@ export function Layout() {
return;
}
- if (selectedCompanyId !== matchedCompany.id) {
+ if (
+ shouldSyncCompanySelectionFromRoute({
+ selectionSource,
+ selectedCompanyId,
+ routeCompanyId: matchedCompany.id,
+ })
+ ) {
setSelectedCompanyId(matchedCompany.id, { source: "route_sync" });
}
}, [
@@ -99,6 +107,7 @@ export function Layout() {
location.pathname,
location.search,
navigate,
+ selectionSource,
selectedCompanyId,
setSelectedCompanyId,
]);
diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx
index 01f210ed..c017306c 100644
--- a/ui/src/components/NewIssueDialog.tsx
+++ b/ui/src/components/NewIssueDialog.tsx
@@ -10,6 +10,11 @@ import { assetsApi } from "../api/assets";
import { queryKeys } from "../lib/queryKeys";
import { useProjectOrder } from "../hooks/useProjectOrder";
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
+import {
+ assigneeValueFromSelection,
+ currentUserAssigneeOption,
+ parseAssigneeValue,
+} from "../lib/assignees";
import {
Dialog,
DialogContent,
@@ -63,7 +68,8 @@ interface IssueDraft {
description: string;
status: string;
priority: string;
- assigneeId: string;
+ assigneeValue: string;
+ assigneeId?: string;
projectId: string;
assigneeModelOverride: string;
assigneeThinkingEffort: string;
@@ -173,7 +179,7 @@ export function NewIssueDialog() {
const [description, setDescription] = useState("");
const [status, setStatus] = useState("todo");
const [priority, setPriority] = useState("");
- const [assigneeId, setAssigneeId] = useState("");
+ const [assigneeValue, setAssigneeValue] = useState("");
const [projectId, setProjectId] = useState("");
const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false);
const [assigneeModelOverride, setAssigneeModelOverride] = useState("");
@@ -220,7 +226,11 @@ export function NewIssueDialog() {
userId: currentUserId,
});
- const assigneeAdapterType = (agents ?? []).find((agent) => agent.id === assigneeId)?.adapterType ?? null;
+ const selectedAssignee = useMemo(() => parseAssigneeValue(assigneeValue), [assigneeValue]);
+ const selectedAssigneeAgentId = selectedAssignee.assigneeAgentId;
+ const selectedAssigneeUserId = selectedAssignee.assigneeUserId;
+
+ const assigneeAdapterType = (agents ?? []).find((agent) => agent.id === selectedAssigneeAgentId)?.adapterType ?? null;
const supportsAssigneeOverrides = Boolean(
assigneeAdapterType && ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType),
);
@@ -295,7 +305,7 @@ export function NewIssueDialog() {
description,
status,
priority,
- assigneeId,
+ assigneeValue,
projectId,
assigneeModelOverride,
assigneeThinkingEffort,
@@ -307,7 +317,7 @@ export function NewIssueDialog() {
description,
status,
priority,
- assigneeId,
+ assigneeValue,
projectId,
assigneeModelOverride,
assigneeThinkingEffort,
@@ -330,7 +340,7 @@ export function NewIssueDialog() {
setStatus(newIssueDefaults.status ?? "todo");
setPriority(newIssueDefaults.priority ?? "");
setProjectId(newIssueDefaults.projectId ?? "");
- setAssigneeId(newIssueDefaults.assigneeAgentId ?? "");
+ setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
setAssigneeModelOverride("");
setAssigneeThinkingEffort("");
setAssigneeChrome(false);
@@ -340,7 +350,11 @@ export function NewIssueDialog() {
setDescription(draft.description);
setStatus(draft.status || "todo");
setPriority(draft.priority);
- setAssigneeId(newIssueDefaults.assigneeAgentId ?? draft.assigneeId);
+ setAssigneeValue(
+ newIssueDefaults.assigneeAgentId || newIssueDefaults.assigneeUserId
+ ? assigneeValueFromSelection(newIssueDefaults)
+ : (draft.assigneeValue ?? draft.assigneeId ?? ""),
+ );
setProjectId(newIssueDefaults.projectId ?? draft.projectId);
setAssigneeModelOverride(draft.assigneeModelOverride ?? "");
setAssigneeThinkingEffort(draft.assigneeThinkingEffort ?? "");
@@ -350,7 +364,7 @@ export function NewIssueDialog() {
setStatus(newIssueDefaults.status ?? "todo");
setPriority(newIssueDefaults.priority ?? "");
setProjectId(newIssueDefaults.projectId ?? "");
- setAssigneeId(newIssueDefaults.assigneeAgentId ?? "");
+ setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
setAssigneeModelOverride("");
setAssigneeThinkingEffort("");
setAssigneeChrome(false);
@@ -390,7 +404,7 @@ export function NewIssueDialog() {
setDescription("");
setStatus("todo");
setPriority("");
- setAssigneeId("");
+ setAssigneeValue("");
setProjectId("");
setAssigneeOptionsOpen(false);
setAssigneeModelOverride("");
@@ -406,7 +420,7 @@ export function NewIssueDialog() {
function handleCompanyChange(companyId: string) {
if (companyId === effectiveCompanyId) return;
setDialogCompanyId(companyId);
- setAssigneeId("");
+ setAssigneeValue("");
setProjectId("");
setAssigneeModelOverride("");
setAssigneeThinkingEffort("");
@@ -443,7 +457,8 @@ export function NewIssueDialog() {
description: description.trim() || undefined,
status,
priority: priority || "medium",
- ...(assigneeId ? { assigneeAgentId: assigneeId } : {}),
+ ...(selectedAssigneeAgentId ? { assigneeAgentId: selectedAssigneeAgentId } : {}),
+ ...(selectedAssigneeUserId ? { assigneeUserId: selectedAssigneeUserId } : {}),
...(projectId ? { projectId } : {}),
...(assigneeAdapterOverrides ? { assigneeAdapterOverrides } : {}),
...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}),
@@ -475,7 +490,9 @@ export function NewIssueDialog() {
const hasDraft = title.trim().length > 0 || description.trim().length > 0;
const currentStatus = statuses.find((s) => s.value === status) ?? statuses[1]!;
const currentPriority = priorities.find((p) => p.value === priority);
- const currentAssignee = (agents ?? []).find((a) => a.id === assigneeId);
+ const currentAssignee = selectedAssigneeAgentId
+ ? (agents ?? []).find((a) => a.id === selectedAssigneeAgentId)
+ : null;
const currentProject = orderedProjects.find((project) => project.id === projectId);
const currentProjectExecutionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI
? currentProject?.executionWorkspacePolicy ?? null
@@ -497,16 +514,18 @@ export function NewIssueDialog() {
: ISSUE_THINKING_EFFORT_OPTIONS.claude_local;
const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [newIssueOpen]);
const assigneeOptions = useMemo(
- () =>
- sortAgentsByRecency(
+ () => [
+ ...currentUserAssigneeOption(currentUserId),
+ ...sortAgentsByRecency(
(agents ?? []).filter((agent) => agent.status !== "terminated"),
recentAssigneeIds,
).map((agent) => ({
- id: agent.id,
+ id: assigneeValueFromSelection({ assigneeAgentId: agent.id }),
label: agent.name,
searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`,
})),
- [agents, recentAssigneeIds],
+ ],
+ [agents, currentUserId, recentAssigneeIds],
);
const projectOptions = useMemo(
() =>
@@ -710,7 +729,16 @@ export function NewIssueDialog() {
}
if (e.key === "Tab" && !e.shiftKey) {
e.preventDefault();
- assigneeSelectorRef.current?.focus();
+ if (assigneeValue) {
+ // Assignee already set — skip to project or description
+ if (projectId) {
+ descriptionEditorRef.current?.focus();
+ } else {
+ projectSelectorRef.current?.focus();
+ }
+ } else {
+ assigneeSelectorRef.current?.focus();
+ }
}
}}
autoFocus
@@ -723,33 +751,49 @@ export function NewIssueDialog() {
For
{ if (id) trackRecentAssignee(id); setAssigneeId(id); }}
+ onChange={(value) => {
+ const nextAssignee = parseAssigneeValue(value);
+ if (nextAssignee.assigneeAgentId) {
+ trackRecentAssignee(nextAssignee.assigneeAgentId);
+ }
+ setAssigneeValue(value);
+ }}
onConfirm={() => {
- projectSelectorRef.current?.focus();
+ if (projectId) {
+ descriptionEditorRef.current?.focus();
+ } else {
+ projectSelectorRef.current?.focus();
+ }
}}
renderTriggerValue={(option) =>
- option && currentAssignee ? (
- <>
-
+ option ? (
+ currentAssignee ? (
+ <>
+
+ {option.label}
+ >
+ ) : (
{option.label}
- >
+ )
) : (
Assignee
)
}
renderOption={(option) => {
if (!option.id) return {option.label};
- const assignee = (agents ?? []).find((agent) => agent.id === option.id);
+ const assignee = parseAssigneeValue(option.id).assigneeAgentId
+ ? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
+ : null;
return (
<>
-
+ {assignee ? : null}
{option.label}
>
);
diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx
index 5d166929..88e16d09 100644
--- a/ui/src/components/OnboardingWizard.tsx
+++ b/ui/src/components/OnboardingWizard.tsx
@@ -494,23 +494,41 @@ export function OnboardingWizard() {
}
async function handleStep3Next() {
+ if (!createdCompanyId || !createdAgentId) return;
+ setError(null);
+ setStep(4);
+ }
+
+ async function handleLaunch() {
if (!createdCompanyId || !createdAgentId) return;
setLoading(true);
setError(null);
try {
- const issue = await issuesApi.create(createdCompanyId, {
- title: taskTitle.trim(),
- ...(taskDescription.trim()
- ? { description: taskDescription.trim() }
- : {}),
- assigneeAgentId: createdAgentId,
- status: "todo"
- });
- setCreatedIssueRef(issue.identifier ?? issue.id);
- queryClient.invalidateQueries({
- queryKey: queryKeys.issues.list(createdCompanyId)
- });
- setStep(4);
+ let issueRef = createdIssueRef;
+ if (!issueRef) {
+ const issue = await issuesApi.create(createdCompanyId, {
+ title: taskTitle.trim(),
+ ...(taskDescription.trim()
+ ? { description: taskDescription.trim() }
+ : {}),
+ assigneeAgentId: createdAgentId,
+ status: "todo"
+ });
+ issueRef = issue.identifier ?? issue.id;
+ setCreatedIssueRef(issueRef);
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.issues.list(createdCompanyId)
+ });
+ }
+
+ setSelectedCompanyId(createdCompanyId);
+ reset();
+ closeOnboarding();
+ navigate(
+ createdCompanyPrefix
+ ? `/${createdCompanyPrefix}/issues/${issueRef}`
+ : `/issues/${issueRef}`
+ );
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create task");
} finally {
@@ -518,20 +536,6 @@ export function OnboardingWizard() {
}
}
- async function handleLaunch() {
- if (!createdAgentId) return;
- setLoading(true);
- setError(null);
- setLoading(false);
- reset();
- closeOnboarding();
- if (createdCompanyPrefix) {
- navigate(`/${createdCompanyPrefix}/dashboard`);
- return;
- }
- navigate("/dashboard");
- }
-
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
@@ -1175,8 +1179,8 @@ export function OnboardingWizard() {
Ready to launch
- Everything is set up. Your assigned task already woke
- the agent, so you can jump straight to the issue.
+ Everything is set up. Launching now will create the
+ starter task, wake the agent, and open the issue.
@@ -1291,7 +1295,7 @@ export function OnboardingWizard() {
) : (
)}
- {loading ? "Opening..." : "Open Issue"}
+ {loading ? "Creating..." : "Create & Open Issue"}
)}
diff --git a/ui/src/context/CompanyContext.tsx b/ui/src/context/CompanyContext.tsx
index eafc7f55..fb074f33 100644
--- a/ui/src/context/CompanyContext.tsx
+++ b/ui/src/context/CompanyContext.tsx
@@ -12,8 +12,7 @@ import type { Company } from "@paperclipai/shared";
import { companiesApi } from "../api/companies";
import { ApiError } from "../api/client";
import { queryKeys } from "../lib/queryKeys";
-
-type CompanySelectionSource = "manual" | "route_sync" | "bootstrap";
+import type { CompanySelectionSource } from "../lib/company-selection";
type CompanySelectionOptions = { source?: CompanySelectionSource };
interface CompanyContextValue {
diff --git a/ui/src/context/DialogContext.tsx b/ui/src/context/DialogContext.tsx
index ef7b12b8..904ceb88 100644
--- a/ui/src/context/DialogContext.tsx
+++ b/ui/src/context/DialogContext.tsx
@@ -5,6 +5,7 @@ interface NewIssueDefaults {
priority?: string;
projectId?: string;
assigneeAgentId?: string;
+ assigneeUserId?: string;
title?: string;
description?: string;
}
diff --git a/ui/src/hooks/useCompanyPageMemory.test.ts b/ui/src/hooks/useCompanyPageMemory.test.ts
new file mode 100644
index 00000000..a64c60b8
--- /dev/null
+++ b/ui/src/hooks/useCompanyPageMemory.test.ts
@@ -0,0 +1,71 @@
+import { describe, expect, it } from "vitest";
+import {
+ getRememberedPathOwnerCompanyId,
+ sanitizeRememberedPathForCompany,
+} from "../lib/company-page-memory";
+
+const companies = [
+ { id: "for", issuePrefix: "FOR" },
+ { id: "pap", issuePrefix: "PAP" },
+];
+
+describe("getRememberedPathOwnerCompanyId", () => {
+ it("uses the route company instead of stale selected-company state for prefixed routes", () => {
+ expect(
+ getRememberedPathOwnerCompanyId({
+ companies,
+ pathname: "/FOR/issues/FOR-1",
+ fallbackCompanyId: "pap",
+ }),
+ ).toBe("for");
+ });
+
+ it("skips saving when a prefixed route cannot yet be resolved to a known company", () => {
+ expect(
+ getRememberedPathOwnerCompanyId({
+ companies: [],
+ pathname: "/FOR/issues/FOR-1",
+ fallbackCompanyId: "pap",
+ }),
+ ).toBeNull();
+ });
+
+ it("falls back to the previous company for unprefixed board routes", () => {
+ expect(
+ getRememberedPathOwnerCompanyId({
+ companies,
+ pathname: "/dashboard",
+ fallbackCompanyId: "pap",
+ }),
+ ).toBe("pap");
+ });
+});
+
+describe("sanitizeRememberedPathForCompany", () => {
+ it("keeps remembered issue paths that belong to the target company", () => {
+ expect(
+ sanitizeRememberedPathForCompany({
+ path: "/issues/PAP-12",
+ companyPrefix: "PAP",
+ }),
+ ).toBe("/issues/PAP-12");
+ });
+
+ it("falls back to dashboard for remembered issue identifiers from another company", () => {
+ expect(
+ sanitizeRememberedPathForCompany({
+ path: "/issues/FOR-1",
+ companyPrefix: "PAP",
+ }),
+ ).toBe("/dashboard");
+ });
+
+ it("falls back to dashboard when no remembered path exists", () => {
+ expect(
+ sanitizeRememberedPathForCompany({
+ path: null,
+ companyPrefix: "PAP",
+ }),
+ ).toBe("/dashboard");
+ });
+});
diff --git a/ui/src/hooks/useCompanyPageMemory.ts b/ui/src/hooks/useCompanyPageMemory.ts
index d427e587..5206df11 100644
--- a/ui/src/hooks/useCompanyPageMemory.ts
+++ b/ui/src/hooks/useCompanyPageMemory.ts
@@ -1,10 +1,14 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useMemo, useRef } from "react";
import { useLocation, useNavigate } from "@/lib/router";
import { useCompany } from "../context/CompanyContext";
import { toCompanyRelativePath } from "../lib/company-routes";
+import {
+ getRememberedPathOwnerCompanyId,
+ isRememberableCompanyPath,
+ sanitizeRememberedPathForCompany,
+} from "../lib/company-page-memory";
const STORAGE_KEY = "paperclip.companyPaths";
-const GLOBAL_SEGMENTS = new Set(["auth", "invite", "board-claim", "docs"]);
function getCompanyPaths(): Record