@@ -28,11 +28,17 @@ import DepositSourceSelection, {
2828import {
2929 buildTerminalExecutionHistoryRequest ,
3030 buildTerminalObfuscationsAnchorDraft ,
31+ formatObfuscationsAnchorDescription ,
3132 mapExecutionHistoryRunToWorkspaceRun ,
3233 readTerminalRouteError ,
3334 type TerminalActivityRecordDraft ,
3435 upsertWorkspaceRun ,
3536} from "@/app/terminal/terminal-activity-history" ;
37+ import {
38+ DepositExcludePathsIcon ,
39+ DepositIncludePathsIcon ,
40+ ObfuscationsAnchorDescription ,
41+ } from "@/app/deposits/deposit-obfuscations-path-icons" ;
3642import type { TerminalRepositoryContextState } from "@/app/terminal/terminal-repository-context" ;
3743import {
3844 clearTerminalTransactionId ,
@@ -82,6 +88,11 @@ import {
8288import { TelemetryExplainerTrigger } from "@/components/base/bitcode/execution/TelemetryExplainerTrigger" ;
8389import { VCSFileTreePicker } from "@/components/base/bitcode/vcs/VCSFileTreePicker" ;
8490import { SearchableSelect } from "@/components/base/bitcode/forms/SearchableSelect" ;
91+ import {
92+ Popover ,
93+ PopoverContent ,
94+ PopoverTrigger ,
95+ } from "@/components/base/shadcn/popover" ;
8596import type {
8697 DepositOptionReviewDecision ,
8798 DepositOptionReviewDecisionState ,
@@ -147,6 +158,11 @@ export default function DepositPageClient() {
147158 const [ repositoryContext , setRepositoryContext ] =
148159 useState < TerminalRepositoryContextState | null > ( null ) ;
149160 const [ obfuscations , setObfuscations ] = useState ( "" ) ;
161+ // Optional display name drafted in the Anchor popover (and restored when a
162+ // named anchor is loaded from the activity ledger).
163+ const [ obfuscationsAnchorName , setObfuscationsAnchorName ] = useState ( "" ) ;
164+ const [ isObfuscationsAnchorPopoverOpen , setIsObfuscationsAnchorPopoverOpen ] =
165+ useState ( false ) ;
150166 // Picked from the repository file tree (selected repo·branch·commit);
151167 // hints and exclusions are mutually exclusive path sets.
152168 const [ sourcePathHints , setSourcePathHints ] = useState < string [ ] > ( [ ] ) ;
@@ -422,22 +438,55 @@ export default function DepositPageClient() {
422438 } , [ liveRuns ] ) ;
423439 // V48-Gate3-F13/F18: previously anchored Obfuscations configurations,
424440 // newest first — same derivation pattern as repositoryAnchors above.
441+ // Dedupe by name+text so two differently-named saves of the same body
442+ // both remain selectable.
425443 const obfuscationsAnchors = useMemo ( ( ) => {
426444 const seen = new Set < string > ( ) ;
427- const anchors : Array < { id : string ; text : string ; repositoryFullName : string | null ; createdAt : string } > = [ ] ;
445+ const anchors : Array < {
446+ id : string ;
447+ name : string | null ;
448+ text : string ;
449+ sourcePathHints : string [ ] ;
450+ protectedIpExclusions : string [ ] ;
451+ repositoryFullName : string | null ;
452+ createdAt : string ;
453+ } > = [ ] ;
428454 for ( const run of [ ...liveRuns ] . sort (
429455 ( a , b ) => new Date ( b . created_at ) . getTime ( ) - new Date ( a . created_at ) . getTime ( ) ,
430456 ) ) {
431457 if (
432458 run . contextSource !== "deposit-obfuscations-anchor" ||
433- ! run . obfuscationsAnchorText ||
434- seen . has ( run . obfuscationsAnchorText )
459+ ! run . obfuscationsAnchorText
435460 )
436461 continue ;
437- seen . add ( run . obfuscationsAnchorText ) ;
462+ const name =
463+ typeof run . obfuscationsAnchorName === "string" &&
464+ run . obfuscationsAnchorName . trim ( )
465+ ? run . obfuscationsAnchorName . trim ( )
466+ : null ;
467+ const sourcePathHints = Array . isArray ( run . obfuscationsAnchorSourcePathHints )
468+ ? run . obfuscationsAnchorSourcePathHints . filter (
469+ ( path ) : path is string =>
470+ typeof path === "string" && path . trim ( ) . length > 0 ,
471+ )
472+ : [ ] ;
473+ const protectedIpExclusions = Array . isArray (
474+ run . obfuscationsAnchorProtectedIpExclusions ,
475+ )
476+ ? run . obfuscationsAnchorProtectedIpExclusions . filter (
477+ ( path ) : path is string =>
478+ typeof path === "string" && path . trim ( ) . length > 0 ,
479+ )
480+ : [ ] ;
481+ const dedupeKey = `${ name || "" } \u0000${ run . obfuscationsAnchorText } \u0000${ sourcePathHints . join ( "," ) } \u0000${ protectedIpExclusions . join ( "," ) } ` ;
482+ if ( seen . has ( dedupeKey ) ) continue ;
483+ seen . add ( dedupeKey ) ;
438484 anchors . push ( {
439485 id : run . id ,
486+ name,
440487 text : run . obfuscationsAnchorText ,
488+ sourcePathHints,
489+ protectedIpExclusions,
441490 repositoryFullName : run . repository || null ,
442491 createdAt : run . created_at ,
443492 } ) ;
@@ -959,6 +1008,7 @@ export default function DepositPageClient() {
9591008 // V48-Gate3-F13/F18: anchor the CURRENT Obfuscations text into the activity
9601009 // ledger (mirrors handleAnchorRepository in DepositSourceSelection), so it
9611010 // can be reloaded on a later run via the "Load anchor" selector below.
1011+ // Optional display name (from the Anchor popover) labels the dropdown entry.
9621012 const handleAnchorObfuscations = useCallback ( async ( ) => {
9631013 if ( ! obfuscations . trim ( ) ) return ;
9641014 setIsAnchoringObfuscations ( true ) ;
@@ -967,13 +1017,19 @@ export default function DepositPageClient() {
9671017 await handleRecordActivity (
9681018 buildTerminalObfuscationsAnchorDraft ( {
9691019 obfuscations,
1020+ name : obfuscationsAnchorName ,
9701021 repositoryFullName :
9711022 repositoryContext ?. selectedRepository ?. fullName || null ,
1023+ sourcePathHints,
1024+ protectedIpExclusions,
9721025 } ) ,
9731026 ) ;
9741027 setObfuscationsAnchorMessage (
975- "Obfuscations configuration anchored into the Bitcode activity ledger." ,
1028+ obfuscationsAnchorName . trim ( )
1029+ ? `Obfuscations anchor "${ obfuscationsAnchorName . trim ( ) } " saved into the Bitcode activity ledger.`
1030+ : "Obfuscations configuration anchored into the Bitcode activity ledger." ,
9761031 ) ;
1032+ setIsObfuscationsAnchorPopoverOpen ( false ) ;
9771033 } catch ( error ) {
9781034 setObfuscationsAnchorMessage (
9791035 error instanceof Error
@@ -983,7 +1039,14 @@ export default function DepositPageClient() {
9831039 } finally {
9841040 setIsAnchoringObfuscations ( false ) ;
9851041 }
986- } , [ handleRecordActivity , obfuscations , repositoryContext ] ) ;
1042+ } , [
1043+ handleRecordActivity ,
1044+ obfuscations ,
1045+ obfuscationsAnchorName ,
1046+ protectedIpExclusions ,
1047+ repositoryContext ,
1048+ sourcePathHints ,
1049+ ] ) ;
9871050
9881051 // Real option synthesis via the AssetPacksSynthesis pipeline (deposit
9891052 // lens). The server route builds the exclusion-filtered source inventory,
@@ -1625,25 +1688,51 @@ export default function DepositPageClient() {
16251688 < BitcodeInlineExplainer explainer = { DEPOSIT_SECTION_EXPLAINERS . obfuscations } />
16261689 </ h2 >
16271690 </ div >
1628- < div className = "flex items-center gap-2" >
1691+ < div className = "flex flex-wrap items-center justify-end gap-2" >
16291692 { obfuscationsAnchors . length > 0 ? (
1630- < div className = "w-40 " >
1693+ < div className = "w-56 " >
16311694 < SearchableSelect
16321695 aria-label = "Load a previously anchored Obfuscations configuration"
16331696 items = { obfuscationsAnchors . map ( ( anchor ) => ( {
16341697 key : anchor . id ,
1635- label : anchor . repositoryFullName || "Obfuscations anchor" ,
1636- description :
1637- anchor . text . length > 60
1638- ? `${ anchor . text . slice ( 0 , 60 ) } …`
1639- : anchor . text ,
1698+ label :
1699+ anchor . name ||
1700+ anchor . repositoryFullName ||
1701+ "Obfuscations anchor" ,
1702+ // Sub-text: clipped body | include icon+count | exclude
1703+ // icon+count — same icons as the picker section headers.
1704+ description : (
1705+ < ObfuscationsAnchorDescription
1706+ text = { anchor . text }
1707+ sourcePathHints = { anchor . sourcePathHints }
1708+ protectedIpExclusions = {
1709+ anchor . protectedIpExclusions
1710+ }
1711+ />
1712+ ) ,
1713+ searchText : [
1714+ anchor . name ,
1715+ anchor . repositoryFullName ,
1716+ formatObfuscationsAnchorDescription ( {
1717+ text : anchor . text ,
1718+ sourcePathHints : anchor . sourcePathHints ,
1719+ protectedIpExclusions :
1720+ anchor . protectedIpExclusions ,
1721+ } ) ,
1722+ ]
1723+ . filter ( Boolean )
1724+ . join ( " " ) ,
16401725 } ) ) }
16411726 value = { null }
16421727 onSelect = { ( key ) => {
16431728 const anchor = obfuscationsAnchors . find (
16441729 ( entry ) => entry . id === key ,
16451730 ) ;
1646- if ( anchor ) setObfuscations ( anchor . text ) ;
1731+ if ( ! anchor ) return ;
1732+ setObfuscations ( anchor . text ) ;
1733+ setObfuscationsAnchorName ( anchor . name || "" ) ;
1734+ setSourcePathHints ( anchor . sourcePathHints ) ;
1735+ setProtectedIpExclusions ( anchor . protectedIpExclusions ) ;
16471736 } }
16481737 placeholder = "Load anchor..."
16491738 searchPlaceholder = "Search anchors..."
@@ -1656,28 +1745,123 @@ export default function DepositPageClient() {
16561745 type = "button"
16571746 aria-label = "Clear obfuscations"
16581747 title = "Clear obfuscations"
1659- disabled = { ! obfuscations }
1660- onClick = { ( ) => setObfuscations ( "" ) }
1748+ disabled = {
1749+ ! obfuscations &&
1750+ ! obfuscationsAnchorName &&
1751+ sourcePathHints . length === 0 &&
1752+ protectedIpExclusions . length === 0
1753+ }
1754+ onClick = { ( ) => {
1755+ setObfuscations ( "" ) ;
1756+ setObfuscationsAnchorName ( "" ) ;
1757+ setSourcePathHints ( [ ] ) ;
1758+ setProtectedIpExclusions ( [ ] ) ;
1759+ setIsObfuscationsAnchorPopoverOpen ( false ) ;
1760+ } }
16611761 className = "border border-white/10 px-2.5 py-1.5 text-[0.66rem] uppercase tracking-[0.14em] text-neutral-300 transition hover:border-rose-300/35 hover:text-rose-100 disabled:cursor-not-allowed disabled:opacity-40"
16621762 >
16631763 Clear
16641764 </ button >
1665- < button
1666- type = "button"
1667- aria-label = "Anchor obfuscations to the activity ledger"
1668- title = "Anchor obfuscations to the activity ledger"
1669- disabled = { ! obfuscations . trim ( ) || isAnchoringObfuscations }
1670- onClick = { ( ) => {
1671- void handleAnchorObfuscations ( ) ;
1765+ < Popover
1766+ open = { isObfuscationsAnchorPopoverOpen }
1767+ onOpenChange = { ( open ) => {
1768+ // Require Obfuscations body before opening the name popover.
1769+ if ( open && ! obfuscations . trim ( ) ) return ;
1770+ if ( isAnchoringObfuscations ) return ;
1771+ setIsObfuscationsAnchorPopoverOpen ( open ) ;
16721772 } }
1673- className = "flex h-9 w-9 items-center justify-center border border-white/10 bg-white/5 text-neutral-200 transition hover:border-emerald-300/35 hover:bg-emerald-300/10 disabled:cursor-not-allowed disabled:opacity-40"
16741773 >
1675- { isAnchoringObfuscations ? (
1676- < RefreshCw className = "h-4 w-4 animate-spin" />
1677- ) : (
1678- < Anchor className = "h-4 w-4" />
1679- ) }
1680- </ button >
1774+ < PopoverTrigger asChild >
1775+ < button
1776+ type = "button"
1777+ aria-label = "Anchor obfuscations to the activity ledger"
1778+ title = "Anchor obfuscations to the activity ledger"
1779+ disabled = {
1780+ ! obfuscations . trim ( ) || isAnchoringObfuscations
1781+ }
1782+ className = "flex h-9 w-9 items-center justify-center border border-white/10 bg-white/5 text-neutral-200 transition hover:border-emerald-300/35 hover:bg-emerald-300/10 disabled:cursor-not-allowed disabled:opacity-40"
1783+ >
1784+ { isAnchoringObfuscations ? (
1785+ < RefreshCw className = "h-4 w-4 animate-spin" />
1786+ ) : (
1787+ < Anchor className = "h-4 w-4" />
1788+ ) }
1789+ </ button >
1790+ </ PopoverTrigger >
1791+ < PopoverContent
1792+ align = "end"
1793+ sideOffset = { 6 }
1794+ className = "w-64 border-white/10 bg-neutral-950 p-3 text-neutral-100 shadow-xl"
1795+ >
1796+ < p className = "text-[0.62rem] uppercase tracking-[0.14em] text-neutral-500" >
1797+ Name this anchor
1798+ </ p >
1799+ < input
1800+ id = "deposit-obfuscations-anchor-name"
1801+ type = "text"
1802+ value = { obfuscationsAnchorName }
1803+ onChange = { ( event ) =>
1804+ setObfuscationsAnchorName (
1805+ event . target . value . slice ( 0 , 80 ) ,
1806+ )
1807+ }
1808+ onKeyDown = { ( event ) => {
1809+ if ( event . key === "Enter" ) {
1810+ event . preventDefault ( ) ;
1811+ void handleAnchorObfuscations ( ) ;
1812+ }
1813+ if ( event . key === "Escape" ) {
1814+ event . preventDefault ( ) ;
1815+ setIsObfuscationsAnchorPopoverOpen ( false ) ;
1816+ }
1817+ } }
1818+ placeholder = "Optional name"
1819+ maxLength = { 80 }
1820+ autoFocus
1821+ aria-label = "Obfuscations anchor name"
1822+ className = "mt-2 h-9 w-full border border-white/10 bg-black/40 px-2.5 text-xs text-neutral-100 outline-none transition placeholder:text-neutral-500 focus:border-emerald-300/35"
1823+ />
1824+ < p className = "mt-1.5 text-[0.68rem] leading-4 text-neutral-500" >
1825+ Shown as the label when reloading. Leave blank to use
1826+ the repository name.
1827+ </ p >
1828+ < div className = "mt-3 flex items-center justify-end gap-2" >
1829+ < button
1830+ type = "button"
1831+ onClick = { ( ) =>
1832+ setIsObfuscationsAnchorPopoverOpen ( false )
1833+ }
1834+ disabled = { isAnchoringObfuscations }
1835+ className = "border border-white/10 px-2.5 py-1.5 text-[0.62rem] uppercase tracking-[0.14em] text-neutral-300 transition hover:border-white/25 disabled:opacity-40"
1836+ >
1837+ Cancel
1838+ </ button >
1839+ < button
1840+ type = "button"
1841+ onClick = { ( ) => {
1842+ void handleAnchorObfuscations ( ) ;
1843+ } }
1844+ disabled = {
1845+ ! obfuscations . trim ( ) || isAnchoringObfuscations
1846+ }
1847+ className = "inline-flex items-center gap-1.5 border border-emerald-300/30 bg-emerald-300/12 px-2.5 py-1.5 text-[0.62rem] uppercase tracking-[0.14em] text-emerald-100 transition hover:border-emerald-200/45 hover:bg-emerald-300/18 disabled:cursor-not-allowed disabled:opacity-40"
1848+ >
1849+ { isAnchoringObfuscations ? (
1850+ < RefreshCw
1851+ className = "h-3 w-3 animate-spin"
1852+ aria-hidden = "true"
1853+ />
1854+ ) : (
1855+ < Anchor
1856+ className = "h-3 w-3"
1857+ aria-hidden = "true"
1858+ />
1859+ ) }
1860+ Save anchor
1861+ </ button >
1862+ </ div >
1863+ </ PopoverContent >
1864+ </ Popover >
16811865 < Sparkles
16821866 className = "h-5 w-5 text-emerald-200"
16831867 aria-hidden = "true"
@@ -1713,6 +1897,7 @@ export default function DepositPageClient() {
17131897 < div className = "mt-4 grid gap-4 tablet:grid-cols-2" >
17141898 < div className = "block" >
17151899 < span className = "flex items-center gap-2 text-[0.62rem] uppercase tracking-[0.16em] text-neutral-500" >
1900+ < DepositIncludePathsIcon />
17161901 < span > Source path hints</ span >
17171902 < span onClick = { ( event ) => event . stopPropagation ( ) } >
17181903 < BitcodeInlineExplainer explainer = { DEPOSIT_SECTION_EXPLAINERS . sourcePathHints } triggerAriaLabel = "More info about this field" />
@@ -1739,6 +1924,7 @@ export default function DepositPageClient() {
17391924 </ div >
17401925 < div className = "block" >
17411926 < span className = "flex items-center gap-2 text-[0.62rem] uppercase tracking-[0.16em] text-neutral-500" >
1927+ < DepositExcludePathsIcon />
17421928 < span > Protected IP exclusions</ span >
17431929 < span onClick = { ( event ) => event . stopPropagation ( ) } >
17441930 < BitcodeInlineExplainer explainer = { DEPOSIT_SECTION_EXPLAINERS . protectedIpExclusions } triggerAriaLabel = "More info about this field" />
0 commit comments