window.__ModuleLoader__.load({ id: 'dsh-abap-workbench', factory(require) { const React = require('react') const h = React.createElement const { useCallback, useEffect, useMemo, useState } = React 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: '新增目的地', name: '名称', 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: { 'read-only': '只读:写/激活/删除/传输/执行/调试全部关闭,只做调查与补丁建议。', debug: '调试:允许执行以触发断点,可用 adt_debug 设断点/单步/看变量;写与传输仍关闭。', 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', name: 'Name', 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: { 'read-only': 'Read-only: writes, activation, deletion, transports, execution and the debugger are off; investigate and propose patches only.', debug: 'Debug: execution is allowed to trigger breakpoints; adt_debug is available; writes and transports stay off.', 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() { return { name: '', url: '', client: '100', language: 'EN', username: '', passwordEnv: '', strictSSL: false, profile: 'dev', description: '' } } 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 destinationNames = useMemo( () => destinations.map((dest) => dest.name).filter((value) => value && value.trim()), [destinations], ) const updateDestination = (index, key, value) => { setDestinations((rows) => rows.map((row, rowIndex) => (rowIndex === index ? { ...row, [key]: value } : row))) } const addDestination = () => { setDestinations((rows) => [...rows, blankDestination()]) setMessage('') } const removeDestination = (index) => { 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, })) const payload = await api('/save', { method: 'POST', body: JSON.stringify({ workspace, defaultDestination, destinations: payloadDestinations }), }) 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', { style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 10 } }, 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'), destinationNames.map((value) => h('option', { key: value, value }, value)), ), ), ), 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' const options = ['demo', ...names.filter((name) => name !== 'demo')] 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, }, }, options.map((name) => h('option', { key: name, value: name }, name)), ), 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, ), ) }, } }, })