2026-10-19 12:36:27 +08:00
window . _ _ModuleLoader _ _ . load ( {
id : 'dsh-abap-workbench' ,
factory ( require ) {
const React = require ( 'react' )
const h = React . createElement
2026-10-19 14:14:26 +08:00
const { useCallback , useEffect , useState } = React
2026-10-19 12:36:27 +08:00
const NS = 'abap-adt-ui'
const API = '/abap-adt'
const DICTS = {
zh : {
sectionLabel : 'ABAP ADT' ,
title : 'ABAP ADT 配置' ,
subtitle : '管理当前工作区的 SAP ADT 目的地(写入 .dsh-abap-adt/destinations.yaml,立即生效)' ,
readOnlyBadge : '只读预设' ,
workspace : '工作区' ,
workspaceHint : '目的地文件写到该工作区的 .dsh-abap-adt/ 目录' ,
refresh : '刷新' ,
destinations : '目的地' ,
empty : '还没有配置目的地。内置 demo 模拟系统始终可用。' ,
add : '新增目的地' ,
2026-10-19 14:14:26 +08:00
name : '标识(工具调用用)' ,
displayName : '显示名(中文)' ,
displayNameHint : '只用于界面展示;工具调用仍用「标识」' ,
2026-10-19 12:36:27 +08:00
url : 'URL' ,
client : '客户端' ,
language : '语言' ,
username : '用户名' ,
passwordEnv : '密码引用' ,
passwordHint : '只填引用名(如 ADT_DEV_PASSWORD),真值存 DSH 凭据库或环境变量;不要写明文' ,
strictSSL : '严格 TLS' ,
profile : '环境' ,
description : '说明' ,
defaultDestination : '默认目的地' ,
save : '保存' ,
saving : '保存中…' ,
saved : '已保存' ,
modeTitle : '运行模式(本工作区)' ,
modes : { 'read-only' : '只读' , debug : '调试' , change : '更改' } ,
modeHints : {
2026-10-21 10:32:46 +08:00
'read-only' : '只读:写/激活/删除/执行/调试关闭;可搜索、读源码、读传输请求与版本、数据预览。' ,
debug : '调试:在只读基础上允许执行以触发断点,可用 adt_debug;写与传输修改仍关闭。' ,
2026-10-19 12:36:27 +08:00
change : '更改:按白名单可写(含传输,每次写需给请求号),允许执行与调试器;写前先给用户看 diff。' ,
} ,
modeSwitched : '运行模式已切换' ,
whitelistTitle : '可写白名单(更改模式)' ,
allowedPackages : '允许编辑的包' ,
allowedPackagesHint : '逗号分隔的 glob,例如 Z*,$TMP,Y*; $TMP 为本地对象包' ,
allowedTransports : '允许的传输请求号' ,
allowedTransportsHint : '逗号分隔的 glob,例如 D01K96*;* 表示不限制(仍要求用户提供请求号)' ,
whitelistDisabled : '白名单只在「更改」模式生效;先切到更改模式再编辑' ,
applyWhitelist : '应用白名单' ,
whitelistSaved : '白名单已保存并写入策略' ,
policyTitle : '当前生效策略(来自 destinations.yaml 的 policy 段)' ,
policyNote : '运行模式由本工作区决定(上方切换),以 policy 段写入 destinations.yaml;预设本身只带安全基线(只读)。' ,
test : '测试连接' ,
testing : '测试中…' ,
remove : '删除' ,
loadError : '读取配置失败' ,
saveError : '保存失败' ,
chipPrefix : 'SAP' ,
chipDemo : 'demo' ,
chipDest : '个目的地' ,
chipReadOnly : '只读' ,
chipError : 'ADT 状态未知' ,
chipTitle : '切换该工作区的默认 ADT 目的地(作用于本工作区所有会话)' ,
chipSwitched : '已切换,下次 adt_* 调用生效' ,
chipSwitchFailed : '切换失败' ,
chipGateNote : '状态条只在对该工作区配置了 ABAP(存在 .dsh-abap-adt/destinations.yaml)时显示;可在插件配置 workspaces 白名单里进一步限定。' ,
fileLabel : '配置文件' ,
notExists : '(尚未创建,保存后生成)' ,
never : '—' ,
} ,
en : {
sectionLabel : 'ABAP ADT' ,
title : 'ABAP ADT Settings' ,
subtitle : 'Manage SAP ADT destinations for this workspace (written to .dsh-abap-adt/destinations.yaml, applied immediately)' ,
readOnlyBadge : 'Read-only preset' ,
workspace : 'Workspace' ,
workspaceHint : 'The destinations file is written under .dsh-abap-adt/ in this workspace' ,
refresh : 'Refresh' ,
destinations : 'Destinations' ,
empty : 'No destinations configured yet. The built-in demo mock system is always available.' ,
add : 'Add destination' ,
2026-10-19 14:14:26 +08:00
name : 'Identifier (for tool calls)' ,
displayName : 'Display name' ,
displayNameHint : 'Shown in the UI only; tool calls still use the identifier' ,
2026-10-19 12:36:27 +08:00
url : 'URL' ,
client : 'Client' ,
language : 'Language' ,
username : 'User' ,
passwordEnv : 'Password ref' ,
passwordHint : 'Use a reference name (e.g. ADT_DEV_PASSWORD); the value lives in the DSH credential store or environment. Never paste a password here.' ,
strictSSL : 'Strict TLS' ,
profile : 'Profile' ,
description : 'Description' ,
defaultDestination : 'Default destination' ,
save : 'Save' ,
saving : 'Saving…' ,
saved : 'Saved' ,
modeTitle : 'Run mode (this workspace)' ,
modes : { 'read-only' : 'Read-only' , debug : 'Debug' , change : 'Change' } ,
modeHints : {
2026-10-21 10:32:46 +08:00
'read-only' : 'Read-only: writes, activation, deletion, execution and the debugger are off; search, read source, READ transport requests/versions and preview data.' ,
debug : 'Debug: read-only plus execution to trigger breakpoints and adt_debug; writes and transport changes stay off.' ,
2026-10-19 12:36:27 +08:00
change : 'Change: whitelisted packages are writable (transports need the request number on every write), execution and the debugger are allowed; show a diff before writing.' ,
} ,
modeSwitched : 'Run mode switched' ,
whitelistTitle : 'Write whitelists (change mode)' ,
allowedPackages : 'Editable packages' ,
allowedPackagesHint : 'Comma-separated globs, e.g. Z*,$TMP,Y*; $TMP is the local-objects package' ,
allowedTransports : 'Allowed transport numbers' ,
allowedTransportsHint : 'Comma-separated globs, e.g. D01K96*; * means unrestricted (the request number is still required from the user)' ,
whitelistDisabled : 'Whitelists only apply in Change mode; switch to Change first' ,
applyWhitelist : 'Apply whitelists' ,
whitelistSaved : 'Whitelists saved and written into the policy' ,
policyTitle : 'Effective policy (from the destinations.yaml policy block)' ,
policyNote : 'The run mode belongs to this workspace (switch above) and is written as a policy block into destinations.yaml; the preset itself only carries the read-only baseline.' ,
test : 'Test connection' ,
testing : 'Testing…' ,
remove : 'Remove' ,
policyTitle : 'Permission policy (locked by the preset)' ,
policyNote : 'The current session preset locks SAP writes (sentinel allowedPackages + transports/execution/debugger off). This page manages destinations only; switch to a writable preset to edit code.' ,
loadError : 'Failed to load configuration' ,
saveError : 'Save failed' ,
chipPrefix : 'SAP' ,
chipDemo : 'demo' ,
chipDest : 'dest(s)' ,
chipReadOnly : 'read-only' ,
chipError : 'ADT status unknown' ,
chipTitle : 'Switch this workspace default ADT destination (applies to every session in the workspace)' ,
chipSwitched : 'Switched — applies to the next adt_* call' ,
chipSwitchFailed : 'Switch failed' ,
chipGateNote : 'The status chip only appears where ABAP is configured for the workspace (.dsh-abap-adt/destinations.yaml exists); narrow it further with the plugin config workspaces allowlist.' ,
fileLabel : 'Config file' ,
notExists : ' (not created yet — save to create)' ,
never : '—' ,
} ,
}
const C = {
text : 'var(--dsw-alias-label-primary, #1f2328)' ,
muted : 'var(--dsw-alias-label-secondary, #6b7280)' ,
border : 'var(--dsw-alias-border-l1, rgba(128,128,128,.28))' ,
borderStrong : 'var(--dsw-alias-border-l2, rgba(128,128,128,.45))' ,
surface : 'var(--dsw-alias-bg-layer-1, #ffffff)' ,
surface2 : 'var(--dsw-alias-bg-layer-2, #f5f6f7)' ,
brand : 'var(--dsw-alias-brand-primary, #0a6ed1)' ,
ok : 'var(--dsw-alias-state-success-primary, #1a7f37)' ,
err : 'var(--dsw-alias-state-error-primary, #cf222e)' ,
warn : 'var(--dsw-alias-state-warn-primary, #9a6700)' ,
}
const inputStyle = {
boxSizing : 'border-box' ,
width : '100%' ,
padding : '6px 8px' ,
fontSize : 13 ,
color : C . text ,
background : C . surface ,
border : ` 1px solid ${ C . border } ` ,
borderRadius : 6 ,
outline : 'none' ,
}
const buttonStyle = ( primary ) => ( {
padding : '6px 12px' ,
fontSize : 13 ,
cursor : 'pointer' ,
borderRadius : 6 ,
border : ` 1px solid ${ primary ? C . brand : C . borderStrong } ` ,
background : primary ? C . brand : 'transparent' ,
color : primary ? '#ffffff' : C . text ,
} )
const smallButtonStyle = {
padding : '4px 8px' ,
fontSize : 12 ,
cursor : 'pointer' ,
borderRadius : 5 ,
border : ` 1px solid ${ C . border } ` ,
background : 'transparent' ,
color : C . text ,
}
async function api ( path , init ) {
const response = await fetch ( ` ${ API } ${ path } ` , {
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
... init ,
} )
const payload = await response . json ( ) . catch ( ( ) => ( { ok : false , error : ` HTTP ${ response . status } ` } ) )
if ( ! response . ok || payload . ok === false ) throw new Error ( payload . error || ` HTTP ${ response . status } ` )
return payload
}
function blankDestination ( ) {
2026-10-19 14:14:26 +08:00
return { name : '' , label : '' , url : '' , client : '100' , language : 'EN' , username : '' , passwordEnv : '' , strictSSL : false , profile : 'dev' , description : '' }
2026-10-19 12:36:27 +08:00
}
function Field ( { label , hint , children } ) {
return h (
'label' ,
{ style : { display : 'flex' , flexDirection : 'column' , gap : 4 , minWidth : 0 } } ,
h ( 'span' , { style : { fontSize : 12 , color : C . muted } } , label ) ,
children ,
hint ? h ( 'span' , { style : { fontSize : 11 , color : C . muted , lineHeight : 1.4 } } , hint ) : null ,
)
}
function Badge ( { children , tone } ) {
const toneColor =
tone === 'ok' ? C . ok : tone === 'warn' ? C . warn : tone === 'err' ? C . err : tone === 'brand' ? C . brand : C . muted
return h (
'span' ,
{
style : {
display : 'inline-flex' ,
alignItems : 'center' ,
gap : 4 ,
padding : '2px 8px' ,
fontSize : 11 ,
fontWeight : 600 ,
borderRadius : 999 ,
border : ` 1px solid ${ toneColor } ` ,
color : toneColor ,
background : 'transparent' ,
whiteSpace : 'nowrap' ,
} ,
} ,
children ,
)
}
function SettingsPage ( ) {
const [ loading , setLoading ] = useState ( true )
const [ error , setError ] = useState ( '' )
const [ workspace , setWorkspace ] = useState ( '' )
const [ workspaces , setWorkspaces ] = useState ( [ ] )
const [ file , setFile ] = useState ( null )
const [ defaultDestination , setDefaultDestination ] = useState ( '' )
const [ destinations , setDestinations ] = useState ( [ ] )
const [ policy , setPolicy ] = useState ( { } )
const [ mode , setMode ] = useState ( 'read-only' )
const [ whitelist , setWhitelist ] = useState ( null )
const [ saving , setSaving ] = useState ( false )
const [ message , setMessage ] = useState ( '' )
const [ messageTone , setMessageTone ] = useState ( 'ok' )
const [ tests , setTests ] = useState ( { } )
const [ testing , setTesting ] = useState ( { } )
const load = useCallback ( async ( target ) => {
setLoading ( true )
setError ( '' )
setMessage ( '' )
try {
const query = new URLSearchParams ( )
if ( target ) query . set ( 'workspace' , target )
const payload = await api ( ` /state? ${ query . toString ( ) } ` )
setWorkspace ( payload . workspace || '' )
setWorkspaces ( payload . workspaces || [ ] )
setFile ( payload . file || null )
setDefaultDestination ( payload . defaultDestination || '' )
setDestinations ( payload . destinations || [ ] )
setPolicy ( payload . policy || { } )
setMode ( payload . mode || 'read-only' )
setWhitelist ( payload . whitelist || null )
setTests ( { } )
} catch ( err ) {
setError ( err . message )
} finally {
setLoading ( false )
}
} , [ ] )
useEffect ( ( ) => {
load ( )
} , [ load ] )
const updateDestination = ( index , key , value ) => {
2026-10-19 14:14:26 +08:00
const row = destinations [ index ]
// Renaming the identifier keeps the default destination in sync, so a
// rename cannot leave defaultDestination pointing at a gone name.
if ( key === 'name' && row && defaultDestination === row . name ) setDefaultDestination ( value )
setDestinations ( ( rows ) => rows . map ( ( item , itemIndex ) => ( itemIndex === index ? { ... item , [ key ] : value } : item ) ) )
2026-10-19 12:36:27 +08:00
}
const addDestination = ( ) => {
setDestinations ( ( rows ) => [ ... rows , blankDestination ( ) ] )
setMessage ( '' )
}
const removeDestination = ( index ) => {
2026-10-19 14:14:26 +08:00
const removed = destinations [ index ]
if ( removed && removed . name && removed . name === defaultDestination ) setDefaultDestination ( '' )
2026-10-19 12:36:27 +08:00
setDestinations ( ( rows ) => rows . filter ( ( _ , rowIndex ) => rowIndex !== index ) )
setMessage ( '' )
}
const save = async ( ) => {
setSaving ( true )
setMessage ( '' )
try {
// Round-trip only the fields this page edits: schema defaults such as
// timeoutMs must not be re-serialized from a display value.
const payloadDestinations = destinations . map ( ( dest ) => ( {
name : dest . name ,
url : dest . url ,
description : dest . description ,
client : dest . client ,
language : dest . language ,
username : dest . username ,
passwordEnv : dest . passwordEnv ,
strictSSL : dest . strictSSL ,
profile : dest . profile ,
} ) )
2026-10-19 14:14:26 +08:00
// Display labels are UI-only and keyed by the stable identifier.
const labels = { }
for ( const dest of destinations ) {
const name = String ( dest . name || '' ) . trim ( )
const label = String ( dest . label || '' ) . trim ( )
if ( name && label ) labels [ name ] = label
}
2026-10-19 12:36:27 +08:00
const payload = await api ( '/save' , {
method : 'POST' ,
2026-10-19 14:14:26 +08:00
body : JSON . stringify ( { workspace , defaultDestination , destinations : payloadDestinations , labels } ) ,
2026-10-19 12:36:27 +08:00
} )
setMessage ( ` ${ DICTS . zh . saved } : ${ payload . path } ` )
setMessageTone ( 'ok' )
await load ( workspace )
} catch ( err ) {
setMessage ( ` ${ DICTS . zh . saveError } : ${ err . message } ` )
setMessageTone ( 'err' )
} finally {
setSaving ( false )
}
}
const switchMode = async ( next ) => {
if ( next === mode || ! workspace ) return
setSaving ( true )
setMessage ( '' )
try {
await api ( '/mode' , { method : 'POST' , body : JSON . stringify ( { workspace , mode : next } ) } )
await load ( workspace )
setMessage ( ` ${ DICTS . zh . modeSwitched } : ${ DICTS . zh . modes [ next ] } ` )
setMessageTone ( 'ok' )
} catch ( err ) {
setMessage ( ` ${ DICTS . zh . saveError } : ${ err . message } ` )
setMessageTone ( 'err' )
} finally {
setSaving ( false )
}
}
const applyWhitelist = async ( ) => {
if ( ! workspace || ! whitelist ) return
setSaving ( true )
setMessage ( '' )
try {
await api ( '/mode' , {
method : 'POST' ,
body : JSON . stringify ( {
workspace ,
mode ,
allowedPackages : whitelist . allowedPackages ,
allowedTransports : whitelist . allowedTransports ,
} ) ,
} )
await load ( workspace )
setMessage ( DICTS . zh . whitelistSaved )
setMessageTone ( 'ok' )
} catch ( err ) {
setMessage ( ` ${ DICTS . zh . saveError } : ${ err . message } ` )
setMessageTone ( 'err' )
} finally {
setSaving ( false )
}
}
const test = async ( dest ) => {
const key = dest . name || ` # ${ destinations . indexOf ( dest ) + 1 } `
setTesting ( ( map ) => ( { ... map , [ key ] : true } ) )
try {
const payload = await api ( '/ping' , {
method : 'POST' ,
body : JSON . stringify ( {
url : dest . url ,
client : dest . client ,
language : dest . language ,
username : dest . username ,
passwordEnv : dest . passwordEnv ,
strictSSL : dest . strictSSL ,
} ) ,
} )
setTests ( ( map ) => ( { ... map , [ key ] : payload . result } ) )
} catch ( err ) {
setTests ( ( map ) => ( { ... map , [ key ] : { ok : false , error : err . message , status : 0 , ms : 0 } } ) )
} finally {
setTesting ( ( map ) => ( { ... map , [ key ] : false } ) )
}
}
if ( loading ) {
return h ( 'div' , { style : { padding : 24 , color : C . muted , fontSize : 13 } } , '…' )
}
const header = h (
'div' ,
{ style : { display : 'flex' , alignItems : 'center' , gap : 10 , flexWrap : 'wrap' } } ,
h ( 'div' , { style : { fontSize : 18 , fontWeight : 700 , color : C . text } } , DICTS . zh . title ) ,
h ( Badge , { tone : mode === 'change' ? 'err' : mode === 'debug' ? 'brand' : 'warn' } , DICTS . zh . modes [ mode ] || mode ) ,
h ( 'div' , { style : { flex : 1 } } ) ,
h ( 'button' , { style : smallButtonStyle , onClick : ( ) => load ( workspace ) , type : 'button' } , DICTS . zh . refresh ) ,
)
const errorBox = error
? h (
'div' ,
{
style : {
marginTop : 12 ,
padding : '10px 12px' ,
borderRadius : 8 ,
border : ` 1px solid ${ C . err } ` ,
color : C . err ,
fontSize : 13 ,
} ,
} ,
` ${ DICTS . zh . loadError } : ${ error } ` ,
)
: null
const workspaceRow = h (
'div' ,
{ style : { display : 'grid' , gridTemplateColumns : '2fr 1fr' , gap : 12 , marginTop : 16 } } ,
h (
Field ,
{ label : DICTS . zh . workspace , hint : DICTS . zh . workspaceHint } ,
h ( 'input' , {
style : inputStyle ,
value : workspace ,
list : 'abap-adt-workspaces' ,
onChange : ( event ) => setWorkspace ( event . target . value ) ,
onBlur : ( ) => {
if ( workspace && workspace !== file ? . path ? . replace ( /[\\/]\.dsh-abap-adt[\\/].*$/ , '' ) ) load ( workspace )
} ,
placeholder : 'D:\\AIagent\\SAP' ,
} ) ,
) ,
h (
Field ,
{ label : DICTS . zh . fileLabel } ,
h (
'div' ,
{ style : { fontSize : 12 , color : C . muted , wordBreak : 'break-all' , paddingTop : 6 } } ,
file ? . path ? ` ${ file . path } ${ file . exists ? '' : DICTS . zh . notExists } ` : DICTS . zh . never ,
) ,
) ,
h (
'datalist' ,
{ id : 'abap-adt-workspaces' } ,
workspaces . map ( ( item ) => h ( 'option' , { key : item . id || item . path , value : item . path } , item . title ) ) ,
) ,
)
const destinationCards = destinations . length
? destinations . map ( ( dest , index ) => {
const key = dest . name || ` # ${ index + 1 } `
const result = tests [ key ]
return h (
'div' ,
{
key : index ,
style : {
marginTop : 12 ,
padding : 12 ,
borderRadius : 10 ,
border : ` 1px solid ${ C . border } ` ,
background : C . surface2 ,
display : 'flex' ,
flexDirection : 'column' ,
gap : 10 ,
} ,
} ,
h (
'div' ,
2026-10-19 14:14:26 +08:00
{ style : { display : 'grid' , gridTemplateColumns : '1.2fr 1fr 1.6fr 0.55fr 0.55fr' , gap : 10 } } ,
h ( Field , { label : DICTS . zh . displayName , hint : DICTS . zh . displayNameHint } , h ( 'input' , { style : inputStyle , value : dest . label || '' , onChange : ( e ) => updateDestination ( index , 'label' , e . target . value ) , placeholder : '广东开发' } ) ) ,
2026-10-19 12:36:27 +08:00
h ( Field , { label : DICTS . zh . name } , h ( 'input' , { style : inputStyle , value : dest . name || '' , onChange : ( e ) => updateDestination ( index , 'name' , e . target . value ) , placeholder : 'dev' } ) ) ,
h ( Field , { label : DICTS . zh . url } , h ( 'input' , { style : inputStyle , value : dest . url || '' , onChange : ( e ) => updateDestination ( index , 'url' , e . target . value ) , placeholder : 'https://sap.example.com:44300' } ) ) ,
h ( Field , { label : DICTS . zh . client } , h ( 'input' , { style : inputStyle , value : dest . client || '' , onChange : ( e ) => updateDestination ( index , 'client' , e . target . value ) , placeholder : '100' } ) ) ,
h ( Field , { label : DICTS . zh . language } , h ( 'input' , { style : inputStyle , value : dest . language || '' , onChange : ( e ) => updateDestination ( index , 'language' , e . target . value ) , placeholder : 'ZH' } ) ) ,
) ,
h (
'div' ,
{ style : { display : 'grid' , gridTemplateColumns : 'repeat(4, minmax(0, 1fr))' , gap : 10 } } ,
h ( Field , { label : DICTS . zh . username } , h ( 'input' , { style : inputStyle , value : dest . username || '' , onChange : ( e ) => updateDestination ( index , 'username' , e . target . value ) } ) ) ,
h ( Field , { label : DICTS . zh . passwordEnv , hint : DICTS . zh . passwordHint } , h ( 'input' , { style : inputStyle , value : dest . passwordEnv || '' , onChange : ( e ) => updateDestination ( index , 'passwordEnv' , e . target . value ) , placeholder : 'ADT_DEV_PASSWORD' } ) ) ,
h ( Field , { label : DICTS . zh . profile } , h ( 'select' , { style : inputStyle , value : dest . profile || 'dev' , onChange : ( e ) => updateDestination ( index , 'profile' , e . target . value ) } , [ 'dev' , 'qa' , 'prd' ] . map ( ( value ) => h ( 'option' , { key : value , value } , value ) ) ) ) ,
h ( Field , { label : DICTS . zh . description } , h ( 'input' , { style : inputStyle , value : dest . description || '' , onChange : ( e ) => updateDestination ( index , 'description' , e . target . value ) } ) ) ,
) ,
h (
'div' ,
{ style : { display : 'flex' , alignItems : 'center' , gap : 12 , flexWrap : 'wrap' } } ,
h (
'label' ,
{ style : { display : 'flex' , alignItems : 'center' , gap : 6 , fontSize : 12 , color : C . muted } } ,
h ( 'input' , { type : 'checkbox' , checked : dest . strictSSL !== false , onChange : ( e ) => updateDestination ( index , 'strictSSL' , e . target . checked ) } ) ,
DICTS . zh . strictSSL ,
) ,
h ( 'div' , { style : { flex : 1 } } ) ,
result
? h (
'span' ,
{ style : { fontSize : 12 , color : result . ok ? C . ok : C . err } } ,
` ${ result . status || 'ERR' } · ${ result . ms } ms ${ result . error ? ` · ${ result . error } ` : '' } ` ,
)
: null ,
h ( 'button' , { type : 'button' , style : smallButtonStyle , onClick : ( ) => test ( dest ) , disabled : testing [ key ] } , testing [ key ] ? DICTS . zh . testing : DICTS . zh . test ) ,
h ( 'button' , { type : 'button' , style : smallButtonStyle , onClick : ( ) => removeDestination ( index ) } , DICTS . zh . remove ) ,
) ,
)
} )
: h ( 'div' , { style : { marginTop : 12 , fontSize : 13 , color : C . muted } } , DICTS . zh . empty )
const policyRows = Object . entries ( policy ) . length
? Object . entries ( policy ) . map ( ( [ key , value ] ) =>
h (
'div' ,
{ key , style : { display : 'flex' , gap : 10 , fontSize : 12 , padding : '3px 0' } } ,
h ( 'span' , { style : { minWidth : 210 , color : C . muted , fontFamily : 'ui-monospace, monospace' } } , key ) ,
h ( 'span' , { style : { color : C . text , wordBreak : 'break-all' } } , Array . isArray ( value ) ? value . join ( ', ' ) || '(empty)' : String ( value ) ) ,
) ,
)
: h ( 'div' , { style : { fontSize : 12 , color : C . muted } } , '—' )
return h (
'div' ,
{ style : { padding : '20px 24px 40px' , color : C . text , maxWidth : 1080 } } ,
header ,
h ( 'div' , { style : { marginTop : 6 , fontSize : 12.5 , color : C . muted } } , DICTS . zh . subtitle ) ,
errorBox ,
workspaceRow ,
h ( 'div' , { style : { marginTop : 6 , fontSize : 11.5 , color : C . muted , lineHeight : 1.5 } } , DICTS . zh . chipGateNote ) ,
h (
'div' ,
{
style : {
marginTop : 16 ,
padding : 12 ,
borderRadius : 10 ,
border : ` 1px solid ${ C . border } ` ,
background : C . surface2 ,
} ,
} ,
h (
'div' ,
{ style : { display : 'flex' , alignItems : 'center' , gap : 8 , flexWrap : 'wrap' } } ,
h ( 'span' , { style : { fontSize : 13 , fontWeight : 650 } } , DICTS . zh . modeTitle ) ,
[ 'read-only' , 'debug' , 'change' ] . map ( ( value ) =>
h (
'button' ,
{
key : value ,
type : 'button' ,
disabled : saving || ! workspace ,
onClick : ( ) => switchMode ( value ) ,
style : { ... buttonStyle ( value === mode ) , padding : '5px 14px' } ,
} ,
DICTS . zh . modes [ value ] ,
) ,
) ,
h ( 'div' , { style : { flex : 1 } } ) ,
h ( Badge , { tone : mode === 'change' ? 'err' : mode === 'debug' ? 'brand' : 'warn' } , DICTS . zh . modes [ mode ] || mode ) ,
) ,
h ( 'div' , { style : { marginTop : 8 , fontSize : 12 , color : C . muted , lineHeight : 1.5 } } , DICTS . zh . modeHints [ mode ] || '' ) ,
whitelist
? h (
'div' ,
{ style : { marginTop : 12 , paddingTop : 12 , borderTop : ` 1px solid ${ C . border } ` } } ,
h ( 'div' , { style : { fontSize : 12.5 , fontWeight : 600 , marginBottom : 8 } } , DICTS . zh . whitelistTitle ) ,
h (
'div' ,
{ style : { display : 'grid' , gridTemplateColumns : '1fr 1fr auto' , gap : 12 , alignItems : 'end' } } ,
h (
Field ,
{
label : DICTS . zh . allowedPackages ,
hint : whitelist . editable ? DICTS . zh . allowedPackagesHint : DICTS . zh . whitelistDisabled ,
} ,
h ( 'input' , {
style : inputStyle ,
value : whitelist . allowedPackages || '' ,
disabled : ! whitelist . editable || saving ,
onChange : ( event ) => setWhitelist ( ( value ) => ( { ... value , allowedPackages : event . target . value } ) ) ,
} ) ,
) ,
h (
Field ,
{
label : DICTS . zh . allowedTransports ,
hint : whitelist . editable ? DICTS . zh . allowedTransportsHint : DICTS . zh . whitelistDisabled ,
} ,
h ( 'input' , {
style : inputStyle ,
value : whitelist . allowedTransports || '' ,
disabled : ! whitelist . editable || saving ,
onChange : ( event ) => setWhitelist ( ( value ) => ( { ... value , allowedTransports : event . target . value } ) ) ,
} ) ,
) ,
h (
'button' ,
{
type : 'button' ,
style : buttonStyle ( true ) ,
disabled : ! whitelist . editable || saving ,
onClick : applyWhitelist ,
} ,
DICTS . zh . applyWhitelist ,
) ,
) ,
)
: null ,
) ,
h (
'div' ,
{ style : { marginTop : 24 , display : 'flex' , alignItems : 'center' , gap : 10 } } ,
h ( 'div' , { style : { fontSize : 15 , fontWeight : 650 } } , DICTS . zh . destinations ) ,
h ( 'div' , { style : { flex : 1 } } ) ,
h ( 'button' , { type : 'button' , style : smallButtonStyle , onClick : addDestination } , ` + ${ DICTS . zh . add } ` ) ,
) ,
destinationCards ,
h (
'div' ,
{ style : { marginTop : 16 , display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } ,
h (
Field ,
{ label : DICTS . zh . defaultDestination } ,
h (
'select' ,
{ style : inputStyle , value : defaultDestination , onChange : ( e ) => setDefaultDestination ( e . target . value ) } ,
h ( 'option' , { value : '' } , 'demo' ) ,
2026-10-19 14:14:26 +08:00
destinations
. filter ( ( dest ) => dest . name )
. map ( ( dest ) =>
h ( 'option' , { key : dest . name , value : dest . name } , dest . label ? ` ${ dest . label } ( ${ dest . name } ) ` : dest . name ) ,
) ,
2026-10-19 12:36:27 +08:00
) ,
) ,
) ,
h (
'div' ,
{ style : { marginTop : 16 , display : 'flex' , alignItems : 'center' , gap : 12 } } ,
h ( 'button' , { type : 'button' , style : buttonStyle ( true ) , onClick : save , disabled : saving || ! workspace } , saving ? DICTS . zh . saving : DICTS . zh . save ) ,
message ? h ( 'span' , { style : { fontSize : 12 , color : messageTone === 'err' ? C . err : C . ok , wordBreak : 'break-all' } } , message ) : null ,
) ,
h (
'div' ,
{ style : { marginTop : 28 , padding : 14 , borderRadius : 10 , border : ` 1px solid ${ C . border } ` , background : C . surface2 } } ,
h ( 'div' , { style : { fontSize : 14 , fontWeight : 650 } } , DICTS . zh . policyTitle ) ,
h ( 'div' , { style : { marginTop : 6 , fontSize : 12.5 , color : C . muted , lineHeight : 1.5 } } , DICTS . zh . policyNote ) ,
h ( 'div' , { style : { marginTop : 10 } } , policyRows ) ,
) ,
)
}
function StatusChip ( props ) {
const sessionId = props ? . sessionId
const [ state , setState ] = useState ( null )
const [ failed , setFailed ] = useState ( false )
const [ busy , setBusy ] = useState ( false )
const [ message , setMessage ] = useState ( '' )
useEffect ( ( ) => {
let alive = true
const query = sessionId ? ` ?sessionId= ${ encodeURIComponent ( sessionId ) } ` : ''
api ( ` /state ${ query } ` )
. then ( ( payload ) => {
if ( alive ) setState ( payload )
} )
. catch ( ( ) => {
if ( alive ) setFailed ( true )
} )
return ( ) => {
alive = false
}
} , [ sessionId ] )
if ( failed ) return null
if ( ! state ) return null
// Workspace gate: only workspaces that actually have an ABAP
// destinations file (or are explicitly allowlisted on the Host) get the
// chip — unrelated workspaces and modes stay clean.
const enabled = state . chip ? state . chip . enabled : Boolean ( state . file ? . exists )
if ( ! enabled ) return null
const destinations = state . destinations || [ ]
const names = destinations . map ( ( dest ) => dest . name ) . filter ( Boolean )
const selected = state . defaultDestination || 'demo'
2026-10-19 14:14:26 +08:00
const options = [
{ name : 'demo' , label : DICTS . zh . chipDemo } ,
... destinations
. filter ( ( dest ) => dest . name && dest . name !== 'demo' )
. map ( ( dest ) => ( { name : dest . name , label : dest . label || dest . name } ) ) ,
]
2026-10-19 12:36:27 +08:00
const modeText = DICTS . zh . modes [ state . mode ]
const modeBadge = modeText
? { text : modeText , tone : state . mode === 'change' ? 'err' : state . mode === 'debug' ? 'brand' : 'warn' }
: null
const switchDefault = async ( value ) => {
if ( value === selected ) return
setBusy ( true )
setMessage ( '' )
try {
await api ( '/default' , {
method : 'POST' ,
body : JSON . stringify ( { workspace : state . workspace , defaultDestination : value } ) ,
} )
setState ( ( previous ) => ( previous ? { ... previous , defaultDestination : value } : previous ) )
setMessage ( DICTS . zh . chipSwitched )
} catch ( err ) {
setMessage ( ` ${ DICTS . zh . chipSwitchFailed } : ${ err . message } ` )
} finally {
setBusy ( false )
}
}
return h (
'span' ,
{
title : ` ${ DICTS . zh . chipTitle } \n ${ state . file ? . path || '' } ` ,
style : {
display : 'inline-flex' ,
alignItems : 'center' ,
gap : 6 ,
padding : '3px 8px' ,
borderRadius : 999 ,
border : ` 1px solid ${ C . border } ` ,
background : 'transparent' ,
fontSize : 11.5 ,
color : C . muted ,
whiteSpace : 'nowrap' ,
userSelect : 'none' ,
} ,
} ,
h ( 'span' , { style : { width : 6 , height : 6 , borderRadius : '50%' , background : names . length ? C . ok : C . warn } } ) ,
h ( 'span' , { style : { fontSize : 11.5 , color : C . muted } } , DICTS . zh . chipPrefix ) ,
h (
'select' ,
{
value : selected ,
disabled : busy ,
onChange : ( event ) => switchDefault ( event . target . value ) ,
style : {
appearance : 'none' ,
border : 'none' ,
background : 'transparent' ,
color : C . text ,
fontSize : 11.5 ,
fontWeight : 600 ,
padding : '0 2px' ,
cursor : busy ? 'progress' : 'pointer' ,
maxWidth : 170 ,
} ,
} ,
2026-10-19 14:14:26 +08:00
options . map ( ( option ) =>
h (
'option' ,
{ key : option . name , value : option . name } ,
option . label && option . label !== option . name ? ` ${ option . label } ( ${ option . name } ) ` : option . name ,
) ,
) ,
2026-10-19 12:36:27 +08:00
) ,
names . length ? h ( 'span' , { style : { fontSize : 11 , color : C . muted } } , ` ${ names . length } ${ DICTS . zh . chipDest } ` ) : null ,
modeBadge ? h ( Badge , { tone : modeBadge . tone } , modeBadge . text ) : null ,
message ? h ( 'span' , { style : { fontSize : 10.5 , color : C . muted , maxWidth : 220 , overflow : 'hidden' , textOverflow : 'ellipsis' } } , message ) : null ,
)
}
return {
inject : [ 'slots' ] ,
apply ( ctx ) {
let t = ( key ) => DICTS . zh [ key ] ? ? key
try {
if ( ctx . locale ) {
const disposeLocale = ctx . locale . register ( NS , { en : DICTS . en , zh : DICTS . zh } )
ctx . effect ? . ( ( ) => disposeLocale )
const bound = ctx . locale . bind ( NS )
t = ( key , params ) => bound ( key , params )
}
} catch ( err ) {
// Locale is optional: fall back to the shipped Chinese strings.
console . warn ( 'abap-adt-ui: locale registration failed' , err )
}
ctx . slots . inject ( 'settings.section' , ( ) =>
ctx . slots . register (
{
name : 'settings.section' ,
id : 'abap-adt' ,
order : 30 ,
label : ( ) => t ( 'sectionLabel' ) ,
} ,
SettingsPage ,
) ,
)
ctx . slots . inject ( 'conversation.input.left' , ( ) =>
ctx . slots . register (
{
name : 'conversation.input.left' ,
id : 'abap-adt-status' ,
order : 40 ,
label : ( ) => t ( 'chipPrefix' ) ,
} ,
StatusChip ,
) ,
)
} ,
}
} ,
} )