Polish UI components and rework AgentConfigForm
Major AgentConfigForm rework with improved adapter configuration fields and layout. Refine sidebar, breadcrumbs, and card/tab components for visual consistency. Clean up page layouts across Activity, Agents, Approvals, Costs, Dashboard, Goals, Inbox, Issues, Org, and Projects pages. Minor heartbeat-run CLI fix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -174,6 +174,8 @@ export async function heartbeatRun(opts: HeartbeatRunOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
if (typeof logResult.nextOffset === "number") {
|
if (typeof logResult.nextOffset === "number") {
|
||||||
logOffset = logResult.nextOffset;
|
logOffset = logResult.nextOffset;
|
||||||
|
} else if (logResult.content) {
|
||||||
|
logOffset += Buffer.byteLength(logResult.content, "utf8");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { AGENT_ADAPTER_TYPES } from "@paperclip/shared";
|
import { AGENT_ADAPTER_TYPES } from "@paperclip/shared";
|
||||||
import type { Agent } from "@paperclip/shared";
|
import type { Agent } from "@paperclip/shared";
|
||||||
import type { AdapterModel } from "../api/agents";
|
import type { AdapterModel } from "../api/agents";
|
||||||
|
import { agentsApi } from "../api/agents";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { FolderOpen, Heart, ChevronDown } from "lucide-react";
|
import { FolderOpen, Heart, ChevronDown } from "lucide-react";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +21,6 @@ import {
|
|||||||
DraftInput,
|
DraftInput,
|
||||||
DraftTextarea,
|
DraftTextarea,
|
||||||
DraftNumberInput,
|
DraftNumberInput,
|
||||||
HintIcon,
|
|
||||||
help,
|
help,
|
||||||
adapterLabels,
|
adapterLabels,
|
||||||
} from "./agent-config-primitives";
|
} from "./agent-config-primitives";
|
||||||
@@ -73,9 +75,37 @@ type AgentConfigFormProps = {
|
|||||||
mode: "edit";
|
mode: "edit";
|
||||||
agent: Agent;
|
agent: Agent;
|
||||||
onSave: (patch: Record<string, unknown>) => void;
|
onSave: (patch: Record<string, unknown>) => void;
|
||||||
|
isSaving?: boolean;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* ---- Edit mode overlay (dirty tracking) ---- */
|
||||||
|
|
||||||
|
interface Overlay {
|
||||||
|
identity: Record<string, unknown>;
|
||||||
|
adapterType?: string;
|
||||||
|
adapterConfig: Record<string, unknown>;
|
||||||
|
heartbeat: Record<string, unknown>;
|
||||||
|
runtime: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyOverlay: Overlay = {
|
||||||
|
identity: {},
|
||||||
|
adapterConfig: {},
|
||||||
|
heartbeat: {},
|
||||||
|
runtime: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
function isOverlayDirty(o: Overlay): boolean {
|
||||||
|
return (
|
||||||
|
Object.keys(o.identity).length > 0 ||
|
||||||
|
o.adapterType !== undefined ||
|
||||||
|
Object.keys(o.adapterConfig).length > 0 ||
|
||||||
|
Object.keys(o.heartbeat).length > 0 ||
|
||||||
|
Object.keys(o.runtime).length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Shared input class ---- */
|
/* ---- Shared input class ---- */
|
||||||
const inputClass =
|
const inputClass =
|
||||||
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
|
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
|
||||||
@@ -83,20 +113,87 @@ const inputClass =
|
|||||||
/* ---- Form ---- */
|
/* ---- Form ---- */
|
||||||
|
|
||||||
export function AgentConfigForm(props: AgentConfigFormProps) {
|
export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||||
const { mode, adapterModels } = props;
|
const { mode, adapterModels: externalModels } = props;
|
||||||
const isCreate = mode === "create";
|
const isCreate = mode === "create";
|
||||||
|
|
||||||
// Resolve adapter type + config + heartbeat from props
|
// ---- Edit mode: overlay for dirty tracking ----
|
||||||
const adapterType = isCreate ? props.values.adapterType : props.agent.adapterType;
|
const [overlay, setOverlay] = useState<Overlay>(emptyOverlay);
|
||||||
const isLocal = adapterType === "claude_local" || adapterType === "codex_local";
|
const agentRef = useRef<Agent | null>(null);
|
||||||
|
|
||||||
// Edit mode: extract from agent
|
// Clear overlay when agent data refreshes (after save)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCreate) {
|
||||||
|
if (agentRef.current !== null && props.agent !== agentRef.current) {
|
||||||
|
setOverlay({ ...emptyOverlay });
|
||||||
|
}
|
||||||
|
agentRef.current = props.agent;
|
||||||
|
}
|
||||||
|
}, [isCreate, !isCreate ? props.agent : undefined]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const isDirty = !isCreate && isOverlayDirty(overlay);
|
||||||
|
|
||||||
|
/** Read effective value: overlay if dirty, else original */
|
||||||
|
function eff<T>(group: keyof Omit<Overlay, "adapterType">, field: string, original: T): T {
|
||||||
|
const o = overlay[group];
|
||||||
|
if (field in o) return o[field] as T;
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mark field dirty in overlay */
|
||||||
|
function mark(group: keyof Omit<Overlay, "adapterType">, field: string, value: unknown) {
|
||||||
|
setOverlay((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[group]: { ...prev[group], [field]: value },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build accumulated patch and send to parent */
|
||||||
|
function handleSave() {
|
||||||
|
if (isCreate || !isDirty) return;
|
||||||
|
const agent = props.agent;
|
||||||
|
const patch: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (Object.keys(overlay.identity).length > 0) {
|
||||||
|
Object.assign(patch, overlay.identity);
|
||||||
|
}
|
||||||
|
if (overlay.adapterType !== undefined) {
|
||||||
|
patch.adapterType = overlay.adapterType;
|
||||||
|
}
|
||||||
|
if (Object.keys(overlay.adapterConfig).length > 0) {
|
||||||
|
const existing = (agent.adapterConfig ?? {}) as Record<string, unknown>;
|
||||||
|
patch.adapterConfig = { ...existing, ...overlay.adapterConfig };
|
||||||
|
}
|
||||||
|
if (Object.keys(overlay.heartbeat).length > 0) {
|
||||||
|
const existingRc = (agent.runtimeConfig ?? {}) as Record<string, unknown>;
|
||||||
|
const existingHb = (existingRc.heartbeat ?? {}) as Record<string, unknown>;
|
||||||
|
patch.runtimeConfig = { ...existingRc, heartbeat: { ...existingHb, ...overlay.heartbeat } };
|
||||||
|
}
|
||||||
|
if (Object.keys(overlay.runtime).length > 0) {
|
||||||
|
Object.assign(patch, overlay.runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onSave(patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Resolve values ----
|
||||||
const config = !isCreate ? ((props.agent.adapterConfig ?? {}) as Record<string, unknown>) : {};
|
const config = !isCreate ? ((props.agent.adapterConfig ?? {}) as Record<string, unknown>) : {};
|
||||||
const runtimeConfig = !isCreate ? ((props.agent.runtimeConfig ?? {}) as Record<string, unknown>) : {};
|
const runtimeConfig = !isCreate ? ((props.agent.runtimeConfig ?? {}) as Record<string, unknown>) : {};
|
||||||
const heartbeat = !isCreate ? ((runtimeConfig.heartbeat ?? {}) as Record<string, unknown>) : {};
|
const heartbeat = !isCreate ? ((runtimeConfig.heartbeat ?? {}) as Record<string, unknown>) : {};
|
||||||
|
|
||||||
// Section toggle state
|
const adapterType = isCreate
|
||||||
const [adapterAdvancedOpen, setAdapterAdvancedOpen] = useState(!isCreate);
|
? props.values.adapterType
|
||||||
|
: overlay.adapterType ?? props.agent.adapterType;
|
||||||
|
const isLocal = adapterType === "claude_local" || adapterType === "codex_local";
|
||||||
|
|
||||||
|
// Fetch adapter models for the effective adapter type
|
||||||
|
const { data: fetchedModels } = useQuery({
|
||||||
|
queryKey: ["adapter-models", adapterType],
|
||||||
|
queryFn: () => agentsApi.adapterModels(adapterType),
|
||||||
|
});
|
||||||
|
const models = fetchedModels ?? externalModels ?? [];
|
||||||
|
|
||||||
|
// Section toggle state — advanced always starts collapsed
|
||||||
|
const [adapterAdvancedOpen, setAdapterAdvancedOpen] = useState(false);
|
||||||
const [heartbeatOpen, setHeartbeatOpen] = useState(!isCreate);
|
const [heartbeatOpen, setHeartbeatOpen] = useState(!isCreate);
|
||||||
|
|
||||||
// Popover states
|
// Popover states
|
||||||
@@ -108,27 +205,29 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
? (patch: Partial<CreateConfigValues>) => props.onChange(patch)
|
? (patch: Partial<CreateConfigValues>) => props.onChange(patch)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Edit mode helpers
|
|
||||||
const saveConfig = !isCreate
|
|
||||||
? (field: string, value: unknown) =>
|
|
||||||
props.onSave({ adapterConfig: { ...config, [field]: value } })
|
|
||||||
: null;
|
|
||||||
const saveHeartbeat = !isCreate
|
|
||||||
? (field: string, value: unknown) =>
|
|
||||||
props.onSave({
|
|
||||||
runtimeConfig: { ...runtimeConfig, heartbeat: { ...heartbeat, [field]: value } },
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
const saveIdentity = !isCreate
|
|
||||||
? (field: string, value: unknown) => props.onSave({ [field]: value })
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Current model for display
|
// Current model for display
|
||||||
const currentModelId = isCreate ? val!.model : String(config.model ?? "");
|
const currentModelId = isCreate
|
||||||
const selectedModel = (adapterModels ?? []).find((m) => m.id === currentModelId);
|
? val!.model
|
||||||
|
: eff("adapterConfig", "model", String(config.model ?? ""));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="relative">
|
||||||
|
{/* ---- Floating Save button (edit mode, when dirty) ---- */}
|
||||||
|
{isDirty && (
|
||||||
|
<div className="sticky top-0 z-10 flex items-center justify-end px-4 py-2 bg-background/90 backdrop-blur-sm border-b border-primary/20">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-xs text-muted-foreground">Unsaved changes</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!isCreate && props.isSaving}
|
||||||
|
>
|
||||||
|
{!isCreate && props.isSaving ? "Saving..." : "Save"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ---- Identity (edit only) ---- */}
|
{/* ---- Identity (edit only) ---- */}
|
||||||
{!isCreate && (
|
{!isCreate && (
|
||||||
<div className="border-b border-border">
|
<div className="border-b border-border">
|
||||||
@@ -136,24 +235,24 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
<div className="px-4 pb-3 space-y-3">
|
<div className="px-4 pb-3 space-y-3">
|
||||||
<Field label="Name" hint={help.name}>
|
<Field label="Name" hint={help.name}>
|
||||||
<DraftInput
|
<DraftInput
|
||||||
value={props.agent.name}
|
value={eff("identity", "name", props.agent.name)}
|
||||||
onCommit={(v) => saveIdentity!("name", v)}
|
onCommit={(v) => mark("identity", "name", v)}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder="Agent name"
|
placeholder="Agent name"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Title" hint={help.title}>
|
<Field label="Title" hint={help.title}>
|
||||||
<DraftInput
|
<DraftInput
|
||||||
value={props.agent.title ?? ""}
|
value={eff("identity", "title", props.agent.title ?? "")}
|
||||||
onCommit={(v) => saveIdentity!("title", v || null)}
|
onCommit={(v) => mark("identity", "title", v || null)}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
placeholder="e.g. VP of Engineering"
|
placeholder="e.g. VP of Engineering"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Capabilities" hint={help.capabilities}>
|
<Field label="Capabilities" hint={help.capabilities}>
|
||||||
<DraftTextarea
|
<DraftTextarea
|
||||||
value={props.agent.capabilities ?? ""}
|
value={eff("identity", "capabilities", props.agent.capabilities ?? "")}
|
||||||
onCommit={(v) => saveIdentity!("capabilities", v || null)}
|
onCommit={(v) => mark("identity", "capabilities", v || null)}
|
||||||
placeholder="Describe what this agent can do..."
|
placeholder="Describe what this agent can do..."
|
||||||
minRows={2}
|
minRows={2}
|
||||||
/>
|
/>
|
||||||
@@ -167,9 +266,17 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
<Field label="Adapter" hint={help.adapterType}>
|
<Field label="Adapter" hint={help.adapterType}>
|
||||||
<AdapterTypeDropdown
|
<AdapterTypeDropdown
|
||||||
value={adapterType}
|
value={adapterType}
|
||||||
onChange={(t) =>
|
onChange={(t) => {
|
||||||
isCreate ? set!({ adapterType: t }) : props.onSave({ adapterType: t })
|
if (isCreate) {
|
||||||
}
|
set!({ adapterType: t });
|
||||||
|
} else {
|
||||||
|
setOverlay((prev) => ({
|
||||||
|
...prev,
|
||||||
|
adapterType: t,
|
||||||
|
adapterConfig: {}, // clear adapter config when type changes
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,9 +293,15 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
<div className="flex items-center gap-2 rounded-md border border-border px-2.5 py-1.5">
|
<div className="flex items-center gap-2 rounded-md border border-border px-2.5 py-1.5">
|
||||||
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||||
<DraftInput
|
<DraftInput
|
||||||
value={isCreate ? val!.cwd : String(config.cwd ?? "")}
|
value={
|
||||||
|
isCreate
|
||||||
|
? val!.cwd
|
||||||
|
: eff("adapterConfig", "cwd", String(config.cwd ?? ""))
|
||||||
|
}
|
||||||
onCommit={(v) =>
|
onCommit={(v) =>
|
||||||
isCreate ? set!({ cwd: v }) : saveConfig!("cwd", v || undefined)
|
isCreate
|
||||||
|
? set!({ cwd: v })
|
||||||
|
: mark("adapterConfig", "cwd", v || undefined)
|
||||||
}
|
}
|
||||||
immediate={isCreate}
|
immediate={isCreate}
|
||||||
className="w-full bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"
|
className="w-full bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"
|
||||||
@@ -202,7 +315,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
// @ts-expect-error -- showDirectoryPicker is not in all TS lib defs yet
|
// @ts-expect-error -- showDirectoryPicker is not in all TS lib defs yet
|
||||||
const handle = await window.showDirectoryPicker({ mode: "read" });
|
const handle = await window.showDirectoryPicker({ mode: "read" });
|
||||||
if (isCreate) set!({ cwd: handle.name });
|
if (isCreate) set!({ cwd: handle.name });
|
||||||
else saveConfig!("cwd", handle.name);
|
else mark("adapterConfig", "cwd", handle.name);
|
||||||
} catch {
|
} catch {
|
||||||
// user cancelled or API unsupported
|
// user cancelled or API unsupported
|
||||||
}
|
}
|
||||||
@@ -226,8 +339,14 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DraftTextarea
|
<DraftTextarea
|
||||||
value={String(config.promptTemplate ?? "")}
|
value={eff(
|
||||||
onCommit={(v) => saveConfig!("promptTemplate", v || undefined)}
|
"adapterConfig",
|
||||||
|
"promptTemplate",
|
||||||
|
String(config.promptTemplate ?? ""),
|
||||||
|
)}
|
||||||
|
onCommit={(v) =>
|
||||||
|
mark("adapterConfig", "promptTemplate", v || undefined)
|
||||||
|
}
|
||||||
placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
|
placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
|
||||||
minRows={4}
|
minRows={4}
|
||||||
/>
|
/>
|
||||||
@@ -243,12 +362,16 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
checked={
|
checked={
|
||||||
isCreate
|
isCreate
|
||||||
? val!.dangerouslySkipPermissions
|
? val!.dangerouslySkipPermissions
|
||||||
: config.dangerouslySkipPermissions !== false
|
: eff(
|
||||||
|
"adapterConfig",
|
||||||
|
"dangerouslySkipPermissions",
|
||||||
|
config.dangerouslySkipPermissions !== false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
isCreate
|
isCreate
|
||||||
? set!({ dangerouslySkipPermissions: v })
|
? set!({ dangerouslySkipPermissions: v })
|
||||||
: saveConfig!("dangerouslySkipPermissions", v)
|
: mark("adapterConfig", "dangerouslySkipPermissions", v)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -262,20 +385,30 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
checked={
|
checked={
|
||||||
isCreate
|
isCreate
|
||||||
? val!.dangerouslyBypassSandbox
|
? val!.dangerouslyBypassSandbox
|
||||||
: config.dangerouslyBypassApprovalsAndSandbox !== false
|
: eff(
|
||||||
|
"adapterConfig",
|
||||||
|
"dangerouslyBypassApprovalsAndSandbox",
|
||||||
|
config.dangerouslyBypassApprovalsAndSandbox !== false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
isCreate
|
isCreate
|
||||||
? set!({ dangerouslyBypassSandbox: v })
|
? set!({ dangerouslyBypassSandbox: v })
|
||||||
: saveConfig!("dangerouslyBypassApprovalsAndSandbox", v)
|
: mark("adapterConfig", "dangerouslyBypassApprovalsAndSandbox", v)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ToggleField
|
<ToggleField
|
||||||
label="Enable search"
|
label="Enable search"
|
||||||
hint={help.search}
|
hint={help.search}
|
||||||
checked={isCreate ? val!.search : !!config.search}
|
checked={
|
||||||
|
isCreate
|
||||||
|
? val!.search
|
||||||
|
: eff("adapterConfig", "search", !!config.search)
|
||||||
|
}
|
||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
isCreate ? set!({ search: v }) : saveConfig!("search", v)
|
isCreate
|
||||||
|
? set!({ search: v })
|
||||||
|
: mark("adapterConfig", "search", v)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -286,9 +419,15 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
<>
|
<>
|
||||||
<Field label="Command" hint={help.command}>
|
<Field label="Command" hint={help.command}>
|
||||||
<DraftInput
|
<DraftInput
|
||||||
value={isCreate ? val!.command : String(config.command ?? "")}
|
value={
|
||||||
|
isCreate
|
||||||
|
? val!.command
|
||||||
|
: eff("adapterConfig", "command", String(config.command ?? ""))
|
||||||
|
}
|
||||||
onCommit={(v) =>
|
onCommit={(v) =>
|
||||||
isCreate ? set!({ command: v }) : saveConfig!("command", v || undefined)
|
isCreate
|
||||||
|
? set!({ command: v })
|
||||||
|
: mark("adapterConfig", "command", v || undefined)
|
||||||
}
|
}
|
||||||
immediate={isCreate}
|
immediate={isCreate}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
@@ -297,11 +436,16 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label="Args (comma-separated)" hint={help.args}>
|
<Field label="Args (comma-separated)" hint={help.args}>
|
||||||
<DraftInput
|
<DraftInput
|
||||||
value={isCreate ? val!.args : String(config.args ?? "")}
|
value={
|
||||||
|
isCreate
|
||||||
|
? val!.args
|
||||||
|
: eff("adapterConfig", "args", String(config.args ?? ""))
|
||||||
|
}
|
||||||
onCommit={(v) =>
|
onCommit={(v) =>
|
||||||
isCreate
|
isCreate
|
||||||
? set!({ args: v })
|
? set!({ args: v })
|
||||||
: saveConfig!(
|
: mark(
|
||||||
|
"adapterConfig",
|
||||||
"args",
|
"args",
|
||||||
v
|
v
|
||||||
? v
|
? v
|
||||||
@@ -323,9 +467,15 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
{adapterType === "http" && (
|
{adapterType === "http" && (
|
||||||
<Field label="Webhook URL" hint={help.webhookUrl}>
|
<Field label="Webhook URL" hint={help.webhookUrl}>
|
||||||
<DraftInput
|
<DraftInput
|
||||||
value={isCreate ? val!.url : String(config.url ?? "")}
|
value={
|
||||||
|
isCreate
|
||||||
|
? val!.url
|
||||||
|
: eff("adapterConfig", "url", String(config.url ?? ""))
|
||||||
|
}
|
||||||
onCommit={(v) =>
|
onCommit={(v) =>
|
||||||
isCreate ? set!({ url: v }) : saveConfig!("url", v || undefined)
|
isCreate
|
||||||
|
? set!({ url: v })
|
||||||
|
: mark("adapterConfig", "url", v || undefined)
|
||||||
}
|
}
|
||||||
immediate={isCreate}
|
immediate={isCreate}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
@@ -334,90 +484,100 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Advanced adapter section */}
|
{/* Advanced adapter section — collapsible in both modes */}
|
||||||
{isLocal && (
|
{isLocal && (
|
||||||
<>
|
<CollapsibleSection
|
||||||
{isCreate ? (
|
title="Advanced Adapter Settings"
|
||||||
<CollapsibleSection
|
open={adapterAdvancedOpen}
|
||||||
title="Advanced Adapter Configuration"
|
onToggle={() => setAdapterAdvancedOpen(!adapterAdvancedOpen)}
|
||||||
open={adapterAdvancedOpen}
|
>
|
||||||
onToggle={() => setAdapterAdvancedOpen(!adapterAdvancedOpen)}
|
<div className="space-y-3">
|
||||||
>
|
<ModelDropdown
|
||||||
<div className="space-y-3">
|
models={models}
|
||||||
<ModelDropdown
|
value={currentModelId}
|
||||||
models={adapterModels ?? []}
|
onChange={(v) =>
|
||||||
value={val!.model}
|
isCreate
|
||||||
onChange={(v) => set!({ model: v })}
|
? set!({ model: v })
|
||||||
open={modelOpen}
|
: mark("adapterConfig", "model", v || undefined)
|
||||||
onOpenChange={setModelOpen}
|
}
|
||||||
|
open={modelOpen}
|
||||||
|
onOpenChange={setModelOpen}
|
||||||
|
/>
|
||||||
|
<Field label="Bootstrap prompt (first run)" hint={help.bootstrapPrompt}>
|
||||||
|
{isCreate ? (
|
||||||
|
<AutoExpandTextarea
|
||||||
|
placeholder="Optional initial setup prompt for the first run"
|
||||||
|
value={val!.bootstrapPrompt}
|
||||||
|
onChange={(v) => set!({ bootstrapPrompt: v })}
|
||||||
|
minRows={2}
|
||||||
/>
|
/>
|
||||||
<Field label="Bootstrap prompt (first run)" hint={help.bootstrapPrompt}>
|
) : (
|
||||||
<AutoExpandTextarea
|
|
||||||
placeholder="Optional initial setup prompt for the first run"
|
|
||||||
value={val!.bootstrapPrompt}
|
|
||||||
onChange={(v) => set!({ bootstrapPrompt: v })}
|
|
||||||
minRows={2}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
{adapterType === "claude_local" && (
|
|
||||||
<Field label="Max turns per run" hint={help.maxTurnsPerRun}>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
className={inputClass}
|
|
||||||
value={val!.maxTurnsPerRun}
|
|
||||||
onChange={(e) => set!({ maxTurnsPerRun: Number(e.target.value) })}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CollapsibleSection>
|
|
||||||
) : (
|
|
||||||
/* Edit mode: show advanced fields inline (no collapse) */
|
|
||||||
<div className="space-y-3 pt-2 border-t border-border/50">
|
|
||||||
<div className="text-[10px] font-medium text-muted-foreground/60 uppercase tracking-wider">
|
|
||||||
Advanced
|
|
||||||
</div>
|
|
||||||
<ModelDropdown
|
|
||||||
models={adapterModels ?? []}
|
|
||||||
value={String(config.model ?? "")}
|
|
||||||
onChange={(v) => saveConfig!("model", v || undefined)}
|
|
||||||
open={modelOpen}
|
|
||||||
onOpenChange={setModelOpen}
|
|
||||||
/>
|
|
||||||
<Field label="Bootstrap prompt (first run)" hint={help.bootstrapPrompt}>
|
|
||||||
<DraftTextarea
|
<DraftTextarea
|
||||||
value={String(config.bootstrapPromptTemplate ?? "")}
|
value={eff(
|
||||||
onCommit={(v) => saveConfig!("bootstrapPromptTemplate", v || undefined)}
|
"adapterConfig",
|
||||||
|
"bootstrapPromptTemplate",
|
||||||
|
String(config.bootstrapPromptTemplate ?? ""),
|
||||||
|
)}
|
||||||
|
onCommit={(v) =>
|
||||||
|
mark("adapterConfig", "bootstrapPromptTemplate", v || undefined)
|
||||||
|
}
|
||||||
placeholder="Optional initial setup prompt for the first run"
|
placeholder="Optional initial setup prompt for the first run"
|
||||||
minRows={2}
|
minRows={2}
|
||||||
/>
|
/>
|
||||||
</Field>
|
)}
|
||||||
{adapterType === "claude_local" && (
|
</Field>
|
||||||
<Field label="Max turns per run" hint={help.maxTurnsPerRun}>
|
{adapterType === "claude_local" && (
|
||||||
|
<Field label="Max turns per run" hint={help.maxTurnsPerRun}>
|
||||||
|
{isCreate ? (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={inputClass}
|
||||||
|
value={val!.maxTurnsPerRun}
|
||||||
|
onChange={(e) => set!({ maxTurnsPerRun: Number(e.target.value) })}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<DraftNumberInput
|
<DraftNumberInput
|
||||||
value={Number(config.maxTurnsPerRun ?? 80)}
|
value={eff(
|
||||||
onCommit={(v) => saveConfig!("maxTurnsPerRun", v || 80)}
|
"adapterConfig",
|
||||||
|
"maxTurnsPerRun",
|
||||||
|
Number(config.maxTurnsPerRun ?? 80),
|
||||||
|
)}
|
||||||
|
onCommit={(v) => mark("adapterConfig", "maxTurnsPerRun", v || 80)}
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Edit-only: timeout + grace period */}
|
||||||
|
{!isCreate && (
|
||||||
|
<>
|
||||||
|
<Field label="Timeout (sec)" hint={help.timeoutSec}>
|
||||||
|
<DraftNumberInput
|
||||||
|
value={eff(
|
||||||
|
"adapterConfig",
|
||||||
|
"timeoutSec",
|
||||||
|
Number(config.timeoutSec ?? 0),
|
||||||
|
)}
|
||||||
|
onCommit={(v) => mark("adapterConfig", "timeoutSec", v)}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
<Field label="Interrupt grace period (sec)" hint={help.graceSec}>
|
||||||
<Field label="Timeout (sec)" hint={help.timeoutSec}>
|
<DraftNumberInput
|
||||||
<DraftNumberInput
|
value={eff(
|
||||||
value={Number(config.timeoutSec ?? 0)}
|
"adapterConfig",
|
||||||
onCommit={(v) => saveConfig!("timeoutSec", v)}
|
"graceSec",
|
||||||
className={inputClass}
|
Number(config.graceSec ?? 15),
|
||||||
/>
|
)}
|
||||||
</Field>
|
onCommit={(v) => mark("adapterConfig", "graceSec", v)}
|
||||||
<Field label="Interrupt grace period (sec)" hint={help.graceSec}>
|
className={inputClass}
|
||||||
<DraftNumberInput
|
/>
|
||||||
value={Number(config.graceSec ?? 15)}
|
</Field>
|
||||||
onCommit={(v) => saveConfig!("graceSec", v)}
|
</>
|
||||||
className={inputClass}
|
)}
|
||||||
/>
|
</div>
|
||||||
</Field>
|
</CollapsibleSection>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -456,14 +616,14 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
<ToggleWithNumber
|
<ToggleWithNumber
|
||||||
label="Heartbeat on interval"
|
label="Heartbeat on interval"
|
||||||
hint={help.heartbeatInterval}
|
hint={help.heartbeatInterval}
|
||||||
checked={heartbeat.enabled !== false}
|
checked={eff("heartbeat", "enabled", heartbeat.enabled !== false)}
|
||||||
onCheckedChange={(v) => saveHeartbeat!("enabled", v)}
|
onCheckedChange={(v) => mark("heartbeat", "enabled", v)}
|
||||||
number={Number(heartbeat.intervalSec ?? 300)}
|
number={eff("heartbeat", "intervalSec", Number(heartbeat.intervalSec ?? 300))}
|
||||||
onNumberChange={(v) => saveHeartbeat!("intervalSec", v)}
|
onNumberChange={(v) => mark("heartbeat", "intervalSec", v)}
|
||||||
numberLabel="sec"
|
numberLabel="sec"
|
||||||
numberPrefix="Run heartbeat every"
|
numberPrefix="Run heartbeat every"
|
||||||
numberHint={help.intervalSec}
|
numberHint={help.intervalSec}
|
||||||
showNumber={heartbeat.enabled !== false}
|
showNumber={eff("heartbeat", "enabled", heartbeat.enabled !== false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Edit-only: wake-on-* and cooldown */}
|
{/* Edit-only: wake-on-* and cooldown */}
|
||||||
@@ -474,25 +634,41 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
<ToggleField
|
<ToggleField
|
||||||
label="Wake on assignment"
|
label="Wake on assignment"
|
||||||
hint={help.wakeOnAssignment}
|
hint={help.wakeOnAssignment}
|
||||||
checked={heartbeat.wakeOnAssignment !== false}
|
checked={eff(
|
||||||
onChange={(v) => saveHeartbeat!("wakeOnAssignment", v)}
|
"heartbeat",
|
||||||
|
"wakeOnAssignment",
|
||||||
|
heartbeat.wakeOnAssignment !== false,
|
||||||
|
)}
|
||||||
|
onChange={(v) => mark("heartbeat", "wakeOnAssignment", v)}
|
||||||
/>
|
/>
|
||||||
<ToggleField
|
<ToggleField
|
||||||
label="Wake on on-demand"
|
label="Wake on on-demand"
|
||||||
hint={help.wakeOnOnDemand}
|
hint={help.wakeOnOnDemand}
|
||||||
checked={heartbeat.wakeOnOnDemand !== false}
|
checked={eff(
|
||||||
onChange={(v) => saveHeartbeat!("wakeOnOnDemand", v)}
|
"heartbeat",
|
||||||
|
"wakeOnOnDemand",
|
||||||
|
heartbeat.wakeOnOnDemand !== false,
|
||||||
|
)}
|
||||||
|
onChange={(v) => mark("heartbeat", "wakeOnOnDemand", v)}
|
||||||
/>
|
/>
|
||||||
<ToggleField
|
<ToggleField
|
||||||
label="Wake on automation"
|
label="Wake on automation"
|
||||||
hint={help.wakeOnAutomation}
|
hint={help.wakeOnAutomation}
|
||||||
checked={heartbeat.wakeOnAutomation !== false}
|
checked={eff(
|
||||||
onChange={(v) => saveHeartbeat!("wakeOnAutomation", v)}
|
"heartbeat",
|
||||||
|
"wakeOnAutomation",
|
||||||
|
heartbeat.wakeOnAutomation !== false,
|
||||||
|
)}
|
||||||
|
onChange={(v) => mark("heartbeat", "wakeOnAutomation", v)}
|
||||||
/>
|
/>
|
||||||
<Field label="Cooldown (sec)" hint={help.cooldownSec}>
|
<Field label="Cooldown (sec)" hint={help.cooldownSec}>
|
||||||
<DraftNumberInput
|
<DraftNumberInput
|
||||||
value={Number(heartbeat.cooldownSec ?? 10)}
|
value={eff(
|
||||||
onCommit={(v) => saveHeartbeat!("cooldownSec", v)}
|
"heartbeat",
|
||||||
|
"cooldownSec",
|
||||||
|
Number(heartbeat.cooldownSec ?? 10),
|
||||||
|
)}
|
||||||
|
onCommit={(v) => mark("heartbeat", "cooldownSec", v)}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@@ -513,8 +689,12 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label="Monthly budget (cents)" hint={help.budgetMonthlyCents}>
|
<Field label="Monthly budget (cents)" hint={help.budgetMonthlyCents}>
|
||||||
<DraftNumberInput
|
<DraftNumberInput
|
||||||
value={props.agent.budgetMonthlyCents}
|
value={eff(
|
||||||
onCommit={(v) => saveIdentity!("budgetMonthlyCents", v)}
|
"runtime",
|
||||||
|
"budgetMonthlyCents",
|
||||||
|
props.agent.budgetMonthlyCents,
|
||||||
|
)}
|
||||||
|
onCommit={(v) => mark("runtime", "budgetMonthlyCents", v)}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -15,8 +15,20 @@ export function BreadcrumbBar() {
|
|||||||
|
|
||||||
if (breadcrumbs.length === 0) return null;
|
if (breadcrumbs.length === 0) return null;
|
||||||
|
|
||||||
|
// Single breadcrumb = page title (uppercase)
|
||||||
|
if (breadcrumbs.length === 1) {
|
||||||
|
return (
|
||||||
|
<div className="border-b border-border px-6 py-4">
|
||||||
|
<h1 className="text-sm font-semibold uppercase tracking-wider">
|
||||||
|
{breadcrumbs[0].label}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple breadcrumbs = breadcrumb trail
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border px-6 py-2">
|
<div className="border-b border-border px-6 py-3">
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
{breadcrumbs.map((crumb, i) => {
|
{breadcrumbs.map((crumb, i) => {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function CommentThread({ comments, onAdd }: CommentThreadProps) {
|
|||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
<div key={comment.id} className="rounded-md border border-border p-3">
|
<div key={comment.id} className="border border-border p-3">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
{comment.authorAgentId ? "Agent" : "Human"}
|
{comment.authorAgentId ? "Agent" : "Human"}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface EmptyStateProps {
|
|||||||
export function EmptyState({ icon: Icon, message, action, onAction }: EmptyStateProps) {
|
export function EmptyState({ icon: Icon, message, action, onAction }: EmptyStateProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||||
<div className="rounded-xl bg-muted/50 p-4 mb-4">
|
<div className="bg-muted/50 p-4 mb-4">
|
||||||
<Icon className="h-10 w-10 text-muted-foreground/50" />
|
<Icon className="h-10 w-10 text-muted-foreground/50" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mb-4">{message}</p>
|
<p className="text-sm text-muted-foreground mb-4">{message}</p>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function GoalNode({ goal, children, allGoals, depth, onSelect }: GoalNodeProps)
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 px-3 py-1.5 rounded-md text-sm transition-colors cursor-pointer hover:bg-accent/50",
|
"flex items-center gap-2 px-3 py-1.5 text-sm transition-colors cursor-pointer hover:bg-accent/50",
|
||||||
)}
|
)}
|
||||||
style={{ paddingLeft: `${depth * 20 + 12}px` }}
|
style={{ paddingLeft: `${depth * 20 + 12}px` }}
|
||||||
onClick={() => onSelect?.(goal)}
|
onClick={() => onSelect?.(goal)}
|
||||||
@@ -75,7 +75,7 @@ export function GoalTree({ goals, onSelect }: GoalTreeProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-border rounded-md py-1">
|
<div className="border border-border py-1">
|
||||||
{roots.map((goal) => (
|
{roots.map((goal) => (
|
||||||
<GoalNode
|
<GoalNode
|
||||||
key={goal.id}
|
key={goal.id}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function MetricCard({ icon: Icon, value, label, description }: MetricCard
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="rounded-md bg-muted p-2">
|
<div className="bg-muted p-2">
|
||||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ export function OnboardingWizard() {
|
|||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<div className="rounded-lg bg-muted/50 p-2">
|
<div className="bg-muted/50 p-2">
|
||||||
<Building2 className="h-5 w-5 text-muted-foreground" />
|
<Building2 className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -317,7 +317,7 @@ export function OnboardingWizard() {
|
|||||||
{step === 2 && (
|
{step === 2 && (
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<div className="rounded-lg bg-muted/50 p-2">
|
<div className="bg-muted/50 p-2">
|
||||||
<Bot className="h-5 w-5 text-muted-foreground" />
|
<Bot className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -506,7 +506,7 @@ export function OnboardingWizard() {
|
|||||||
{step === 3 && (
|
{step === 3 && (
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<div className="rounded-lg bg-muted/50 p-2">
|
<div className="bg-muted/50 p-2">
|
||||||
<ListTodo className="h-5 w-5 text-muted-foreground" />
|
<ListTodo className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -546,7 +546,7 @@ export function OnboardingWizard() {
|
|||||||
{step === 4 && (
|
{step === 4 && (
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<div className="rounded-lg bg-muted/50 p-2">
|
<div className="bg-muted/50 p-2">
|
||||||
<Rocket className="h-5 w-5 text-muted-foreground" />
|
<Rocket className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -556,7 +556,7 @@ export function OnboardingWizard() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border border-border divide-y divide-border">
|
<div className="border border-border divide-y divide-border">
|
||||||
<div className="flex items-center gap-3 px-3 py-2.5">
|
<div className="flex items-center gap-3 px-3 py-2.5">
|
||||||
<Building2 className="h-4 w-4 text-muted-foreground shrink-0" />
|
<Building2 className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import {
|
|||||||
ListTodo,
|
ListTodo,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Building2,
|
Building2,
|
||||||
|
BookOpen,
|
||||||
|
Paperclip,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CompanySwitcher } from "./CompanySwitcher";
|
import { CompanySwitcher } from "./CompanySwitcher";
|
||||||
import { SidebarSection } from "./SidebarSection";
|
import { SidebarSection } from "./SidebarSection";
|
||||||
import { SidebarNavItem } from "./SidebarNavItem";
|
import { SidebarNavItem } from "./SidebarNavItem";
|
||||||
import { useDialog } from "../context/DialogContext";
|
import { useDialog } from "../context/DialogContext";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
@@ -29,8 +30,15 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-60 h-full border-r border-border bg-card flex flex-col">
|
<aside className="w-60 h-full border-r border-border bg-background flex flex-col">
|
||||||
<div className="flex items-center gap-1 p-3">
|
{/* Logo */}
|
||||||
|
<div className="flex items-center gap-2 px-4 py-3">
|
||||||
|
<Paperclip className="h-5 w-5 text-foreground" />
|
||||||
|
<span className="text-sm font-semibold tracking-tight text-foreground">Paperclip</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Company switcher + actions */}
|
||||||
|
<div className="flex items-center gap-1 px-3 pb-3">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<CompanySwitcher />
|
<CompanySwitcher />
|
||||||
</div>
|
</div>
|
||||||
@@ -52,10 +60,8 @@ export function Sidebar() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
<nav className="flex flex-col gap-3 p-3">
|
<nav className="flex flex-col gap-4 px-3 py-2">
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<SidebarNavItem to="/inbox" label="Inbox" icon={Inbox} />
|
<SidebarNavItem to="/inbox" label="Inbox" icon={Inbox} />
|
||||||
<SidebarNavItem to="/my-issues" label="My Issues" icon={ListTodo} />
|
<SidebarNavItem to="/my-issues" label="My Issues" icon={ListTodo} />
|
||||||
@@ -77,6 +83,11 @@ export function Sidebar() {
|
|||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
</nav>
|
</nav>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Bottom links */}
|
||||||
|
<div className="border-t border-border px-3 py-2">
|
||||||
|
<SidebarNavItem to="/docs" label="Documentation" icon={BookOpen} />
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ export function SidebarNavItem({
|
|||||||
end={end}
|
end={end}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
cn(
|
cn(
|
||||||
"flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
|
"flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-foreground"
|
||||||
: "text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground"
|
: "text-foreground/80 hover:bg-accent/50 hover:text-foreground"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ export function SidebarSection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible open={open} onOpenChange={setOpen}>
|
<Collapsible open={open} onOpenChange={setOpen}>
|
||||||
<CollapsibleTrigger className="flex items-center gap-1 w-full px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground transition-colors">
|
<CollapsibleTrigger className="flex items-center justify-between w-full px-3 py-1.5 text-[10px] font-medium uppercase tracking-widest font-mono text-muted-foreground/60 hover:text-muted-foreground transition-colors">
|
||||||
|
{label}
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-3 w-3 transition-transform",
|
"h-3 w-3 transition-transform",
|
||||||
open && "rotate-90"
|
open && "rotate-90"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{label}
|
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="flex flex-col gap-0.5 mt-0.5">{children}</div>
|
<div className="flex flex-col gap-0.5 mt-0.5">{children}</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
"bg-card text-card-foreground flex flex-col gap-6 border py-6 shadow-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function Tabs({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tabsListVariants = cva(
|
const tabsListVariants = cva(
|
||||||
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
"p-[3px] group-data-[orientation=horizontal]/tabs:h-9 group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -62,7 +62,7 @@ function TabsTrigger({
|
|||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
||||||
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.145 0 0);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
|||||||
@@ -101,8 +101,7 @@ export function Activity() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-end">
|
||||||
<h2 className="text-lg font-semibold">Activity</h2>
|
|
||||||
<Select value={filter} onValueChange={setFilter}>
|
<Select value={filter} onValueChange={setFilter}>
|
||||||
<SelectTrigger className="w-[140px] h-8 text-xs">
|
<SelectTrigger className="w-[140px] h-8 text-xs">
|
||||||
<SelectValue placeholder="Filter by type" />
|
<SelectValue placeholder="Filter by type" />
|
||||||
@@ -126,7 +125,7 @@ export function Activity() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{filtered && filtered.length > 0 && (
|
{filtered && filtered.length > 0 && (
|
||||||
<div className="border border-border rounded-md divide-y divide-border">
|
<div className="border border-border divide-y divide-border">
|
||||||
{filtered.map((event) => {
|
{filtered.map((event) => {
|
||||||
const link = entityLink(event.entityType, event.entityId);
|
const link = entityLink(event.entityType, event.entityId);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ export function AgentDetail() {
|
|||||||
{assignedIssues.length === 0 ? (
|
{assignedIssues.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">No assigned issues.</p>
|
<p className="text-sm text-muted-foreground">No assigned issues.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="border border-border rounded-md">
|
<div className="border border-border">
|
||||||
{assignedIssues.map((issue) => (
|
{assignedIssues.map((issue) => (
|
||||||
<EntityRow
|
<EntityRow
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
@@ -429,6 +429,7 @@ function ConfigurationTab({ agent }: { agent: Agent }) {
|
|||||||
mode="edit"
|
mode="edit"
|
||||||
agent={agent}
|
agent={agent}
|
||||||
onSave={(patch) => updateAgent.mutate(patch)}
|
onSave={(patch) => updateAgent.mutate(patch)}
|
||||||
|
isSaving={updateAgent.isPending}
|
||||||
adapterModels={adapterModels}
|
adapterModels={adapterModels}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -450,7 +451,7 @@ function RunsTab({ runs, companyId }: { runs: HeartbeatRun[]; companyId: string
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-border rounded-md">
|
<div className="border border-border">
|
||||||
{sorted.map((run) => {
|
{sorted.map((run) => {
|
||||||
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" };
|
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" };
|
||||||
const StatusIcon = statusInfo.icon;
|
const StatusIcon = statusInfo.icon;
|
||||||
@@ -975,7 +976,7 @@ function KeysTab({ agentId }: { agentId: string }) {
|
|||||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||||
Active Keys
|
Active Keys
|
||||||
</h3>
|
</h3>
|
||||||
<div className="border border-border rounded-md divide-y divide-border">
|
<div className="border border-border divide-y divide-border">
|
||||||
{activeKeys.map((key: AgentKey) => (
|
{activeKeys.map((key: AgentKey) => (
|
||||||
<div key={key.id} className="flex items-center justify-between px-4 py-2.5">
|
<div key={key.id} className="flex items-center justify-between px-4 py-2.5">
|
||||||
<div>
|
<div>
|
||||||
@@ -1005,7 +1006,7 @@ function KeysTab({ agentId }: { agentId: string }) {
|
|||||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||||
Revoked Keys
|
Revoked Keys
|
||||||
</h3>
|
</h3>
|
||||||
<div className="border border-border rounded-md divide-y divide-border opacity-50">
|
<div className="border border-border divide-y divide-border opacity-50">
|
||||||
{revokedKeys.map((key: AgentKey) => (
|
{revokedKeys.map((key: AgentKey) => (
|
||||||
<div key={key.id} className="flex items-center justify-between px-4 py-2.5">
|
<div key={key.id} className="flex items-center justify-between px-4 py-2.5">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -93,20 +93,17 @@ export function Agents() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<Tabs value={tab} onValueChange={(v) => setTab(v as FilterTab)}>
|
||||||
<h2 className="text-lg font-semibold">Agents</h2>
|
<TabsList variant="line">
|
||||||
<Tabs value={tab} onValueChange={(v) => setTab(v as FilterTab)}>
|
<TabsTrigger value="all">All{agents ? ` (${agents.length})` : ""}</TabsTrigger>
|
||||||
<TabsList>
|
<TabsTrigger value="active">Active</TabsTrigger>
|
||||||
<TabsTrigger value="all">All{agents ? ` (${agents.length})` : ""}</TabsTrigger>
|
<TabsTrigger value="paused">Paused</TabsTrigger>
|
||||||
<TabsTrigger value="active">Active</TabsTrigger>
|
<TabsTrigger value="error">Error</TabsTrigger>
|
||||||
<TabsTrigger value="paused">Paused</TabsTrigger>
|
</TabsList>
|
||||||
<TabsTrigger value="error">Error</TabsTrigger>
|
</Tabs>
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* View toggle */}
|
{/* View toggle */}
|
||||||
<div className="flex items-center border border-border rounded-md">
|
<div className="flex items-center border border-border">
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-1.5 transition-colors",
|
"p-1.5 transition-colors",
|
||||||
@@ -147,7 +144,7 @@ export function Agents() {
|
|||||||
|
|
||||||
{/* List view */}
|
{/* List view */}
|
||||||
{view === "list" && filtered.length > 0 && (
|
{view === "list" && filtered.length > 0 && (
|
||||||
<div className="border border-border rounded-md">
|
<div className="border border-border">
|
||||||
{filtered.map((agent) => {
|
{filtered.map((agent) => {
|
||||||
const budgetPct =
|
const budgetPct =
|
||||||
agent.budgetMonthlyCents > 0
|
agent.budgetMonthlyCents > 0
|
||||||
@@ -221,7 +218,7 @@ export function Agents() {
|
|||||||
|
|
||||||
{/* Org chart view */}
|
{/* Org chart view */}
|
||||||
{view === "org" && filteredOrg.length > 0 && (
|
{view === "org" && filteredOrg.length > 0 && (
|
||||||
<div className="border border-border rounded-md py-1">
|
<div className="border border-border py-1">
|
||||||
{filteredOrg.map((node) => (
|
{filteredOrg.map((node) => (
|
||||||
<OrgTreeNode key={node.id} node={node} depth={0} navigate={navigate} agentMap={agentMap} />
|
<OrgTreeNode key={node.id} node={node} depth={0} navigate={navigate} agentMap={agentMap} />
|
||||||
))}
|
))}
|
||||||
@@ -275,7 +272,7 @@ function OrgTreeNode({
|
|||||||
return (
|
return (
|
||||||
<div style={{ paddingLeft: depth * 24 }}>
|
<div style={{ paddingLeft: depth * 24 }}>
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-accent/30 transition-colors w-full text-left"
|
className="flex items-center gap-3 px-3 py-2 hover:bg-accent/30 transition-colors w-full text-left"
|
||||||
onClick={() => navigate(`/agents/${node.id}`)}
|
onClick={() => navigate(`/agents/${node.id}`)}
|
||||||
>
|
>
|
||||||
<span className="relative flex h-2.5 w-2.5 shrink-0">
|
<span className="relative flex h-2.5 w-2.5 shrink-0">
|
||||||
|
|||||||
@@ -227,25 +227,22 @@ export function Approvals() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<Tabs value={statusFilter} onValueChange={(v) => setStatusFilter(v as StatusFilter)}>
|
||||||
<h2 className="text-lg font-semibold">Approvals</h2>
|
<TabsList variant="line">
|
||||||
<Tabs value={statusFilter} onValueChange={(v) => setStatusFilter(v as StatusFilter)}>
|
<TabsTrigger value="pending">
|
||||||
<TabsList>
|
Pending
|
||||||
<TabsTrigger value="pending">
|
{pendingCount > 0 && (
|
||||||
Pending
|
<span className={cn(
|
||||||
{pendingCount > 0 && (
|
"ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
|
||||||
<span className={cn(
|
"bg-yellow-500/20 text-yellow-500"
|
||||||
"ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
|
)}>
|
||||||
"bg-yellow-500/20 text-yellow-500"
|
{pendingCount}
|
||||||
)}>
|
</span>
|
||||||
{pendingCount}
|
)}
|
||||||
</span>
|
</TabsTrigger>
|
||||||
)}
|
<TabsTrigger value="all">All</TabsTrigger>
|
||||||
</TabsTrigger>
|
</TabsList>
|
||||||
<TabsTrigger value="all">All</TabsTrigger>
|
</Tabs>
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||||
|
|||||||
@@ -89,11 +89,7 @@ export function Companies() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-end">
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold">Companies</h2>
|
|
||||||
<p className="text-sm text-muted-foreground">Manage your companies.</p>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" onClick={openOnboarding}>
|
<Button size="sm" onClick={openOnboarding}>
|
||||||
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
||||||
New Company
|
New Company
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ export function Costs() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-semibold">Costs</h2>
|
|
||||||
|
|
||||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||||
|
|
||||||
|
|||||||
@@ -113,12 +113,9 @@ export function Dashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
{selectedCompany && (
|
||||||
<h2 className="text-lg font-semibold">Dashboard</h2>
|
<p className="text-sm text-muted-foreground">{selectedCompany.name}</p>
|
||||||
{selectedCompany && (
|
)}
|
||||||
<p className="text-sm text-muted-foreground">{selectedCompany.name}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||||
@@ -159,7 +156,7 @@ export function Dashboard() {
|
|||||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
||||||
Recent Activity
|
Recent Activity
|
||||||
</h3>
|
</h3>
|
||||||
<div className="border border-border rounded-md divide-y divide-border">
|
<div className="border border-border divide-y divide-border">
|
||||||
{activity.slice(0, 10).map((event) => (
|
{activity.slice(0, 10).map((event) => (
|
||||||
<div key={event.id} className="px-4 py-2 flex items-center justify-between text-sm">
|
<div key={event.id} className="px-4 py-2 flex items-center justify-between text-sm">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
@@ -185,11 +182,11 @@ export function Dashboard() {
|
|||||||
Stale Tasks
|
Stale Tasks
|
||||||
</h3>
|
</h3>
|
||||||
{staleIssues.length === 0 ? (
|
{staleIssues.length === 0 ? (
|
||||||
<div className="border border-border rounded-md p-4">
|
<div className="border border-border p-4">
|
||||||
<p className="text-sm text-muted-foreground">No stale tasks. All work is up to date.</p>
|
<p className="text-sm text-muted-foreground">No stale tasks. All work is up to date.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border border-border rounded-md divide-y divide-border">
|
<div className="border border-border divide-y divide-border">
|
||||||
{staleIssues.slice(0, 10).map((issue) => (
|
{staleIssues.slice(0, 10).map((issue) => (
|
||||||
<div
|
<div
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function GoalDetail() {
|
|||||||
{linkedProjects.length === 0 ? (
|
{linkedProjects.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">No linked projects.</p>
|
<p className="text-sm text-muted-foreground">No linked projects.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="border border-border rounded-md">
|
<div className="border border-border">
|
||||||
{linkedProjects.map((project) => (
|
{linkedProjects.map((project) => (
|
||||||
<EntityRow
|
<EntityRow
|
||||||
key={project.id}
|
key={project.id}
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ export function Goals() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-lg font-semibold">Goals</h2>
|
|
||||||
|
|
||||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||||
|
|
||||||
|
|||||||
@@ -116,8 +116,6 @@ export function Inbox() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-semibold">Inbox</h2>
|
|
||||||
|
|
||||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||||
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
||||||
@@ -140,7 +138,7 @@ export function Inbox() {
|
|||||||
See all approvals <ExternalLink className="inline h-3 w-3 ml-0.5" />
|
See all approvals <ExternalLink className="inline h-3 w-3 ml-0.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-border rounded-md divide-y divide-border">
|
<div className="border border-border divide-y divide-border">
|
||||||
{approvals!.map((approval) => (
|
{approvals!.map((approval) => (
|
||||||
<div key={approval.id} className="p-4 space-y-2">
|
<div key={approval.id} className="p-4 space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -191,7 +189,7 @@ export function Inbox() {
|
|||||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
||||||
Alerts
|
Alerts
|
||||||
</h3>
|
</h3>
|
||||||
<div className="border border-border rounded-md divide-y divide-border">
|
<div className="border border-border divide-y divide-border">
|
||||||
{dashboard!.agents.error > 0 && (
|
{dashboard!.agents.error > 0 && (
|
||||||
<div
|
<div
|
||||||
className="px-4 py-3 flex items-center gap-3 cursor-pointer hover:bg-accent/50 transition-colors"
|
className="px-4 py-3 flex items-center gap-3 cursor-pointer hover:bg-accent/50 transition-colors"
|
||||||
@@ -232,7 +230,7 @@ export function Inbox() {
|
|||||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
|
||||||
Stale Work
|
Stale Work
|
||||||
</h3>
|
</h3>
|
||||||
<div className="border border-border rounded-md divide-y divide-border">
|
<div className="border border-border divide-y divide-border">
|
||||||
{staleIssues.map((issue) => (
|
{staleIssues.map((issue) => (
|
||||||
<div
|
<div
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
|
|||||||
@@ -89,17 +89,14 @@ export function Issues() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<Tabs value={tab} onValueChange={(v) => setTab(v as TabFilter)}>
|
||||||
<h2 className="text-lg font-semibold">Issues</h2>
|
<TabsList variant="line">
|
||||||
<Tabs value={tab} onValueChange={(v) => setTab(v as TabFilter)}>
|
<TabsTrigger value="all">All Issues</TabsTrigger>
|
||||||
<TabsList>
|
<TabsTrigger value="active">Active</TabsTrigger>
|
||||||
<TabsTrigger value="all">All Issues</TabsTrigger>
|
<TabsTrigger value="backlog">Backlog</TabsTrigger>
|
||||||
<TabsTrigger value="active">Active</TabsTrigger>
|
<TabsTrigger value="done">Done</TabsTrigger>
|
||||||
<TabsTrigger value="backlog">Backlog</TabsTrigger>
|
</TabsList>
|
||||||
<TabsTrigger value="done">Done</TabsTrigger>
|
</Tabs>
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" onClick={() => openNewIssue()}>
|
<Button size="sm" onClick={() => openNewIssue()}>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
New Issue
|
New Issue
|
||||||
@@ -120,7 +117,7 @@ export function Issues() {
|
|||||||
|
|
||||||
{orderedGroups.map(({ status, items }) => (
|
{orderedGroups.map(({ status, items }) => (
|
||||||
<div key={status}>
|
<div key={status}>
|
||||||
<div className="flex items-center gap-2 px-4 py-2 bg-muted/50 rounded-t-md">
|
<div className="flex items-center gap-2 px-4 py-2 bg-muted/50">
|
||||||
<StatusIcon status={status} />
|
<StatusIcon status={status} />
|
||||||
<span className="text-xs font-semibold uppercase tracking-wide">
|
<span className="text-xs font-semibold uppercase tracking-wide">
|
||||||
{statusLabel(status)}
|
{statusLabel(status)}
|
||||||
@@ -135,7 +132,7 @@ export function Issues() {
|
|||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-border rounded-b-md">
|
<div className="border border-border">
|
||||||
{items.map((issue) => (
|
{items.map((issue) => (
|
||||||
<EntityRow
|
<EntityRow
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ export function MyIssues() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-lg font-semibold">My Issues</h2>
|
|
||||||
|
|
||||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||||
|
|
||||||
@@ -48,7 +46,7 @@ export function MyIssues() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{myIssues.length > 0 && (
|
{myIssues.length > 0 && (
|
||||||
<div className="border border-border rounded-md">
|
<div className="border border-border">
|
||||||
{myIssues.map((issue) => (
|
{myIssues.map((issue) => (
|
||||||
<EntityRow
|
<EntityRow
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
|
|||||||
@@ -106,8 +106,6 @@ export function Org() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-lg font-semibold">Org Chart</h2>
|
|
||||||
|
|
||||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||||
|
|
||||||
@@ -119,7 +117,7 @@ export function Org() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{data && data.length > 0 && (
|
{data && data.length > 0 && (
|
||||||
<div className="border border-border rounded-md py-1">
|
<div className="border border-border py-1">
|
||||||
<OrgTree nodes={data} onSelect={(id) => navigate(`/agents/${id}`)} />
|
<OrgTree nodes={data} onSelect={(id) => navigate(`/agents/${id}`)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export function ProjectDetail() {
|
|||||||
{projectIssues.length === 0 ? (
|
{projectIssues.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">No issues in this project.</p>
|
<p className="text-sm text-muted-foreground">No issues in this project.</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="border border-border rounded-md">
|
<div className="border border-border">
|
||||||
{projectIssues.map((issue) => (
|
{projectIssues.map((issue) => (
|
||||||
<EntityRow
|
<EntityRow
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ export function Projects() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-end">
|
||||||
<h2 className="text-lg font-semibold">Projects</h2>
|
|
||||||
<Button size="sm" onClick={openNewProject}>
|
<Button size="sm" onClick={openNewProject}>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
Add Project
|
Add Project
|
||||||
@@ -56,7 +55,7 @@ export function Projects() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{projects && projects.length > 0 && (
|
{projects && projects.length > 0 && (
|
||||||
<div className="border border-border rounded-md">
|
<div className="border border-border">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<EntityRow
|
<EntityRow
|
||||||
key={project.id}
|
key={project.id}
|
||||||
|
|||||||
Reference in New Issue
Block a user