2026-03-14 07:13:59 -05:00
|
|
|
import { useState, useEffect, useRef, useCallback, useMemo, type ChangeEvent, type DragEvent } from "react";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { useDialog } from "../context/DialogContext";
|
|
|
|
|
import { useCompany } from "../context/CompanyContext";
|
2026-03-13 17:12:25 -05:00
|
|
|
import { executionWorkspacesApi } from "../api/execution-workspaces";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { issuesApi } from "../api/issues";
|
2026-03-17 09:24:28 -05:00
|
|
|
import { instanceSettingsApi } from "../api/instanceSettings";
|
2026-02-17 10:53:20 -06:00
|
|
|
import { projectsApi } from "../api/projects";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { agentsApi } from "../api/agents";
|
2026-03-02 14:20:49 -06:00
|
|
|
import { authApi } from "../api/auth";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import { assetsApi } from "../api/assets";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { queryKeys } from "../lib/queryKeys";
|
2026-03-02 14:20:49 -06:00
|
|
|
import { useProjectOrder } from "../hooks/useProjectOrder";
|
2026-03-05 11:19:56 -06:00
|
|
|
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
|
2026-03-14 07:13:59 -05:00
|
|
|
import { useToast } from "../context/ToastContext";
|
2026-03-12 16:11:37 -05:00
|
|
|
import {
|
|
|
|
|
assigneeValueFromSelection,
|
|
|
|
|
currentUserAssigneeOption,
|
|
|
|
|
parseAssigneeValue,
|
|
|
|
|
} from "../lib/assignees";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import {
|
2026-02-17 10:53:20 -06:00
|
|
|
Popover,
|
|
|
|
|
PopoverContent,
|
|
|
|
|
PopoverTrigger,
|
|
|
|
|
} from "@/components/ui/popover";
|
|
|
|
|
import {
|
|
|
|
|
Maximize2,
|
|
|
|
|
Minimize2,
|
|
|
|
|
MoreHorizontal,
|
2026-02-26 10:32:44 -06:00
|
|
|
ChevronRight,
|
|
|
|
|
ChevronDown,
|
2026-02-17 10:53:20 -06:00
|
|
|
CircleDot,
|
|
|
|
|
Minus,
|
|
|
|
|
ArrowUp,
|
|
|
|
|
ArrowDown,
|
|
|
|
|
AlertTriangle,
|
|
|
|
|
Tag,
|
|
|
|
|
Calendar,
|
2026-02-25 21:36:06 -06:00
|
|
|
Paperclip,
|
2026-03-14 07:13:59 -05:00
|
|
|
FileText,
|
2026-03-10 21:06:16 -05:00
|
|
|
Loader2,
|
2026-03-14 07:13:59 -05:00
|
|
|
X,
|
2026-02-17 10:53:20 -06:00
|
|
|
} from "lucide-react";
|
|
|
|
|
import { cn } from "../lib/utils";
|
2026-03-05 15:52:59 +01:00
|
|
|
import { extractProviderIdWithFallback } from "../lib/model-utils";
|
2026-02-23 19:52:43 -06:00
|
|
|
import { issueStatusText, issueStatusTextDefault, priorityColor, priorityColorDefault } from "../lib/status-colors";
|
2026-03-02 13:31:58 -06:00
|
|
|
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor";
|
2026-02-23 14:41:21 -06:00
|
|
|
import { AgentIcon } from "./AgentIconPicker";
|
2026-02-26 08:53:03 -06:00
|
|
|
import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector";
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
const DRAFT_KEY = "paperclip:issue-draft";
|
|
|
|
|
const DEBOUNCE_MS = 800;
|
|
|
|
|
|
2026-02-26 16:33:48 -06:00
|
|
|
/** Return black or white hex based on background luminance (WCAG perceptual weights). */
|
|
|
|
|
function getContrastTextColor(hexColor: string): string {
|
|
|
|
|
const hex = hexColor.replace("#", "");
|
|
|
|
|
const r = parseInt(hex.substring(0, 2), 16);
|
|
|
|
|
const g = parseInt(hex.substring(2, 4), 16);
|
|
|
|
|
const b = parseInt(hex.substring(4, 6), 16);
|
|
|
|
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
|
|
|
return luminance > 0.5 ? "#000000" : "#ffffff";
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
interface IssueDraft {
|
|
|
|
|
title: string;
|
|
|
|
|
description: string;
|
|
|
|
|
status: string;
|
|
|
|
|
priority: string;
|
2026-03-12 16:11:37 -05:00
|
|
|
assigneeValue: string;
|
|
|
|
|
assigneeId?: string;
|
2026-02-17 20:46:12 -06:00
|
|
|
projectId: string;
|
2026-03-13 17:12:25 -05:00
|
|
|
projectWorkspaceId?: string;
|
2026-02-26 10:32:44 -06:00
|
|
|
assigneeModelOverride: string;
|
|
|
|
|
assigneeThinkingEffort: string;
|
2026-02-26 16:33:48 -06:00
|
|
|
assigneeChrome: boolean;
|
2026-03-13 17:12:25 -05:00
|
|
|
executionWorkspaceMode?: string;
|
|
|
|
|
selectedExecutionWorkspaceId?: string;
|
|
|
|
|
useIsolatedExecutionWorkspace?: boolean;
|
2026-02-26 10:32:44 -06:00
|
|
|
}
|
|
|
|
|
|
2026-03-14 07:13:59 -05:00
|
|
|
type StagedIssueFile = {
|
|
|
|
|
id: string;
|
|
|
|
|
file: File;
|
|
|
|
|
kind: "document" | "attachment";
|
|
|
|
|
documentKey?: string;
|
|
|
|
|
title?: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-04 16:48:54 -06:00
|
|
|
const ISSUE_OVERRIDE_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "opencode_local"]);
|
2026-03-14 07:13:59 -05:00
|
|
|
const STAGED_FILE_ACCEPT = "image/*,application/pdf,text/plain,text/markdown,application/json,text/csv,text/html,.md,.markdown";
|
2026-02-26 10:32:44 -06:00
|
|
|
|
|
|
|
|
const ISSUE_THINKING_EFFORT_OPTIONS = {
|
|
|
|
|
claude_local: [
|
|
|
|
|
{ value: "", label: "Default" },
|
|
|
|
|
{ value: "low", label: "Low" },
|
|
|
|
|
{ value: "medium", label: "Medium" },
|
|
|
|
|
{ value: "high", label: "High" },
|
|
|
|
|
],
|
|
|
|
|
codex_local: [
|
|
|
|
|
{ value: "", label: "Default" },
|
|
|
|
|
{ value: "minimal", label: "Minimal" },
|
|
|
|
|
{ value: "low", label: "Low" },
|
|
|
|
|
{ value: "medium", label: "Medium" },
|
|
|
|
|
{ value: "high", label: "High" },
|
|
|
|
|
],
|
2026-03-04 16:48:54 -06:00
|
|
|
opencode_local: [
|
|
|
|
|
{ value: "", label: "Default" },
|
|
|
|
|
{ value: "minimal", label: "Minimal" },
|
|
|
|
|
{ value: "low", label: "Low" },
|
|
|
|
|
{ value: "medium", label: "Medium" },
|
|
|
|
|
{ value: "high", label: "High" },
|
|
|
|
|
{ value: "max", label: "Max" },
|
|
|
|
|
],
|
2026-02-26 10:32:44 -06:00
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
function buildAssigneeAdapterOverrides(input: {
|
|
|
|
|
adapterType: string | null | undefined;
|
|
|
|
|
modelOverride: string;
|
|
|
|
|
thinkingEffortOverride: string;
|
2026-02-26 16:33:48 -06:00
|
|
|
chrome: boolean;
|
2026-02-26 10:32:44 -06:00
|
|
|
}): Record<string, unknown> | null {
|
|
|
|
|
const adapterType = input.adapterType ?? null;
|
|
|
|
|
if (!adapterType || !ISSUE_OVERRIDE_ADAPTER_TYPES.has(adapterType)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const adapterConfig: Record<string, unknown> = {};
|
|
|
|
|
if (input.modelOverride) adapterConfig.model = input.modelOverride;
|
|
|
|
|
if (input.thinkingEffortOverride) {
|
|
|
|
|
if (adapterType === "codex_local") {
|
|
|
|
|
adapterConfig.modelReasoningEffort = input.thinkingEffortOverride;
|
2026-03-04 16:48:54 -06:00
|
|
|
} else if (adapterType === "opencode_local") {
|
|
|
|
|
adapterConfig.variant = input.thinkingEffortOverride;
|
2026-02-26 10:32:44 -06:00
|
|
|
} else if (adapterType === "claude_local") {
|
|
|
|
|
adapterConfig.effort = input.thinkingEffortOverride;
|
2026-03-05 15:24:20 +01:00
|
|
|
} else if (adapterType === "opencode_local") {
|
|
|
|
|
adapterConfig.variant = input.thinkingEffortOverride;
|
2026-02-26 10:32:44 -06:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-26 16:33:48 -06:00
|
|
|
if (adapterType === "claude_local" && input.chrome) {
|
|
|
|
|
adapterConfig.chrome = true;
|
|
|
|
|
}
|
2026-02-26 10:32:44 -06:00
|
|
|
|
|
|
|
|
const overrides: Record<string, unknown> = {};
|
|
|
|
|
if (Object.keys(adapterConfig).length > 0) {
|
|
|
|
|
overrides.adapterConfig = adapterConfig;
|
|
|
|
|
}
|
|
|
|
|
return Object.keys(overrides).length > 0 ? overrides : null;
|
2026-02-17 20:46:12 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadDraft(): IssueDraft | null {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(DRAFT_KEY);
|
|
|
|
|
if (!raw) return null;
|
|
|
|
|
return JSON.parse(raw) as IssueDraft;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveDraft(draft: IssueDraft) {
|
|
|
|
|
localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearDraft() {
|
|
|
|
|
localStorage.removeItem(DRAFT_KEY);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 07:21:21 -05:00
|
|
|
function isTextDocumentFile(file: File) {
|
2026-03-14 07:13:59 -05:00
|
|
|
const name = file.name.toLowerCase();
|
|
|
|
|
return (
|
|
|
|
|
name.endsWith(".md") ||
|
|
|
|
|
name.endsWith(".markdown") ||
|
2026-03-14 07:21:21 -05:00
|
|
|
name.endsWith(".txt") ||
|
|
|
|
|
file.type === "text/markdown" ||
|
|
|
|
|
file.type === "text/plain"
|
2026-03-14 07:13:59 -05:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fileBaseName(filename: string) {
|
|
|
|
|
return filename.replace(/\.[^.]+$/, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function slugifyDocumentKey(input: string) {
|
|
|
|
|
const slug = input
|
|
|
|
|
.trim()
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
|
|
|
.replace(/^-+|-+$/g, "");
|
|
|
|
|
return slug || "document";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function titleizeFilename(input: string) {
|
|
|
|
|
return input
|
|
|
|
|
.split(/[-_ ]+/g)
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
|
|
|
.join(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createUniqueDocumentKey(baseKey: string, stagedFiles: StagedIssueFile[]) {
|
|
|
|
|
const existingKeys = new Set(
|
|
|
|
|
stagedFiles
|
|
|
|
|
.filter((file) => file.kind === "document")
|
|
|
|
|
.map((file) => file.documentKey)
|
|
|
|
|
.filter((key): key is string => Boolean(key)),
|
|
|
|
|
);
|
|
|
|
|
if (!existingKeys.has(baseKey)) return baseKey;
|
|
|
|
|
let suffix = 2;
|
|
|
|
|
while (existingKeys.has(`${baseKey}-${suffix}`)) {
|
|
|
|
|
suffix += 1;
|
|
|
|
|
}
|
|
|
|
|
return `${baseKey}-${suffix}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatFileSize(file: File) {
|
|
|
|
|
if (file.size < 1024) return `${file.size} B`;
|
|
|
|
|
if (file.size < 1024 * 1024) return `${(file.size / 1024).toFixed(1)} KB`;
|
|
|
|
|
return `${(file.size / (1024 * 1024)).toFixed(1)} MB`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
const statuses = [
|
2026-02-23 19:52:43 -06:00
|
|
|
{ value: "backlog", label: "Backlog", color: issueStatusText.backlog ?? issueStatusTextDefault },
|
|
|
|
|
{ value: "todo", label: "Todo", color: issueStatusText.todo ?? issueStatusTextDefault },
|
|
|
|
|
{ value: "in_progress", label: "In Progress", color: issueStatusText.in_progress ?? issueStatusTextDefault },
|
|
|
|
|
{ value: "in_review", label: "In Review", color: issueStatusText.in_review ?? issueStatusTextDefault },
|
|
|
|
|
{ value: "done", label: "Done", color: issueStatusText.done ?? issueStatusTextDefault },
|
2026-02-17 10:53:20 -06:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const priorities = [
|
2026-02-23 19:52:43 -06:00
|
|
|
{ value: "critical", label: "Critical", icon: AlertTriangle, color: priorityColor.critical ?? priorityColorDefault },
|
|
|
|
|
{ value: "high", label: "High", icon: ArrowUp, color: priorityColor.high ?? priorityColorDefault },
|
|
|
|
|
{ value: "medium", label: "Medium", icon: Minus, color: priorityColor.medium ?? priorityColorDefault },
|
|
|
|
|
{ value: "low", label: "Low", icon: ArrowDown, color: priorityColor.low ?? priorityColorDefault },
|
2026-02-17 10:53:20 -06:00
|
|
|
];
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-03-13 17:12:25 -05:00
|
|
|
const EXECUTION_WORKSPACE_MODES = [
|
|
|
|
|
{ value: "shared_workspace", label: "Project default" },
|
|
|
|
|
{ value: "isolated_workspace", label: "New isolated workspace" },
|
|
|
|
|
{ value: "reuse_existing", label: "Reuse existing workspace" },
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
function defaultProjectWorkspaceIdForProject(project: { workspaces?: Array<{ id: string; isPrimary: boolean }>; executionWorkspacePolicy?: { defaultProjectWorkspaceId?: string | null } | null } | null | undefined) {
|
|
|
|
|
if (!project) return "";
|
|
|
|
|
return project.executionWorkspacePolicy?.defaultProjectWorkspaceId
|
|
|
|
|
?? project.workspaces?.find((workspace) => workspace.isPrimary)?.id
|
|
|
|
|
?? project.workspaces?.[0]?.id
|
|
|
|
|
?? "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultExecutionWorkspaceModeForProject(project: { executionWorkspacePolicy?: { enabled?: boolean; defaultMode?: string | null } | null } | null | undefined) {
|
|
|
|
|
const defaultMode = project?.executionWorkspacePolicy?.enabled ? project.executionWorkspacePolicy.defaultMode : null;
|
|
|
|
|
if (
|
|
|
|
|
defaultMode === "isolated_workspace" ||
|
|
|
|
|
defaultMode === "operator_branch" ||
|
|
|
|
|
defaultMode === "adapter_default"
|
|
|
|
|
) {
|
|
|
|
|
return defaultMode === "adapter_default" ? "agent_default" : defaultMode;
|
|
|
|
|
}
|
|
|
|
|
return "shared_workspace";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function issueExecutionWorkspaceModeForExistingWorkspace(mode: string | null | undefined) {
|
|
|
|
|
if (mode === "isolated_workspace" || mode === "operator_branch" || mode === "shared_workspace") {
|
|
|
|
|
return mode;
|
|
|
|
|
}
|
|
|
|
|
if (mode === "adapter_managed" || mode === "cloud_sandbox") {
|
|
|
|
|
return "agent_default";
|
|
|
|
|
}
|
|
|
|
|
return "shared_workspace";
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
export function NewIssueDialog() {
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
const { newIssueOpen, newIssueDefaults, closeNewIssue } = useDialog();
|
2026-02-26 16:33:48 -06:00
|
|
|
const { companies, selectedCompanyId, selectedCompany } = useCompany();
|
2026-02-17 12:24:48 -06:00
|
|
|
const queryClient = useQueryClient();
|
2026-03-14 07:13:59 -05:00
|
|
|
const { pushToast } = useToast();
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
const [title, setTitle] = useState("");
|
|
|
|
|
const [description, setDescription] = useState("");
|
2026-02-17 10:53:20 -06:00
|
|
|
const [status, setStatus] = useState("todo");
|
|
|
|
|
const [priority, setPriority] = useState("");
|
2026-03-12 16:11:37 -05:00
|
|
|
const [assigneeValue, setAssigneeValue] = useState("");
|
2026-02-17 10:53:20 -06:00
|
|
|
const [projectId, setProjectId] = useState("");
|
2026-03-13 17:12:25 -05:00
|
|
|
const [projectWorkspaceId, setProjectWorkspaceId] = useState("");
|
2026-02-26 10:32:44 -06:00
|
|
|
const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false);
|
|
|
|
|
const [assigneeModelOverride, setAssigneeModelOverride] = useState("");
|
|
|
|
|
const [assigneeThinkingEffort, setAssigneeThinkingEffort] = useState("");
|
2026-02-26 16:33:48 -06:00
|
|
|
const [assigneeChrome, setAssigneeChrome] = useState(false);
|
2026-03-13 17:12:25 -05:00
|
|
|
const [executionWorkspaceMode, setExecutionWorkspaceMode] = useState<string>("shared_workspace");
|
|
|
|
|
const [selectedExecutionWorkspaceId, setSelectedExecutionWorkspaceId] = useState("");
|
2026-02-17 10:53:20 -06:00
|
|
|
const [expanded, setExpanded] = useState(false);
|
2026-02-26 16:33:48 -06:00
|
|
|
const [dialogCompanyId, setDialogCompanyId] = useState<string | null>(null);
|
2026-03-14 07:13:59 -05:00
|
|
|
const [stagedFiles, setStagedFiles] = useState<StagedIssueFile[]>([]);
|
|
|
|
|
const [isFileDragOver, setIsFileDragOver] = useState(false);
|
2026-02-17 20:46:12 -06:00
|
|
|
const draftTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
2026-03-10 09:03:31 -05:00
|
|
|
const executionWorkspaceDefaultProjectId = useRef<string | null>(null);
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-26 16:33:48 -06:00
|
|
|
const effectiveCompanyId = dialogCompanyId ?? selectedCompanyId;
|
|
|
|
|
const dialogCompany = companies.find((c) => c.id === effectiveCompanyId) ?? selectedCompany;
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
// Popover states
|
|
|
|
|
const [statusOpen, setStatusOpen] = useState(false);
|
|
|
|
|
const [priorityOpen, setPriorityOpen] = useState(false);
|
|
|
|
|
const [moreOpen, setMoreOpen] = useState(false);
|
2026-02-26 16:33:48 -06:00
|
|
|
const [companyOpen, setCompanyOpen] = useState(false);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
2026-03-14 07:13:59 -05:00
|
|
|
const stageFileInputRef = useRef<HTMLInputElement | null>(null);
|
2026-02-26 08:53:03 -06:00
|
|
|
const assigneeSelectorRef = useRef<HTMLButtonElement | null>(null);
|
|
|
|
|
const projectSelectorRef = useRef<HTMLButtonElement | null>(null);
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const { data: agents } = useQuery({
|
2026-02-26 16:33:48 -06:00
|
|
|
queryKey: queryKeys.agents.list(effectiveCompanyId!),
|
|
|
|
|
queryFn: () => agentsApi.list(effectiveCompanyId!),
|
|
|
|
|
enabled: !!effectiveCompanyId && newIssueOpen,
|
2026-02-17 12:24:48 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { data: projects } = useQuery({
|
2026-02-26 16:33:48 -06:00
|
|
|
queryKey: queryKeys.projects.list(effectiveCompanyId!),
|
|
|
|
|
queryFn: () => projectsApi.list(effectiveCompanyId!),
|
|
|
|
|
enabled: !!effectiveCompanyId && newIssueOpen,
|
2026-02-17 12:24:48 -06:00
|
|
|
});
|
2026-03-13 17:12:25 -05:00
|
|
|
const { data: reusableExecutionWorkspaces } = useQuery({
|
|
|
|
|
queryKey: queryKeys.executionWorkspaces.list(effectiveCompanyId!, {
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId: projectWorkspaceId || undefined,
|
|
|
|
|
reuseEligible: true,
|
|
|
|
|
}),
|
|
|
|
|
queryFn: () =>
|
|
|
|
|
executionWorkspacesApi.list(effectiveCompanyId!, {
|
|
|
|
|
projectId,
|
|
|
|
|
projectWorkspaceId: projectWorkspaceId || undefined,
|
|
|
|
|
reuseEligible: true,
|
|
|
|
|
}),
|
2026-03-16 18:37:59 -05:00
|
|
|
enabled: Boolean(effectiveCompanyId) && newIssueOpen && Boolean(projectId),
|
2026-03-13 17:12:25 -05:00
|
|
|
});
|
2026-03-02 14:20:49 -06:00
|
|
|
const { data: session } = useQuery({
|
|
|
|
|
queryKey: queryKeys.auth.session,
|
|
|
|
|
queryFn: () => authApi.getSession(),
|
|
|
|
|
});
|
2026-03-17 09:24:28 -05:00
|
|
|
const { data: experimentalSettings } = useQuery({
|
|
|
|
|
queryKey: queryKeys.instance.experimentalSettings,
|
|
|
|
|
queryFn: () => instanceSettingsApi.getExperimental(),
|
|
|
|
|
enabled: newIssueOpen,
|
|
|
|
|
});
|
2026-03-02 14:20:49 -06:00
|
|
|
const currentUserId = session?.user?.id ?? session?.session?.userId ?? null;
|
2026-03-14 17:47:53 -05:00
|
|
|
const activeProjects = useMemo(
|
|
|
|
|
() => (projects ?? []).filter((p) => !p.archivedAt),
|
|
|
|
|
[projects],
|
|
|
|
|
);
|
2026-03-02 14:20:49 -06:00
|
|
|
const { orderedProjects } = useProjectOrder({
|
2026-03-14 17:47:53 -05:00
|
|
|
projects: activeProjects,
|
2026-03-02 14:20:49 -06:00
|
|
|
companyId: effectiveCompanyId,
|
|
|
|
|
userId: currentUserId,
|
|
|
|
|
});
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-03-12 16:11:37 -05:00
|
|
|
const selectedAssignee = useMemo(() => parseAssigneeValue(assigneeValue), [assigneeValue]);
|
|
|
|
|
const selectedAssigneeAgentId = selectedAssignee.assigneeAgentId;
|
|
|
|
|
const selectedAssigneeUserId = selectedAssignee.assigneeUserId;
|
|
|
|
|
|
|
|
|
|
const assigneeAdapterType = (agents ?? []).find((agent) => agent.id === selectedAssigneeAgentId)?.adapterType ?? null;
|
2026-02-26 10:32:44 -06:00
|
|
|
const supportsAssigneeOverrides = Boolean(
|
|
|
|
|
assigneeAdapterType && ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType),
|
|
|
|
|
);
|
2026-03-02 13:31:58 -06:00
|
|
|
const mentionOptions = useMemo<MentionOption[]>(() => {
|
|
|
|
|
const options: MentionOption[] = [];
|
|
|
|
|
const activeAgents = [...(agents ?? [])]
|
|
|
|
|
.filter((agent) => agent.status !== "terminated")
|
|
|
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
for (const agent of activeAgents) {
|
|
|
|
|
options.push({
|
|
|
|
|
id: `agent:${agent.id}`,
|
|
|
|
|
name: agent.name,
|
|
|
|
|
kind: "agent",
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-02 14:20:49 -06:00
|
|
|
for (const project of orderedProjects) {
|
2026-03-02 13:31:58 -06:00
|
|
|
options.push({
|
|
|
|
|
id: `project:${project.id}`,
|
|
|
|
|
name: project.name,
|
|
|
|
|
kind: "project",
|
|
|
|
|
projectId: project.id,
|
|
|
|
|
projectColor: project.color,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return options;
|
2026-03-02 14:20:49 -06:00
|
|
|
}, [agents, orderedProjects]);
|
2026-02-26 10:32:44 -06:00
|
|
|
|
|
|
|
|
const { data: assigneeAdapterModels } = useQuery({
|
2026-03-05 15:24:20 +01:00
|
|
|
queryKey:
|
|
|
|
|
effectiveCompanyId && assigneeAdapterType
|
|
|
|
|
? queryKeys.agents.adapterModels(effectiveCompanyId, assigneeAdapterType)
|
|
|
|
|
: ["agents", "none", "adapter-models", assigneeAdapterType ?? "none"],
|
|
|
|
|
queryFn: () => agentsApi.adapterModels(effectiveCompanyId!, assigneeAdapterType!),
|
|
|
|
|
enabled: Boolean(effectiveCompanyId) && newIssueOpen && supportsAssigneeOverrides,
|
2026-02-26 10:32:44 -06:00
|
|
|
});
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const createIssue = useMutation({
|
2026-03-14 07:13:59 -05:00
|
|
|
mutationFn: async ({
|
|
|
|
|
companyId,
|
|
|
|
|
stagedFiles: pendingStagedFiles,
|
|
|
|
|
...data
|
|
|
|
|
}: { companyId: string; stagedFiles: StagedIssueFile[] } & Record<string, unknown>) => {
|
|
|
|
|
const issue = await issuesApi.create(companyId, data);
|
|
|
|
|
const failures: string[] = [];
|
|
|
|
|
|
|
|
|
|
for (const stagedFile of pendingStagedFiles) {
|
|
|
|
|
try {
|
|
|
|
|
if (stagedFile.kind === "document") {
|
|
|
|
|
const body = await stagedFile.file.text();
|
|
|
|
|
await issuesApi.upsertDocument(issue.id, stagedFile.documentKey ?? "document", {
|
|
|
|
|
title: stagedFile.documentKey === "plan" ? null : stagedFile.title ?? null,
|
|
|
|
|
format: "markdown",
|
|
|
|
|
body,
|
|
|
|
|
baseRevisionId: null,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await issuesApi.uploadAttachment(companyId, issue.id, stagedFile.file);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
failures.push(stagedFile.file.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { issue, companyId, failures };
|
|
|
|
|
},
|
|
|
|
|
onSuccess: ({ issue, companyId, failures }) => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId) });
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
if (draftTimer.current) clearTimeout(draftTimer.current);
|
2026-03-14 07:13:59 -05:00
|
|
|
if (failures.length > 0) {
|
|
|
|
|
const prefix = (companies.find((company) => company.id === companyId)?.issuePrefix ?? "").trim();
|
|
|
|
|
const issueRef = issue.identifier ?? issue.id;
|
|
|
|
|
pushToast({
|
|
|
|
|
title: `Created ${issueRef} with upload warnings`,
|
|
|
|
|
body: `${failures.length} staged ${failures.length === 1 ? "file" : "files"} could not be added.`,
|
|
|
|
|
tone: "warn",
|
|
|
|
|
action: prefix
|
|
|
|
|
? { label: `Open ${issueRef}`, href: `/${prefix}/issues/${issueRef}` }
|
|
|
|
|
: undefined,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-17 20:46:12 -06:00
|
|
|
clearDraft();
|
2026-02-17 12:24:48 -06:00
|
|
|
reset();
|
|
|
|
|
closeNewIssue();
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-02-17 10:53:20 -06:00
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const uploadDescriptionImage = useMutation({
|
|
|
|
|
mutationFn: async (file: File) => {
|
2026-02-26 16:33:48 -06:00
|
|
|
if (!effectiveCompanyId) throw new Error("No company selected");
|
|
|
|
|
return assetsApi.uploadImage(effectiveCompanyId, file, "issues/drafts");
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
// Debounced draft saving
|
|
|
|
|
const scheduleSave = useCallback(
|
|
|
|
|
(draft: IssueDraft) => {
|
|
|
|
|
if (draftTimer.current) clearTimeout(draftTimer.current);
|
|
|
|
|
draftTimer.current = setTimeout(() => {
|
|
|
|
|
if (draft.title.trim()) saveDraft(draft);
|
|
|
|
|
}, DEBOUNCE_MS);
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Save draft on meaningful changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!newIssueOpen) return;
|
2026-02-26 10:32:44 -06:00
|
|
|
scheduleSave({
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
status,
|
|
|
|
|
priority,
|
2026-03-12 16:11:37 -05:00
|
|
|
assigneeValue,
|
2026-02-26 10:32:44 -06:00
|
|
|
projectId,
|
2026-03-13 17:12:25 -05:00
|
|
|
projectWorkspaceId,
|
2026-02-26 10:32:44 -06:00
|
|
|
assigneeModelOverride,
|
|
|
|
|
assigneeThinkingEffort,
|
2026-02-26 16:33:48 -06:00
|
|
|
assigneeChrome,
|
2026-03-13 17:12:25 -05:00
|
|
|
executionWorkspaceMode,
|
|
|
|
|
selectedExecutionWorkspaceId,
|
2026-02-26 10:32:44 -06:00
|
|
|
});
|
|
|
|
|
}, [
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
status,
|
|
|
|
|
priority,
|
2026-03-12 16:11:37 -05:00
|
|
|
assigneeValue,
|
2026-02-26 10:32:44 -06:00
|
|
|
projectId,
|
2026-03-13 17:12:25 -05:00
|
|
|
projectWorkspaceId,
|
2026-02-26 10:32:44 -06:00
|
|
|
assigneeModelOverride,
|
|
|
|
|
assigneeThinkingEffort,
|
2026-02-26 16:33:48 -06:00
|
|
|
assigneeChrome,
|
2026-03-13 17:12:25 -05:00
|
|
|
executionWorkspaceMode,
|
|
|
|
|
selectedExecutionWorkspaceId,
|
2026-02-26 10:32:44 -06:00
|
|
|
newIssueOpen,
|
|
|
|
|
scheduleSave,
|
|
|
|
|
]);
|
2026-02-17 20:46:12 -06:00
|
|
|
|
|
|
|
|
// Restore draft or apply defaults when dialog opens
|
2026-02-17 10:53:20 -06:00
|
|
|
useEffect(() => {
|
2026-02-17 20:46:12 -06:00
|
|
|
if (!newIssueOpen) return;
|
2026-02-26 16:33:48 -06:00
|
|
|
setDialogCompanyId(selectedCompanyId);
|
2026-03-10 09:03:31 -05:00
|
|
|
executionWorkspaceDefaultProjectId.current = null;
|
2026-02-17 20:46:12 -06:00
|
|
|
|
|
|
|
|
const draft = loadDraft();
|
2026-03-07 08:26:49 -06:00
|
|
|
if (newIssueDefaults.title) {
|
|
|
|
|
setTitle(newIssueDefaults.title);
|
|
|
|
|
setDescription(newIssueDefaults.description ?? "");
|
|
|
|
|
setStatus(newIssueDefaults.status ?? "todo");
|
|
|
|
|
setPriority(newIssueDefaults.priority ?? "");
|
2026-03-13 17:12:25 -05:00
|
|
|
const defaultProjectId = newIssueDefaults.projectId ?? "";
|
|
|
|
|
const defaultProject = orderedProjects.find((project) => project.id === defaultProjectId);
|
|
|
|
|
setProjectId(defaultProjectId);
|
|
|
|
|
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(defaultProject));
|
2026-03-12 16:11:37 -05:00
|
|
|
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
2026-03-07 08:26:49 -06:00
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
|
|
|
|
setAssigneeChrome(false);
|
2026-03-13 17:12:25 -05:00
|
|
|
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(defaultProject));
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
|
|
|
|
executionWorkspaceDefaultProjectId.current = defaultProjectId || null;
|
2026-03-07 08:26:49 -06:00
|
|
|
} else if (draft && draft.title.trim()) {
|
2026-03-13 17:12:25 -05:00
|
|
|
const restoredProjectId = newIssueDefaults.projectId ?? draft.projectId;
|
|
|
|
|
const restoredProject = orderedProjects.find((project) => project.id === restoredProjectId);
|
2026-02-17 20:46:12 -06:00
|
|
|
setTitle(draft.title);
|
|
|
|
|
setDescription(draft.description);
|
|
|
|
|
setStatus(draft.status || "todo");
|
|
|
|
|
setPriority(draft.priority);
|
2026-03-12 16:11:37 -05:00
|
|
|
setAssigneeValue(
|
|
|
|
|
newIssueDefaults.assigneeAgentId || newIssueDefaults.assigneeUserId
|
|
|
|
|
? assigneeValueFromSelection(newIssueDefaults)
|
|
|
|
|
: (draft.assigneeValue ?? draft.assigneeId ?? ""),
|
|
|
|
|
);
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectId(restoredProjectId);
|
|
|
|
|
setProjectWorkspaceId(draft.projectWorkspaceId ?? defaultProjectWorkspaceIdForProject(restoredProject));
|
2026-02-26 10:32:44 -06:00
|
|
|
setAssigneeModelOverride(draft.assigneeModelOverride ?? "");
|
|
|
|
|
setAssigneeThinkingEffort(draft.assigneeThinkingEffort ?? "");
|
2026-02-26 16:33:48 -06:00
|
|
|
setAssigneeChrome(draft.assigneeChrome ?? false);
|
2026-03-13 17:12:25 -05:00
|
|
|
setExecutionWorkspaceMode(
|
|
|
|
|
draft.executionWorkspaceMode
|
|
|
|
|
?? (draft.useIsolatedExecutionWorkspace ? "isolated_workspace" : defaultExecutionWorkspaceModeForProject(restoredProject)),
|
|
|
|
|
);
|
|
|
|
|
setSelectedExecutionWorkspaceId(draft.selectedExecutionWorkspaceId ?? "");
|
|
|
|
|
executionWorkspaceDefaultProjectId.current = restoredProjectId || null;
|
2026-02-17 20:46:12 -06:00
|
|
|
} else {
|
2026-03-13 17:12:25 -05:00
|
|
|
const defaultProjectId = newIssueDefaults.projectId ?? "";
|
|
|
|
|
const defaultProject = orderedProjects.find((project) => project.id === defaultProjectId);
|
2026-02-17 10:53:20 -06:00
|
|
|
setStatus(newIssueDefaults.status ?? "todo");
|
|
|
|
|
setPriority(newIssueDefaults.priority ?? "");
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectId(defaultProjectId);
|
|
|
|
|
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(defaultProject));
|
2026-03-12 16:11:37 -05:00
|
|
|
setAssigneeValue(assigneeValueFromSelection(newIssueDefaults));
|
2026-02-26 10:32:44 -06:00
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
2026-02-26 16:33:48 -06:00
|
|
|
setAssigneeChrome(false);
|
2026-03-13 17:12:25 -05:00
|
|
|
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(defaultProject));
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
|
|
|
|
executionWorkspaceDefaultProjectId.current = defaultProjectId || null;
|
2026-02-17 10:53:20 -06:00
|
|
|
}
|
2026-03-13 17:12:25 -05:00
|
|
|
}, [newIssueOpen, newIssueDefaults, orderedProjects]);
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-02-26 10:32:44 -06:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!supportsAssigneeOverrides) {
|
|
|
|
|
setAssigneeOptionsOpen(false);
|
|
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
2026-02-26 16:33:48 -06:00
|
|
|
setAssigneeChrome(false);
|
2026-02-26 10:32:44 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validThinkingValues =
|
|
|
|
|
assigneeAdapterType === "codex_local"
|
|
|
|
|
? ISSUE_THINKING_EFFORT_OPTIONS.codex_local
|
2026-03-04 16:48:54 -06:00
|
|
|
: assigneeAdapterType === "opencode_local"
|
|
|
|
|
? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local
|
2026-03-05 15:24:20 +01:00
|
|
|
: ISSUE_THINKING_EFFORT_OPTIONS.claude_local;
|
2026-02-26 10:32:44 -06:00
|
|
|
if (!validThinkingValues.some((option) => option.value === assigneeThinkingEffort)) {
|
|
|
|
|
setAssigneeThinkingEffort("");
|
|
|
|
|
}
|
|
|
|
|
}, [supportsAssigneeOverrides, assigneeAdapterType, assigneeThinkingEffort]);
|
|
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
// Cleanup timer on unmount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (draftTimer.current) clearTimeout(draftTimer.current);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
function reset() {
|
|
|
|
|
setTitle("");
|
|
|
|
|
setDescription("");
|
|
|
|
|
setStatus("todo");
|
2026-02-17 10:53:20 -06:00
|
|
|
setPriority("");
|
2026-03-12 16:11:37 -05:00
|
|
|
setAssigneeValue("");
|
2026-02-17 10:53:20 -06:00
|
|
|
setProjectId("");
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectWorkspaceId("");
|
2026-02-26 10:32:44 -06:00
|
|
|
setAssigneeOptionsOpen(false);
|
|
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
2026-02-26 16:33:48 -06:00
|
|
|
setAssigneeChrome(false);
|
2026-03-13 17:12:25 -05:00
|
|
|
setExecutionWorkspaceMode("shared_workspace");
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
2026-02-17 10:53:20 -06:00
|
|
|
setExpanded(false);
|
2026-02-26 16:33:48 -06:00
|
|
|
setDialogCompanyId(null);
|
2026-03-14 07:13:59 -05:00
|
|
|
setStagedFiles([]);
|
|
|
|
|
setIsFileDragOver(false);
|
2026-02-26 16:33:48 -06:00
|
|
|
setCompanyOpen(false);
|
2026-03-10 09:03:31 -05:00
|
|
|
executionWorkspaceDefaultProjectId.current = null;
|
2026-02-26 16:33:48 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleCompanyChange(companyId: string) {
|
|
|
|
|
if (companyId === effectiveCompanyId) return;
|
|
|
|
|
setDialogCompanyId(companyId);
|
2026-03-12 16:11:37 -05:00
|
|
|
setAssigneeValue("");
|
2026-02-26 16:33:48 -06:00
|
|
|
setProjectId("");
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectWorkspaceId("");
|
2026-02-26 16:33:48 -06:00
|
|
|
setAssigneeModelOverride("");
|
|
|
|
|
setAssigneeThinkingEffort("");
|
|
|
|
|
setAssigneeChrome(false);
|
2026-03-13 17:12:25 -05:00
|
|
|
setExecutionWorkspaceMode("shared_workspace");
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 20:46:12 -06:00
|
|
|
function discardDraft() {
|
|
|
|
|
clearDraft();
|
|
|
|
|
reset();
|
|
|
|
|
closeNewIssue();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
function handleSubmit() {
|
2026-03-10 21:06:16 -05:00
|
|
|
if (!effectiveCompanyId || !title.trim() || createIssue.isPending) return;
|
2026-02-26 10:32:44 -06:00
|
|
|
const assigneeAdapterOverrides = buildAssigneeAdapterOverrides({
|
|
|
|
|
adapterType: assigneeAdapterType,
|
|
|
|
|
modelOverride: assigneeModelOverride,
|
|
|
|
|
thinkingEffortOverride: assigneeThinkingEffort,
|
2026-02-26 16:33:48 -06:00
|
|
|
chrome: assigneeChrome,
|
2026-02-26 10:32:44 -06:00
|
|
|
});
|
2026-03-10 09:03:31 -05:00
|
|
|
const selectedProject = orderedProjects.find((project) => project.id === projectId);
|
2026-03-17 09:24:28 -05:00
|
|
|
const executionWorkspacePolicy =
|
|
|
|
|
experimentalSettings?.enableIsolatedWorkspaces === true
|
|
|
|
|
? selectedProject?.executionWorkspacePolicy ?? null
|
|
|
|
|
: null;
|
2026-03-17 07:46:40 -05:00
|
|
|
const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find(
|
2026-03-13 17:12:25 -05:00
|
|
|
(workspace) => workspace.id === selectedExecutionWorkspaceId,
|
|
|
|
|
);
|
|
|
|
|
const requestedExecutionWorkspaceMode =
|
|
|
|
|
executionWorkspaceMode === "reuse_existing"
|
|
|
|
|
? issueExecutionWorkspaceModeForExistingWorkspace(selectedReusableExecutionWorkspace?.mode)
|
|
|
|
|
: executionWorkspaceMode;
|
2026-03-10 09:03:31 -05:00
|
|
|
const executionWorkspaceSettings = executionWorkspacePolicy?.enabled
|
2026-03-13 17:12:25 -05:00
|
|
|
? { mode: requestedExecutionWorkspaceMode }
|
2026-03-10 09:03:31 -05:00
|
|
|
: null;
|
2026-02-17 12:24:48 -06:00
|
|
|
createIssue.mutate({
|
2026-02-26 16:33:48 -06:00
|
|
|
companyId: effectiveCompanyId,
|
2026-03-14 07:13:59 -05:00
|
|
|
stagedFiles,
|
2026-02-17 12:24:48 -06:00
|
|
|
title: title.trim(),
|
|
|
|
|
description: description.trim() || undefined,
|
|
|
|
|
status,
|
|
|
|
|
priority: priority || "medium",
|
2026-03-12 16:11:37 -05:00
|
|
|
...(selectedAssigneeAgentId ? { assigneeAgentId: selectedAssigneeAgentId } : {}),
|
|
|
|
|
...(selectedAssigneeUserId ? { assigneeUserId: selectedAssigneeUserId } : {}),
|
2026-02-17 12:24:48 -06:00
|
|
|
...(projectId ? { projectId } : {}),
|
2026-03-13 17:12:25 -05:00
|
|
|
...(projectWorkspaceId ? { projectWorkspaceId } : {}),
|
2026-02-26 10:32:44 -06:00
|
|
|
...(assigneeAdapterOverrides ? { assigneeAdapterOverrides } : {}),
|
2026-03-13 17:12:25 -05:00
|
|
|
...(executionWorkspacePolicy?.enabled ? { executionWorkspacePreference: executionWorkspaceMode } : {}),
|
|
|
|
|
...(executionWorkspaceMode === "reuse_existing" && selectedExecutionWorkspaceId
|
|
|
|
|
? { executionWorkspaceId: selectedExecutionWorkspaceId }
|
|
|
|
|
: {}),
|
2026-03-10 09:03:31 -05:00
|
|
|
...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}),
|
2026-02-17 12:24:48 -06:00
|
|
|
});
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
function handleKeyDown(e: React.KeyboardEvent) {
|
|
|
|
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleSubmit();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 07:13:59 -05:00
|
|
|
function stageFiles(files: File[]) {
|
|
|
|
|
if (files.length === 0) return;
|
|
|
|
|
setStagedFiles((current) => {
|
|
|
|
|
const next = [...current];
|
|
|
|
|
for (const file of files) {
|
2026-03-14 07:21:21 -05:00
|
|
|
if (isTextDocumentFile(file)) {
|
2026-03-14 07:13:59 -05:00
|
|
|
const baseName = fileBaseName(file.name);
|
|
|
|
|
const documentKey = createUniqueDocumentKey(slugifyDocumentKey(baseName), next);
|
|
|
|
|
next.push({
|
|
|
|
|
id: `${file.name}:${file.size}:${file.lastModified}:${documentKey}`,
|
|
|
|
|
file,
|
|
|
|
|
kind: "document",
|
|
|
|
|
documentKey,
|
|
|
|
|
title: titleizeFilename(baseName),
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
next.push({
|
|
|
|
|
id: `${file.name}:${file.size}:${file.lastModified}`,
|
|
|
|
|
file,
|
|
|
|
|
kind: "attachment",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleStageFilesPicked(evt: ChangeEvent<HTMLInputElement>) {
|
|
|
|
|
stageFiles(Array.from(evt.target.files ?? []));
|
|
|
|
|
if (stageFileInputRef.current) {
|
|
|
|
|
stageFileInputRef.current.value = "";
|
2026-02-25 21:36:06 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 07:13:59 -05:00
|
|
|
function handleFileDragEnter(evt: DragEvent<HTMLDivElement>) {
|
|
|
|
|
if (!evt.dataTransfer.types.includes("Files")) return;
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
setIsFileDragOver(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFileDragOver(evt: DragEvent<HTMLDivElement>) {
|
|
|
|
|
if (!evt.dataTransfer.types.includes("Files")) return;
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
evt.dataTransfer.dropEffect = "copy";
|
|
|
|
|
setIsFileDragOver(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFileDragLeave(evt: DragEvent<HTMLDivElement>) {
|
|
|
|
|
if (evt.currentTarget.contains(evt.relatedTarget as Node | null)) return;
|
|
|
|
|
setIsFileDragOver(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFileDrop(evt: DragEvent<HTMLDivElement>) {
|
|
|
|
|
if (!evt.dataTransfer.files.length) return;
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
setIsFileDragOver(false);
|
|
|
|
|
stageFiles(Array.from(evt.dataTransfer.files));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeStagedFile(id: string) {
|
|
|
|
|
setStagedFiles((current) => current.filter((file) => file.id !== id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hasDraft = title.trim().length > 0 || description.trim().length > 0 || stagedFiles.length > 0;
|
2026-02-17 10:53:20 -06:00
|
|
|
const currentStatus = statuses.find((s) => s.value === status) ?? statuses[1]!;
|
|
|
|
|
const currentPriority = priorities.find((p) => p.value === priority);
|
2026-03-12 16:11:37 -05:00
|
|
|
const currentAssignee = selectedAssigneeAgentId
|
|
|
|
|
? (agents ?? []).find((a) => a.id === selectedAssigneeAgentId)
|
|
|
|
|
: null;
|
2026-03-02 14:20:49 -06:00
|
|
|
const currentProject = orderedProjects.find((project) => project.id === projectId);
|
2026-03-17 09:24:28 -05:00
|
|
|
const currentProjectExecutionWorkspacePolicy =
|
|
|
|
|
experimentalSettings?.enableIsolatedWorkspaces === true
|
|
|
|
|
? currentProject?.executionWorkspacePolicy ?? null
|
|
|
|
|
: null;
|
2026-03-10 09:03:31 -05:00
|
|
|
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
|
2026-03-17 07:46:40 -05:00
|
|
|
const deduplicatedReusableWorkspaces = useMemo(() => {
|
|
|
|
|
const workspaces = reusableExecutionWorkspaces ?? [];
|
|
|
|
|
const seen = new Map<string, typeof workspaces[number]>();
|
|
|
|
|
for (const ws of workspaces) {
|
|
|
|
|
const key = ws.cwd ?? ws.id;
|
|
|
|
|
const existing = seen.get(key);
|
|
|
|
|
if (!existing || new Date(ws.lastUsedAt) > new Date(existing.lastUsedAt)) {
|
|
|
|
|
seen.set(key, ws);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Array.from(seen.values());
|
|
|
|
|
}, [reusableExecutionWorkspaces]);
|
|
|
|
|
const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find(
|
2026-03-13 17:12:25 -05:00
|
|
|
(workspace) => workspace.id === selectedExecutionWorkspaceId,
|
|
|
|
|
);
|
2026-02-26 10:32:44 -06:00
|
|
|
const assigneeOptionsTitle =
|
|
|
|
|
assigneeAdapterType === "claude_local"
|
|
|
|
|
? "Claude options"
|
|
|
|
|
: assigneeAdapterType === "codex_local"
|
|
|
|
|
? "Codex options"
|
2026-03-04 16:48:54 -06:00
|
|
|
: assigneeAdapterType === "opencode_local"
|
|
|
|
|
? "OpenCode options"
|
2026-02-26 10:32:44 -06:00
|
|
|
: "Agent options";
|
|
|
|
|
const thinkingEffortOptions =
|
|
|
|
|
assigneeAdapterType === "codex_local"
|
|
|
|
|
? ISSUE_THINKING_EFFORT_OPTIONS.codex_local
|
2026-03-04 16:48:54 -06:00
|
|
|
: assigneeAdapterType === "opencode_local"
|
|
|
|
|
? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local
|
2026-02-26 10:32:44 -06:00
|
|
|
: ISSUE_THINKING_EFFORT_OPTIONS.claude_local;
|
2026-03-05 11:19:56 -06:00
|
|
|
const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [newIssueOpen]);
|
2026-02-26 08:53:03 -06:00
|
|
|
const assigneeOptions = useMemo<InlineEntityOption[]>(
|
2026-03-12 16:11:37 -05:00
|
|
|
() => [
|
|
|
|
|
...currentUserAssigneeOption(currentUserId),
|
|
|
|
|
...sortAgentsByRecency(
|
2026-03-05 11:19:56 -06:00
|
|
|
(agents ?? []).filter((agent) => agent.status !== "terminated"),
|
|
|
|
|
recentAssigneeIds,
|
|
|
|
|
).map((agent) => ({
|
2026-03-12 16:11:37 -05:00
|
|
|
id: assigneeValueFromSelection({ assigneeAgentId: agent.id }),
|
2026-03-05 11:19:56 -06:00
|
|
|
label: agent.name,
|
|
|
|
|
searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`,
|
|
|
|
|
})),
|
2026-03-12 16:11:37 -05:00
|
|
|
],
|
|
|
|
|
[agents, currentUserId, recentAssigneeIds],
|
2026-02-26 08:53:03 -06:00
|
|
|
);
|
|
|
|
|
const projectOptions = useMemo<InlineEntityOption[]>(
|
|
|
|
|
() =>
|
2026-03-02 14:20:49 -06:00
|
|
|
orderedProjects.map((project) => ({
|
2026-02-26 08:53:03 -06:00
|
|
|
id: project.id,
|
|
|
|
|
label: project.name,
|
|
|
|
|
searchText: project.description ?? "",
|
|
|
|
|
})),
|
2026-03-02 14:20:49 -06:00
|
|
|
[orderedProjects],
|
2026-02-26 08:53:03 -06:00
|
|
|
);
|
2026-03-10 21:06:16 -05:00
|
|
|
const savedDraft = loadDraft();
|
|
|
|
|
const hasSavedDraft = Boolean(savedDraft?.title.trim() || savedDraft?.description.trim());
|
|
|
|
|
const canDiscardDraft = hasDraft || hasSavedDraft;
|
|
|
|
|
const createIssueErrorMessage =
|
|
|
|
|
createIssue.error instanceof Error ? createIssue.error.message : "Failed to create issue. Try again.";
|
2026-03-14 07:13:59 -05:00
|
|
|
const stagedDocuments = stagedFiles.filter((file) => file.kind === "document");
|
|
|
|
|
const stagedAttachments = stagedFiles.filter((file) => file.kind === "attachment");
|
2026-03-10 09:03:31 -05:00
|
|
|
|
|
|
|
|
const handleProjectChange = useCallback((nextProjectId: string) => {
|
|
|
|
|
setProjectId(nextProjectId);
|
|
|
|
|
const nextProject = orderedProjects.find((project) => project.id === nextProjectId);
|
|
|
|
|
executionWorkspaceDefaultProjectId.current = nextProjectId || null;
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(nextProject));
|
|
|
|
|
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(nextProject));
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
2026-03-10 09:03:31 -05:00
|
|
|
}, [orderedProjects]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!newIssueOpen || !projectId || executionWorkspaceDefaultProjectId.current === projectId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const project = orderedProjects.find((entry) => entry.id === projectId);
|
|
|
|
|
if (!project) return;
|
|
|
|
|
executionWorkspaceDefaultProjectId.current = projectId;
|
2026-03-13 17:12:25 -05:00
|
|
|
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(project));
|
|
|
|
|
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(project));
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
2026-03-16 18:37:59 -05:00
|
|
|
}, [newIssueOpen, orderedProjects, projectId]);
|
2026-02-26 10:32:44 -06:00
|
|
|
const modelOverrideOptions = useMemo<InlineEntityOption[]>(
|
2026-03-05 15:24:20 +01:00
|
|
|
() => {
|
|
|
|
|
return [...(assigneeAdapterModels ?? [])]
|
|
|
|
|
.sort((a, b) => {
|
2026-03-05 15:52:59 +01:00
|
|
|
const providerA = extractProviderIdWithFallback(a.id);
|
|
|
|
|
const providerB = extractProviderIdWithFallback(b.id);
|
2026-03-05 15:24:20 +01:00
|
|
|
const byProvider = providerA.localeCompare(providerB);
|
|
|
|
|
if (byProvider !== 0) return byProvider;
|
|
|
|
|
return a.id.localeCompare(b.id);
|
|
|
|
|
})
|
|
|
|
|
.map((model) => ({
|
|
|
|
|
id: model.id,
|
|
|
|
|
label: model.label,
|
2026-03-05 15:52:59 +01:00
|
|
|
searchText: `${model.id} ${extractProviderIdWithFallback(model.id)}`,
|
2026-03-05 15:24:20 +01:00
|
|
|
}));
|
|
|
|
|
},
|
2026-02-26 10:32:44 -06:00
|
|
|
[assigneeAdapterModels],
|
|
|
|
|
);
|
2026-02-17 10:53:20 -06:00
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
return (
|
|
|
|
|
<Dialog
|
|
|
|
|
open={newIssueOpen}
|
|
|
|
|
onOpenChange={(open) => {
|
2026-03-10 21:06:16 -05:00
|
|
|
if (!open && !createIssue.isPending) closeNewIssue();
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
}}
|
|
|
|
|
>
|
2026-02-17 10:53:20 -06:00
|
|
|
<DialogContent
|
|
|
|
|
showCloseButton={false}
|
2026-02-23 15:21:04 -06:00
|
|
|
aria-describedby={undefined}
|
2026-02-17 10:53:20 -06:00
|
|
|
className={cn(
|
2026-02-26 08:53:03 -06:00
|
|
|
"p-0 gap-0 flex flex-col max-h-[calc(100dvh-2rem)]",
|
2026-02-17 20:46:12 -06:00
|
|
|
expanded
|
2026-02-26 08:53:03 -06:00
|
|
|
? "sm:max-w-2xl h-[calc(100dvh-2rem)]"
|
2026-02-17 20:46:12 -06:00
|
|
|
: "sm:max-w-lg"
|
2026-02-17 10:53:20 -06:00
|
|
|
)}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
2026-03-10 21:06:16 -05:00
|
|
|
onEscapeKeyDown={(event) => {
|
|
|
|
|
if (createIssue.isPending) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-03-05 17:04:25 -06:00
|
|
|
onPointerDownOutside={(event) => {
|
2026-03-10 21:06:16 -05:00
|
|
|
if (createIssue.isPending) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-05 17:04:25 -06:00
|
|
|
// Radix Dialog's modal DismissableLayer calls preventDefault() on
|
|
|
|
|
// pointerdown events that originate outside the Dialog DOM tree.
|
|
|
|
|
// Popover portals render at the body level (outside the Dialog), so
|
|
|
|
|
// touch events on popover content get their default prevented — which
|
|
|
|
|
// kills scroll gesture recognition on mobile. Telling Radix "this
|
|
|
|
|
// event is handled" skips that preventDefault, restoring touch scroll.
|
|
|
|
|
const target = event.detail.originalEvent.target as HTMLElement | null;
|
|
|
|
|
if (target?.closest("[data-radix-popper-content-wrapper]")) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-02-17 10:53:20 -06:00
|
|
|
>
|
|
|
|
|
{/* Header bar */}
|
2026-02-17 20:46:12 -06:00
|
|
|
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border shrink-0">
|
2026-02-17 10:53:20 -06:00
|
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
2026-02-26 16:33:48 -06:00
|
|
|
<Popover open={companyOpen} onOpenChange={setCompanyOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button
|
|
|
|
|
className={cn(
|
|
|
|
|
"px-1.5 py-0.5 rounded text-xs font-semibold cursor-pointer hover:opacity-80 transition-opacity",
|
|
|
|
|
!dialogCompany?.brandColor && "bg-muted",
|
|
|
|
|
)}
|
|
|
|
|
style={
|
|
|
|
|
dialogCompany?.brandColor
|
|
|
|
|
? {
|
|
|
|
|
backgroundColor: dialogCompany.brandColor,
|
|
|
|
|
color: getContrastTextColor(dialogCompany.brandColor),
|
|
|
|
|
}
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{(dialogCompany?.name ?? "").slice(0, 3).toUpperCase()}
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-48 p-1" align="start">
|
2026-03-04 12:20:14 -06:00
|
|
|
{companies.filter((c) => c.status !== "archived").map((c) => (
|
2026-02-26 16:33:48 -06:00
|
|
|
<button
|
|
|
|
|
key={c.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
|
|
|
|
c.id === effectiveCompanyId && "bg-accent",
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
handleCompanyChange(c.id);
|
|
|
|
|
setCompanyOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
"px-1 py-0.5 rounded text-[10px] font-semibold leading-none",
|
|
|
|
|
!c.brandColor && "bg-muted",
|
|
|
|
|
)}
|
|
|
|
|
style={
|
|
|
|
|
c.brandColor
|
|
|
|
|
? {
|
|
|
|
|
backgroundColor: c.brandColor,
|
|
|
|
|
color: getContrastTextColor(c.brandColor),
|
|
|
|
|
}
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{c.name.slice(0, 3).toUpperCase()}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="truncate">{c.name}</span>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
2026-02-17 10:53:20 -06:00
|
|
|
<span className="text-muted-foreground/60">›</span>
|
|
|
|
|
<span>New issue</span>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</div>
|
2026-02-17 10:53:20 -06:00
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
className="text-muted-foreground"
|
|
|
|
|
onClick={() => setExpanded(!expanded)}
|
2026-03-10 21:06:16 -05:00
|
|
|
disabled={createIssue.isPending}
|
2026-02-17 10:53:20 -06:00
|
|
|
>
|
|
|
|
|
{expanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</Button>
|
2026-02-17 10:53:20 -06:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
className="text-muted-foreground"
|
2026-02-17 20:46:12 -06:00
|
|
|
onClick={() => closeNewIssue()}
|
2026-03-10 21:06:16 -05:00
|
|
|
disabled={createIssue.isPending}
|
2026-02-17 10:53:20 -06:00
|
|
|
>
|
|
|
|
|
<span className="text-lg leading-none">×</span>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</Button>
|
2026-02-17 10:53:20 -06:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Title */}
|
2026-02-17 20:46:12 -06:00
|
|
|
<div className="px-4 pt-4 pb-2 shrink-0">
|
2026-02-26 08:53:03 -06:00
|
|
|
<textarea
|
|
|
|
|
className="w-full text-lg font-semibold bg-transparent outline-none resize-none overflow-hidden placeholder:text-muted-foreground/50"
|
2026-02-17 10:53:20 -06:00
|
|
|
placeholder="Issue title"
|
2026-02-26 08:53:03 -06:00
|
|
|
rows={1}
|
2026-02-17 10:53:20 -06:00
|
|
|
value={title}
|
2026-02-26 08:53:03 -06:00
|
|
|
onChange={(e) => {
|
|
|
|
|
setTitle(e.target.value);
|
|
|
|
|
e.target.style.height = "auto";
|
|
|
|
|
e.target.style.height = `${e.target.scrollHeight}px`;
|
|
|
|
|
}}
|
2026-03-10 21:06:16 -05:00
|
|
|
readOnly={createIssue.isPending}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
onKeyDown={(e) => {
|
2026-03-11 11:57:46 +09:00
|
|
|
if (
|
|
|
|
|
e.key === "Enter" &&
|
|
|
|
|
!e.metaKey &&
|
|
|
|
|
!e.ctrlKey &&
|
|
|
|
|
!e.nativeEvent.isComposing
|
|
|
|
|
) {
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
e.preventDefault();
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
}
|
2026-02-26 08:53:03 -06:00
|
|
|
if (e.key === "Tab" && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
2026-03-12 16:11:37 -05:00
|
|
|
if (assigneeValue) {
|
|
|
|
|
// Assignee already set — skip to project or description
|
|
|
|
|
if (projectId) {
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
} else {
|
|
|
|
|
projectSelectorRef.current?.focus();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
assigneeSelectorRef.current?.focus();
|
|
|
|
|
}
|
2026-02-26 08:53:03 -06:00
|
|
|
}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
}}
|
2026-02-17 10:53:20 -06:00
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-26 08:53:03 -06:00
|
|
|
<div className="px-4 pb-2 shrink-0">
|
2026-03-05 17:18:15 -06:00
|
|
|
<div className="overflow-x-auto overscroll-x-contain">
|
|
|
|
|
<div className="inline-flex items-center gap-2 text-sm text-muted-foreground flex-wrap sm:flex-nowrap sm:min-w-max">
|
2026-02-26 08:53:03 -06:00
|
|
|
<span>For</span>
|
|
|
|
|
<InlineEntitySelector
|
|
|
|
|
ref={assigneeSelectorRef}
|
2026-03-12 16:11:37 -05:00
|
|
|
value={assigneeValue}
|
2026-02-26 08:53:03 -06:00
|
|
|
options={assigneeOptions}
|
|
|
|
|
placeholder="Assignee"
|
2026-03-06 08:04:35 -06:00
|
|
|
disablePortal
|
2026-02-26 08:53:03 -06:00
|
|
|
noneLabel="No assignee"
|
|
|
|
|
searchPlaceholder="Search assignees..."
|
|
|
|
|
emptyMessage="No assignees found."
|
2026-03-12 16:11:37 -05:00
|
|
|
onChange={(value) => {
|
|
|
|
|
const nextAssignee = parseAssigneeValue(value);
|
|
|
|
|
if (nextAssignee.assigneeAgentId) {
|
|
|
|
|
trackRecentAssignee(nextAssignee.assigneeAgentId);
|
|
|
|
|
}
|
|
|
|
|
setAssigneeValue(value);
|
|
|
|
|
}}
|
2026-02-26 08:53:03 -06:00
|
|
|
onConfirm={() => {
|
2026-03-12 16:11:37 -05:00
|
|
|
if (projectId) {
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
} else {
|
|
|
|
|
projectSelectorRef.current?.focus();
|
|
|
|
|
}
|
2026-02-26 08:53:03 -06:00
|
|
|
}}
|
|
|
|
|
renderTriggerValue={(option) =>
|
2026-03-12 16:11:37 -05:00
|
|
|
option ? (
|
|
|
|
|
currentAssignee ? (
|
|
|
|
|
<>
|
|
|
|
|
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
2026-02-26 08:53:03 -06:00
|
|
|
<span className="truncate">{option.label}</span>
|
2026-03-12 16:11:37 -05:00
|
|
|
)
|
2026-02-26 08:53:03 -06:00
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">Assignee</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
renderOption={(option) => {
|
|
|
|
|
if (!option.id) return <span className="truncate">{option.label}</span>;
|
2026-03-12 16:11:37 -05:00
|
|
|
const assignee = parseAssigneeValue(option.id).assigneeAgentId
|
|
|
|
|
? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId)
|
|
|
|
|
: null;
|
2026-02-26 08:53:03 -06:00
|
|
|
return (
|
|
|
|
|
<>
|
2026-03-12 16:11:37 -05:00
|
|
|
{assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
2026-02-26 08:53:03 -06:00
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<span>in</span>
|
|
|
|
|
<InlineEntitySelector
|
|
|
|
|
ref={projectSelectorRef}
|
|
|
|
|
value={projectId}
|
|
|
|
|
options={projectOptions}
|
|
|
|
|
placeholder="Project"
|
2026-03-06 08:04:35 -06:00
|
|
|
disablePortal
|
2026-02-26 08:53:03 -06:00
|
|
|
noneLabel="No project"
|
|
|
|
|
searchPlaceholder="Search projects..."
|
|
|
|
|
emptyMessage="No projects found."
|
2026-03-10 09:03:31 -05:00
|
|
|
onChange={handleProjectChange}
|
2026-02-26 08:53:03 -06:00
|
|
|
onConfirm={() => {
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
}}
|
|
|
|
|
renderTriggerValue={(option) =>
|
|
|
|
|
option && currentProject ? (
|
|
|
|
|
<>
|
|
|
|
|
<span
|
|
|
|
|
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
|
|
|
|
style={{ backgroundColor: currentProject.color ?? "#6366f1" }}
|
|
|
|
|
/>
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">Project</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
renderOption={(option) => {
|
|
|
|
|
if (!option.id) return <span className="truncate">{option.label}</span>;
|
2026-03-02 14:20:49 -06:00
|
|
|
const project = orderedProjects.find((item) => item.id === option.id);
|
2026-02-26 08:53:03 -06:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<span
|
|
|
|
|
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
|
|
|
|
style={{ backgroundColor: project?.color ?? "#6366f1" }}
|
|
|
|
|
/>
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-16 18:37:59 -05:00
|
|
|
{currentProject && (
|
2026-03-17 09:24:28 -05:00
|
|
|
<div className="px-4 py-3 shrink-0 space-y-2">
|
2026-03-13 17:12:25 -05:00
|
|
|
{currentProjectSupportsExecutionWorkspace && (
|
2026-03-17 09:24:28 -05:00
|
|
|
<div className="space-y-1.5">
|
2026-03-13 17:12:25 -05:00
|
|
|
<div className="text-xs font-medium">Execution workspace</div>
|
|
|
|
|
<div className="text-[11px] text-muted-foreground">
|
|
|
|
|
Control whether this issue runs in the shared workspace, a new isolated workspace, or an existing one.
|
|
|
|
|
</div>
|
|
|
|
|
<select
|
|
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1.5 text-xs outline-none"
|
|
|
|
|
value={executionWorkspaceMode}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
setExecutionWorkspaceMode(e.target.value);
|
|
|
|
|
if (e.target.value !== "reuse_existing") {
|
|
|
|
|
setSelectedExecutionWorkspaceId("");
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{EXECUTION_WORKSPACE_MODES.map((option) => (
|
|
|
|
|
<option key={option.value} value={option.value}>
|
|
|
|
|
{option.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
{executionWorkspaceMode === "reuse_existing" && (
|
|
|
|
|
<select
|
|
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1.5 text-xs outline-none"
|
|
|
|
|
value={selectedExecutionWorkspaceId}
|
|
|
|
|
onChange={(e) => setSelectedExecutionWorkspaceId(e.target.value)}
|
|
|
|
|
>
|
|
|
|
|
<option value="">Choose an existing workspace</option>
|
2026-03-17 07:46:40 -05:00
|
|
|
{deduplicatedReusableWorkspaces.map((workspace) => (
|
2026-03-13 17:12:25 -05:00
|
|
|
<option key={workspace.id} value={workspace.id}>
|
|
|
|
|
{workspace.name} · {workspace.status} · {workspace.branchName ?? workspace.cwd ?? workspace.id.slice(0, 8)}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
2026-03-10 09:03:31 -05:00
|
|
|
)}
|
2026-03-13 17:12:25 -05:00
|
|
|
{executionWorkspaceMode === "reuse_existing" && selectedReusableExecutionWorkspace && (
|
|
|
|
|
<div className="text-[11px] text-muted-foreground">
|
|
|
|
|
Reusing {selectedReusableExecutionWorkspace.name} from {selectedReusableExecutionWorkspace.branchName ?? selectedReusableExecutionWorkspace.cwd ?? "existing execution workspace"}.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-10 09:03:31 -05:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-26 10:32:44 -06:00
|
|
|
{supportsAssigneeOverrides && (
|
|
|
|
|
<div className="px-4 pb-2 shrink-0">
|
|
|
|
|
<button
|
|
|
|
|
className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
|
|
|
|
|
onClick={() => setAssigneeOptionsOpen((open) => !open)}
|
|
|
|
|
>
|
|
|
|
|
{assigneeOptionsOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
|
|
|
|
{assigneeOptionsTitle}
|
|
|
|
|
</button>
|
|
|
|
|
{assigneeOptionsOpen && (
|
|
|
|
|
<div className="mt-2 rounded-md border border-border p-3 bg-muted/20 space-y-3">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<div className="text-xs text-muted-foreground">Model</div>
|
|
|
|
|
<InlineEntitySelector
|
|
|
|
|
value={assigneeModelOverride}
|
|
|
|
|
options={modelOverrideOptions}
|
|
|
|
|
placeholder="Default model"
|
2026-03-06 08:04:35 -06:00
|
|
|
disablePortal
|
2026-02-26 10:32:44 -06:00
|
|
|
noneLabel="Default model"
|
|
|
|
|
searchPlaceholder="Search models..."
|
|
|
|
|
emptyMessage="No models found."
|
|
|
|
|
onChange={setAssigneeModelOverride}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<div className="text-xs text-muted-foreground">Thinking effort</div>
|
|
|
|
|
<div className="flex items-center gap-1.5 flex-wrap">
|
|
|
|
|
{thinkingEffortOptions.map((option) => (
|
|
|
|
|
<button
|
|
|
|
|
key={option.value || "default"}
|
|
|
|
|
className={cn(
|
|
|
|
|
"px-2 py-1 rounded-md text-xs border border-border hover:bg-accent/50 transition-colors",
|
|
|
|
|
assigneeThinkingEffort === option.value && "bg-accent"
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => setAssigneeThinkingEffort(option.value)}
|
|
|
|
|
>
|
|
|
|
|
{option.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-26 16:33:48 -06:00
|
|
|
{assigneeAdapterType === "claude_local" && (
|
|
|
|
|
<div className="flex items-center justify-between rounded-md border border-border px-2 py-1.5">
|
|
|
|
|
<div className="text-xs text-muted-foreground">Enable Chrome (--chrome)</div>
|
|
|
|
|
<button
|
|
|
|
|
className={cn(
|
|
|
|
|
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
|
|
|
|
|
assigneeChrome ? "bg-green-600" : "bg-muted"
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => setAssigneeChrome((value) => !value)}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
|
|
|
|
|
assigneeChrome ? "translate-x-4.5" : "translate-x-0.5"
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-26 10:32:44 -06:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
{/* Description */}
|
2026-03-14 07:13:59 -05:00
|
|
|
<div
|
|
|
|
|
className={cn("px-4 pb-2 overflow-y-auto min-h-0 border-t border-border/60 pt-3", expanded ? "flex-1" : "")}
|
|
|
|
|
onDragEnter={handleFileDragEnter}
|
|
|
|
|
onDragOver={handleFileDragOver}
|
|
|
|
|
onDragLeave={handleFileDragLeave}
|
|
|
|
|
onDrop={handleFileDrop}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"rounded-md transition-colors",
|
|
|
|
|
isFileDragOver && "bg-accent/20",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<MarkdownEditor
|
|
|
|
|
ref={descriptionEditorRef}
|
|
|
|
|
value={description}
|
|
|
|
|
onChange={setDescription}
|
|
|
|
|
placeholder="Add description..."
|
|
|
|
|
bordered={false}
|
|
|
|
|
mentions={mentionOptions}
|
|
|
|
|
contentClassName={cn("text-sm text-muted-foreground pb-12", expanded ? "min-h-[220px]" : "min-h-[120px]")}
|
|
|
|
|
imageUploadHandler={async (file) => {
|
|
|
|
|
const asset = await uploadDescriptionImage.mutateAsync(file);
|
|
|
|
|
return asset.contentPath;
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{stagedFiles.length > 0 ? (
|
|
|
|
|
<div className="mt-4 space-y-3 rounded-lg border border-border/70 p-3">
|
|
|
|
|
{stagedDocuments.length > 0 ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="text-xs font-medium text-muted-foreground">Documents</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{stagedDocuments.map((file) => (
|
|
|
|
|
<div key={file.id} className="flex items-start justify-between gap-3 rounded-md border border-border/70 px-3 py-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="rounded-full border border-border px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
|
|
|
|
|
{file.documentKey}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="truncate text-sm">{file.file.name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 flex items-center gap-2 text-[11px] text-muted-foreground">
|
|
|
|
|
<FileText className="h-3.5 w-3.5" />
|
|
|
|
|
<span>{file.title || file.file.name}</span>
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span>{formatFileSize(file.file)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
className="shrink-0 text-muted-foreground"
|
|
|
|
|
onClick={() => removeStagedFile(file.id)}
|
|
|
|
|
disabled={createIssue.isPending}
|
|
|
|
|
title="Remove document"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{stagedAttachments.length > 0 ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="text-xs font-medium text-muted-foreground">Attachments</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{stagedAttachments.map((file) => (
|
|
|
|
|
<div key={file.id} className="flex items-start justify-between gap-3 rounded-md border border-border/70 px-3 py-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Paperclip className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
|
|
|
<span className="truncate text-sm">{file.file.name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
|
|
|
{file.file.type || "application/octet-stream"} • {formatFileSize(file.file)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
className="shrink-0 text-muted-foreground"
|
|
|
|
|
onClick={() => removeStagedFile(file.id)}
|
|
|
|
|
disabled={createIssue.isPending}
|
|
|
|
|
title="Remove attachment"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-02-17 10:53:20 -06:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Property chips bar */}
|
2026-02-17 20:46:12 -06:00
|
|
|
<div className="flex items-center gap-1.5 px-4 py-2 border-t border-border flex-wrap shrink-0">
|
2026-02-17 10:53:20 -06:00
|
|
|
{/* Status chip */}
|
|
|
|
|
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
|
|
|
|
<CircleDot className={cn("h-3 w-3", currentStatus.color)} />
|
|
|
|
|
{currentStatus.label}
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-36 p-1" align="start">
|
|
|
|
|
{statuses.map((s) => (
|
|
|
|
|
<button
|
|
|
|
|
key={s.value}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
|
|
|
|
s.value === status && "bg-accent"
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => { setStatus(s.value); setStatusOpen(false); }}
|
|
|
|
|
>
|
|
|
|
|
<CircleDot className={cn("h-3 w-3", s.color)} />
|
|
|
|
|
{s.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
|
|
|
|
|
{/* Priority chip */}
|
|
|
|
|
<Popover open={priorityOpen} onOpenChange={setPriorityOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
|
|
|
|
{currentPriority ? (
|
|
|
|
|
<>
|
|
|
|
|
<currentPriority.icon className={cn("h-3 w-3", currentPriority.color)} />
|
|
|
|
|
{currentPriority.label}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Minus className="h-3 w-3 text-muted-foreground" />
|
|
|
|
|
Priority
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-36 p-1" align="start">
|
|
|
|
|
{priorities.map((p) => (
|
|
|
|
|
<button
|
|
|
|
|
key={p.value}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
|
|
|
|
p.value === priority && "bg-accent"
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => { setPriority(p.value); setPriorityOpen(false); }}
|
|
|
|
|
>
|
|
|
|
|
<p.icon className={cn("h-3 w-3", p.color)} />
|
|
|
|
|
{p.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
|
|
|
|
|
{/* Labels chip (placeholder) */}
|
|
|
|
|
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground">
|
|
|
|
|
<Tag className="h-3 w-3" />
|
|
|
|
|
Labels
|
|
|
|
|
</button>
|
|
|
|
|
|
2026-02-25 21:36:06 -06:00
|
|
|
<input
|
2026-03-14 07:13:59 -05:00
|
|
|
ref={stageFileInputRef}
|
2026-02-25 21:36:06 -06:00
|
|
|
type="file"
|
2026-03-14 07:13:59 -05:00
|
|
|
accept={STAGED_FILE_ACCEPT}
|
2026-02-25 21:36:06 -06:00
|
|
|
className="hidden"
|
2026-03-14 07:13:59 -05:00
|
|
|
onChange={handleStageFilesPicked}
|
|
|
|
|
multiple
|
2026-02-25 21:36:06 -06:00
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground"
|
2026-03-14 07:13:59 -05:00
|
|
|
onClick={() => stageFileInputRef.current?.click()}
|
|
|
|
|
disabled={createIssue.isPending}
|
2026-02-25 21:36:06 -06:00
|
|
|
>
|
|
|
|
|
<Paperclip className="h-3 w-3" />
|
2026-03-14 07:21:21 -05:00
|
|
|
Upload
|
2026-02-25 21:36:06 -06:00
|
|
|
</button>
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
{/* More (dates) */}
|
|
|
|
|
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button className="inline-flex items-center justify-center rounded-md border border-border p-1 text-xs hover:bg-accent/50 transition-colors text-muted-foreground">
|
|
|
|
|
<MoreHorizontal className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-44 p-1" align="start">
|
|
|
|
|
<button className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-muted-foreground">
|
|
|
|
|
<Calendar className="h-3 w-3" />
|
|
|
|
|
Start date
|
|
|
|
|
</button>
|
|
|
|
|
<button className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-muted-foreground">
|
|
|
|
|
<Calendar className="h-3 w-3" />
|
|
|
|
|
Due date
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
2026-02-17 20:46:12 -06:00
|
|
|
<div className="flex items-center justify-between px-4 py-2.5 border-t border-border shrink-0">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-muted-foreground"
|
|
|
|
|
onClick={discardDraft}
|
2026-03-10 21:06:16 -05:00
|
|
|
disabled={createIssue.isPending || !canDiscardDraft}
|
2026-02-17 20:46:12 -06:00
|
|
|
>
|
|
|
|
|
Discard Draft
|
|
|
|
|
</Button>
|
2026-03-10 21:06:16 -05:00
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="min-h-5 text-right">
|
|
|
|
|
{createIssue.isPending ? (
|
|
|
|
|
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
|
|
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
|
|
|
Creating issue...
|
|
|
|
|
</span>
|
|
|
|
|
) : createIssue.isError ? (
|
|
|
|
|
<span className="text-xs text-destructive">{createIssueErrorMessage}</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
className="min-w-[8.5rem] disabled:opacity-100"
|
|
|
|
|
disabled={!title.trim() || createIssue.isPending}
|
|
|
|
|
onClick={handleSubmit}
|
|
|
|
|
aria-busy={createIssue.isPending}
|
|
|
|
|
>
|
|
|
|
|
<span className="inline-flex items-center justify-center gap-1.5">
|
|
|
|
|
{createIssue.isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
|
|
|
|
|
<span>{createIssue.isPending ? "Creating..." : "Create Issue"}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-02-17 10:53:20 -06:00
|
|
|
</div>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|