1- import { clearPendingPluginDeepLink , usePluginDeepLink , usePluginSettings } from "@haloforge/plugin-sdk" ;
2- import { Bot , Braces , LayoutDashboard , RefreshCcw , Shield , TerminalSquare } from "lucide-react" ;
1+ import { clearPendingPluginDeepLink , usePluginDeepLink , usePluginSettings , type PluginDeepLink } from "@haloforge/plugin-sdk" ;
2+ import { Bot , Braces , CheckCircle2 , KeyRound , LayoutDashboard , RefreshCcw , Shield , TerminalSquare , X } from "lucide-react" ;
33import { useCallback , useEffect , useState } from "react" ;
44import { BackupPanel } from "./components/BackupPanel" ;
55import { McpPanel } from "./components/McpPanel" ;
66import { ProviderPanel } from "./components/ProviderPanel" ;
77import { TargetCard } from "./components/TargetCard" ;
88import { DEFAULT_CODEX_PROVIDER_ID , DEFAULT_MCP_SPEC , defaultProviderForm } from "./defaults" ;
9- import { useSwitchboardT } from "./i18n" ;
9+ import { useSwitchboardT , type SwitchboardTranslationKey } from "./i18n" ;
1010import { useSwitchboard } from "./hooks/useSwitchboard" ;
1111import type { McpAppSelection , PluginSettings , ProviderForm , SwitchboardImportPatch } from "./types" ;
1212
1313type SwitchboardTab = "overview" | "claude" | "codex" | "mcp" | "backups" ;
1414
15+ interface PendingImport {
16+ patch : SwitchboardImportPatch ;
17+ providerPatch : SwitchboardImportPatch [ "provider" ] | null ;
18+ }
19+
1520function buildProviderForm ( settings : PluginSettings , target : ProviderForm [ "target" ] ) : ProviderForm {
1621 return {
1722 ...defaultProviderForm ( settings ) ,
@@ -31,6 +36,7 @@ export function SwitchboardPanel() {
3136 codex : true ,
3237 } ) ;
3338 const [ mcpSpec , setMcpSpec ] = useState ( DEFAULT_MCP_SPEC ) ;
39+ const [ pendingImport , setPendingImport ] = useState < PendingImport | null > ( null ) ;
3440 const {
3541 status,
3642 busy,
@@ -75,7 +81,7 @@ export function SwitchboardPanel() {
7581 setMessage ( t ( "switchboard.message.importReady" ) ) ;
7682 } , [ setMessage , settings . defaultTarget , t ] ) ;
7783
78- usePluginDeepLink ( useCallback ( ( link ) => {
84+ usePluginDeepLink ( useCallback ( ( link : PluginDeepLink ) => {
7985 if ( link . route !== "/v1/import" && link . route !== "/import" ) {
8086 return ;
8187 }
@@ -84,9 +90,25 @@ export function SwitchboardPanel() {
8490 setMessage ( t ( "switchboard.message.importInvalid" ) ) ;
8591 return ;
8692 }
87- applyImportPatch ( patch ) ;
93+ const providerPatch = normalizeProviderPatch ( patch . provider ) ;
94+ const targetTab = resolveImportTab ( patch , providerPatch , settings . defaultTarget ?? "both" ) ;
95+ setActiveTab ( targetTab ) ;
96+ setPendingImport ( { patch, providerPatch } ) ;
97+ setMessage ( null ) ;
8898 clearPendingPluginDeepLink ( ) ;
89- } , [ applyImportPatch , setMessage , t ] ) ) ;
99+ } , [ setMessage , settings . defaultTarget , t ] ) ) ;
100+
101+ const confirmPendingImport = useCallback ( ( ) => {
102+ if ( ! pendingImport ) {
103+ return ;
104+ }
105+ applyImportPatch ( pendingImport . patch ) ;
106+ setPendingImport ( null ) ;
107+ } , [ applyImportPatch , pendingImport ] ) ;
108+
109+ const cancelPendingImport = useCallback ( ( ) => {
110+ setPendingImport ( null ) ;
111+ } , [ ] ) ;
90112
91113 useEffect ( ( ) => {
92114 setClaudeForm ( ( current ) => ( {
@@ -251,10 +273,167 @@ export function SwitchboardPanel() {
251273 < BackupPanel backups = { status ?. backups ?? [ ] } busy = { busy } onRestore = { ( id ) => void restoreBackup ( id ) } t = { t } />
252274 </ section >
253275 ) }
276+
277+ { pendingImport && (
278+ < ImportPreviewDialog
279+ pendingImport = { pendingImport }
280+ defaultTarget = { settings . defaultTarget ?? "both" }
281+ onCancel = { cancelPendingImport }
282+ onConfirm = { confirmPendingImport }
283+ t = { t }
284+ />
285+ ) }
254286 </ main >
255287 ) ;
256288}
257289
290+ interface ImportPreviewDialogProps {
291+ pendingImport : PendingImport ;
292+ defaultTarget : ProviderForm [ "target" ] ;
293+ onCancel : ( ) => void ;
294+ onConfirm : ( ) => void ;
295+ t : ( key : SwitchboardTranslationKey , vars ?: Record < string , string | number > ) => string ;
296+ }
297+
298+ function ImportPreviewDialog ( {
299+ pendingImport,
300+ defaultTarget,
301+ onCancel,
302+ onConfirm,
303+ t,
304+ } : ImportPreviewDialogProps ) {
305+ const { patch, providerPatch } = pendingImport ;
306+ const target = providerPatch ?. target ?? defaultTarget ;
307+ const targetLabel = target === "codex"
308+ ? t ( "switchboard.tab.codex" )
309+ : target === "claude"
310+ ? t ( "switchboard.tab.claude" )
311+ : t ( "switchboard.import.targetBoth" ) ;
312+ const providerRows = buildImportProviderRows ( providerPatch , t ) ;
313+ const hasMcp = Boolean ( patch . mcp ) ;
314+ const mcpTargets = patch . mcp ?. apps
315+ ? [
316+ patch . mcp . apps . claude ? t ( "switchboard.tab.claude" ) : null ,
317+ patch . mcp . apps . codex ? t ( "switchboard.tab.codex" ) : null ,
318+ ] . filter ( Boolean ) . join ( ", " )
319+ : t ( "switchboard.import.targetBoth" ) ;
320+
321+ return (
322+ < div className = "sb-modal-backdrop" role = "presentation" >
323+ < section
324+ aria-labelledby = "switchboard-import-title"
325+ aria-modal = "true"
326+ className = "sb-import-dialog"
327+ role = "dialog"
328+ >
329+ < div className = "sb-import-head" >
330+ < div className = "sb-import-title-row" >
331+ < span className = "sb-import-icon" aria-hidden = "true" >
332+ < KeyRound size = { 18 } />
333+ </ span >
334+ < div >
335+ < h2 id = "switchboard-import-title" > { t ( "switchboard.import.title" ) } </ h2 >
336+ < p > { t ( "switchboard.import.subtitle" , { target : targetLabel } ) } </ p >
337+ </ div >
338+ </ div >
339+ < button className = "sb-mini-icon-button" type = "button" onClick = { onCancel } title = { t ( "switchboard.import.cancel" ) } >
340+ < X size = { 14 } />
341+ </ button >
342+ </ div >
343+
344+ { providerPatch && (
345+ < div className = "sb-import-section" >
346+ < div className = "sb-import-section-head" >
347+ < strong > { t ( "switchboard.import.providerTitle" ) } </ strong >
348+ < span className = "sb-status-chip sb-status-chip-on" > { targetLabel } </ span >
349+ </ div >
350+ < div className = "sb-import-grid" >
351+ { providerRows . map ( ( row ) => (
352+ < div className = "sb-import-row" key = { row . label } >
353+ < span > { row . label } </ span >
354+ < code > { row . value } </ code >
355+ </ div >
356+ ) ) }
357+ </ div >
358+ </ div >
359+ ) }
360+
361+ { hasMcp && (
362+ < div className = "sb-import-section" >
363+ < div className = "sb-import-section-head" >
364+ < strong > { t ( "switchboard.import.mcpTitle" ) } </ strong >
365+ < span className = "sb-status-chip" > { mcpTargets } </ span >
366+ </ div >
367+ < div className = "sb-import-grid" >
368+ { patch . mcp ?. id && (
369+ < div className = "sb-import-row" >
370+ < span > { t ( "switchboard.mcp.id" ) } </ span >
371+ < code > { patch . mcp . id } </ code >
372+ </ div >
373+ ) }
374+ { patch . mcp ?. specText && (
375+ < div className = "sb-import-row sb-import-row-wide" >
376+ < span > { t ( "switchboard.mcp.spec" ) } </ span >
377+ < code > { compactPreview ( patch . mcp . specText ) } </ code >
378+ </ div >
379+ ) }
380+ </ div >
381+ </ div >
382+ ) }
383+
384+ < p className = "sb-import-note" > { t ( "switchboard.import.note" ) } </ p >
385+
386+ < div className = "sb-import-actions" >
387+ < button className = "sb-secondary-button" type = "button" onClick = { onCancel } >
388+ { t ( "switchboard.import.cancel" ) }
389+ </ button >
390+ < button className = "sb-primary-button" type = "button" onClick = { onConfirm } >
391+ < CheckCircle2 size = { 16 } />
392+ { t ( "switchboard.import.confirm" ) }
393+ </ button >
394+ </ div >
395+ </ section >
396+ </ div >
397+ ) ;
398+ }
399+
400+ function buildImportProviderRows (
401+ providerPatch : SwitchboardImportPatch [ "provider" ] | null ,
402+ t : ( key : SwitchboardTranslationKey , vars ?: Record < string , string | number > ) => string ,
403+ ) {
404+ if ( ! providerPatch ) {
405+ return [ ] ;
406+ }
407+ return [
408+ { label : t ( "switchboard.provider.name" ) , value : providerPatch . name } ,
409+ { label : t ( "switchboard.provider.baseUrl" ) , value : providerPatch . baseUrl } ,
410+ { label : t ( "switchboard.provider.apiKey" ) , value : maskSecret ( providerPatch . apiKey ) } ,
411+ { label : t ( "switchboard.provider.model" ) , value : providerPatch . model } ,
412+ { label : t ( "switchboard.provider.modelsPath" ) , value : providerPatch . modelsPath } ,
413+ { label : t ( "switchboard.provider.providerId" ) , value : providerPatch . providerId } ,
414+ { label : t ( "switchboard.provider.reasoning" ) , value : providerPatch . reasoningEffort } ,
415+ ] . filter ( ( row ) : row is { label : string ; value : string } => typeof row . value === "string" && row . value . trim ( ) . length > 0 ) ;
416+ }
417+
418+ function maskSecret ( value : string | undefined ) : string | undefined {
419+ if ( ! value ) {
420+ return value ;
421+ }
422+ const trimmed = value . trim ( ) ;
423+ if ( trimmed . length <= 8 ) {
424+ return "****" ;
425+ }
426+ return `${ trimmed . slice ( 0 , 4 ) } ****${ trimmed . slice ( - 4 ) } ` ;
427+ }
428+
429+ function compactPreview ( value : string ) : string {
430+ return value . replace ( / \s + / g, " " ) . trim ( ) ;
431+ }
432+
433+ function parseProviderTarget ( value : string | undefined ) : ProviderForm [ "target" ] | undefined {
434+ return value === "claude" || value === "codex" || value === "both" ? value : undefined ;
435+ }
436+
258437function resolveImportTab (
259438 patch : SwitchboardImportPatch ,
260439 providerPatch : SwitchboardImportPatch [ "provider" ] | null ,
@@ -307,7 +486,7 @@ function parseImportPatch(params: Record<string, string>): SwitchboardImportPatc
307486 }
308487
309488 const provider = normalizeProviderPatch ( {
310- target : params . target ,
489+ target : parseProviderTarget ( params . target ) ,
311490 name : params . name ,
312491 baseUrl : params . baseUrl ?? params . base_url ,
313492 apiKey : params . apiKey ?? params . api_key ,
0 commit comments