UI: secrets-aware env editor, issue hiding, and adapter env bindings

Replace plain text env var editor with structured EnvVarEditor
supporting plain values and secret references with inline "Seal"
to convert a value to a managed secret. Add issue hide action via
popover menu on issue detail. Adapters now emit envBindings with
typed plain/secret_ref entries instead of raw KEY=VALUE strings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-19 15:44:05 -06:00
parent f1b558dcfb
commit 5c259a9470
8 changed files with 365 additions and 48 deletions

View File

@@ -72,6 +72,14 @@ function shouldRedactSecretValue(key: string, value: unknown): boolean {
}
function redactEnvValue(key: string, value: unknown): string {
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
(value as { type?: unknown }).type === "secret_ref"
) {
return "***SECRET_REF***";
}
if (shouldRedactSecretValue(key, value)) return REDACTED_ENV_VALUE;
if (value === null || value === undefined) return "";
if (typeof value === "string") return value;

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo } from "react";
import { useParams, Link } from "react-router-dom";
import { useEffect, useMemo, useState } from "react";
import { useParams, Link, useNavigate } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { issuesApi } from "../api/issues";
import { activityApi } from "../api/activity";
@@ -18,7 +18,9 @@ import { PriorityIcon } from "../components/PriorityIcon";
import { StatusBadge } from "../components/StatusBadge";
import { Identity } from "../components/Identity";
import { Separator } from "@/components/ui/separator";
import { ChevronRight } from "lucide-react";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { ChevronRight, MoreHorizontal, EyeOff } from "lucide-react";
import type { ActivityEvent } from "@paperclip/shared";
import type { Agent } from "@paperclip/shared";
@@ -94,6 +96,8 @@ export function IssueDetail() {
const { openPanel, closePanel } = usePanel();
const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient();
const navigate = useNavigate();
const [moreOpen, setMoreOpen] = useState(false);
const { data: issue, isLoading, error } = useQuery({
queryKey: queryKeys.issues.detail(issueId!),
@@ -242,6 +246,29 @@ export function IssueDetail() {
onChange={(priority) => updateIssue.mutate({ priority })}
/>
<span className="text-xs font-mono text-muted-foreground">{issue.identifier ?? issue.id.slice(0, 8)}</span>
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon-xs">
<MoreHorizontal className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-44 p-1" align="end">
<button
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-destructive"
onClick={() => {
updateIssue.mutate(
{ hiddenAt: new Date().toISOString() },
{ onSuccess: () => navigate("/issues") },
);
setMoreOpen(false);
}}
>
<EyeOff className="h-3 w-3" />
Hide this Issue
</button>
</PopoverContent>
</Popover>
</div>
<InlineEditor