@ddtcorex/dsh-maestro-config 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +825 -445
- package/lib/index.d.ts +24 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +8 -1
- package/lib/index.js.map +1 -1
- package/lib/types/client/MaestroSettings.d.ts +25 -0
- package/lib/types/client/MaestroSettings.d.ts.map +1 -0
- package/lib/types/client/Settings.d.ts +11 -0
- package/lib/types/client/Settings.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/maestro-card.d.ts.map +1 -1
- package/package.json +4 -6
- package/src/client/MaestroSettings.tsx +1463 -0
- package/src/client/index.tsx +10 -7
- package/src/client/maestro-card.jsx +225 -21
- package/src/host/index.ts +12 -3
package/src/client/index.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
import { MaestroSettingsTab } from './
|
|
1
|
+
// DSH-native redesign — reuses DSH tokens & primitive geometry maximally (see MaestroSettings.tsx).
|
|
2
|
+
import { MaestroSettingsTab } from './MaestroSettings.js'
|
|
3
3
|
import { MAESTRO_RPC_CHANNEL } from './api.js'
|
|
4
4
|
import { registerSettingsNavIcon, SETTINGS_NAV_MARKER } from './settings-nav-icon.js'
|
|
5
5
|
|
|
@@ -7,12 +7,13 @@ import { registerSettingsNavIcon, SETTINGS_NAV_MARKER } from './settings-nav-ico
|
|
|
7
7
|
* DSH 0.1.x gives external settings sections a generic gear and exposes no
|
|
8
8
|
* icon field in the settings.section contract (mirrors dsh-better-sidebar):
|
|
9
9
|
* the marker only claims this plugin's localized row and this CSS paints the
|
|
10
|
-
*
|
|
11
|
-
* hover/active colors at the shell's 16px icon rhythm.
|
|
10
|
+
* Maestro M-logo glyph as a currentColor mask so it follows native nav
|
|
11
|
+
* hover/active colors at the shell's 16px icon rhythm. The path matches the
|
|
12
|
+
* sidebar MaestroTrigger (trigger.tsx#MaestroLogo) — M2 11 L5 4 L8 9 L11 4 L14 11.
|
|
12
13
|
*/
|
|
13
14
|
const SETTINGS_NAV_CSS = `
|
|
14
15
|
|
|
15
|
-
/* maestro: replace the settings-nav fallback gear with the
|
|
16
|
+
/* maestro: replace the settings-nav fallback gear with the Maestro M-logo glyph */
|
|
16
17
|
[${SETTINGS_NAV_MARKER}] > svg:first-child {
|
|
17
18
|
display: none;
|
|
18
19
|
}
|
|
@@ -23,8 +24,8 @@ const SETTINGS_NAV_CSS = `
|
|
|
23
24
|
width: 16px;
|
|
24
25
|
height: 16px;
|
|
25
26
|
background: currentColor;
|
|
26
|
-
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='
|
|
27
|
-
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='
|
|
27
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 11 L5 4 L8 9 L11 4 L14 11'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
28
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 11 L5 4 L8 9 L11 4 L14 11'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
28
29
|
}
|
|
29
30
|
`
|
|
30
31
|
|
|
@@ -42,6 +43,8 @@ interface ClientCtx {
|
|
|
42
43
|
effect(fn: () => () => void, label?: string): unknown
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export const inject = ['slots', 'connection'] as const
|
|
47
|
+
|
|
45
48
|
function installNavIconStyle(): () => void {
|
|
46
49
|
const tag = document.createElement('style')
|
|
47
50
|
tag.dataset.plugin = '@ddtcorex/dsh-maestro-config'
|
|
@@ -86,6 +86,32 @@ const codeStyle = {
|
|
|
86
86
|
wordBreak: 'break-all',
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
const textareaStyle = {
|
|
90
|
+
...inputStyle,
|
|
91
|
+
height: 120,
|
|
92
|
+
padding: '8px 12px',
|
|
93
|
+
resize: 'vertical',
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const tabBarStyle = {
|
|
97
|
+
display: 'flex',
|
|
98
|
+
gap: 8,
|
|
99
|
+
borderBottom: '1px solid var(--dsw-alias-border-l2)',
|
|
100
|
+
marginBottom: 16,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const tabButtonStyle = (active) => ({
|
|
104
|
+
padding: '8px 14px',
|
|
105
|
+
border: 'none',
|
|
106
|
+
borderBottom: active ? '2px solid var(--dsw-alias-button-primary-fill)' : '2px solid transparent',
|
|
107
|
+
background: 'transparent',
|
|
108
|
+
color: active ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
109
|
+
font: 'inherit',
|
|
110
|
+
fontSize: 13,
|
|
111
|
+
fontWeight: active ? 600 : 400,
|
|
112
|
+
cursor: 'pointer',
|
|
113
|
+
})
|
|
114
|
+
|
|
89
115
|
/** QR code centered in a light tile with an even scanner-friendly quiet zone. */
|
|
90
116
|
function QrImage({ url, size = 104 }) {
|
|
91
117
|
const [dataUrl, setDataUrl] = useState(null)
|
|
@@ -373,26 +399,6 @@ function ToggleField({ label, caption, checked, onChange }) {
|
|
|
373
399
|
)
|
|
374
400
|
}
|
|
375
401
|
|
|
376
|
-
/** Newest-first list of recorded review runs from the host's reviews.json. */
|
|
377
|
-
function ReviewHistoryPanel({ rpcCall }) {
|
|
378
|
-
const [entries, setEntries] = useState(null)
|
|
379
|
-
useEffect(() => {
|
|
380
|
-
rpcCall(MAESTRO_ENDPOINTS.reviewsList, {})
|
|
381
|
-
.then(res => { if (res?.ok) setEntries(res.value ?? []) })
|
|
382
|
-
.catch(() => setEntries([]))
|
|
383
|
-
}, [])
|
|
384
|
-
if (entries === null) return h('p', { style: captionStyle }, 'Loading review history…')
|
|
385
|
-
if (entries.length === 0) return h('p', { style: captionStyle }, 'No reviews recorded yet.')
|
|
386
|
-
const icon = entry => entry.status === 'completed' ? '✅' : entry.status === 'failed' ? '⚠️' : '👀'
|
|
387
|
-
return h('ul', { style: { listStyle: 'none', margin: 0, padding: 0 } },
|
|
388
|
-
entries.map(entry => h('li', { key: entry.id, style: { padding: '6px 0', borderBottom: '1px solid var(--dsw-alias-separator-default, #333)', fontSize: 13 } },
|
|
389
|
-
h('span', null, `${icon(entry)} ${entry.projectPath} !${entry.mrIid} · ${entry.mode}${entry.trigger !== 'mention' ? ` · ${entry.trigger}` : ''}`),
|
|
390
|
-
h('div', { style: captionStyle },
|
|
391
|
-
`${new Date(entry.startedAt).toLocaleString()}${entry.summary ? ` — ${entry.summary}` : ''}${entry.error ? ` — ${entry.error}` : ''}`),
|
|
392
|
-
)),
|
|
393
|
-
)
|
|
394
|
-
}
|
|
395
|
-
|
|
396
402
|
/** One selectable LAN address chip + the QR of the currently selected URL. */
|
|
397
403
|
function LanAccess({ proxyStatus, lanPin }) {
|
|
398
404
|
const urls = proxyStatus?.lanUrls ?? []
|
|
@@ -492,6 +498,13 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
|
|
|
492
498
|
const [lanPinEnabled, setLanPinEnabled] = useState(false)
|
|
493
499
|
const [lanPin, setLanPin] = useState(null)
|
|
494
500
|
const [showLanPin, setShowLanPin] = useState(false)
|
|
501
|
+
// Task 3: Guard/Blacklist/Supervisor/Notifier tabs state
|
|
502
|
+
const [activeTab, setActiveTab] = useState('guard')
|
|
503
|
+
const [guard, setGuard] = useState({})
|
|
504
|
+
const [patternsText, setPatternsText] = useState('')
|
|
505
|
+
const [placeholdersText, setPlaceholdersText] = useState('')
|
|
506
|
+
const [supervisorCfg, setSupervisorCfg] = useState({})
|
|
507
|
+
const [notifierCfg, setNotifierCfg] = useState({})
|
|
495
508
|
|
|
496
509
|
const call = async (endpoint, payload) => {
|
|
497
510
|
const res = await rpcCall(endpoint, payload)
|
|
@@ -499,6 +512,60 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
|
|
|
499
512
|
return res.value
|
|
500
513
|
}
|
|
501
514
|
|
|
515
|
+
// Helpers for guard/supervisor/notifier domains via generic config RPC (Task 3)
|
|
516
|
+
const unwrap = (res) => {
|
|
517
|
+
if (res && typeof res === 'object' && 'ok' in res) {
|
|
518
|
+
if (res.ok) return res.value
|
|
519
|
+
throw new Error(res.error?.message ?? 'RPC failed')
|
|
520
|
+
}
|
|
521
|
+
return res
|
|
522
|
+
}
|
|
523
|
+
const cfgGet = async (domain) => {
|
|
524
|
+
if (!configRpcCall) throw new Error('config RPC not available')
|
|
525
|
+
const res = await configRpcCall('get', { domain })
|
|
526
|
+
return unwrap(res)
|
|
527
|
+
}
|
|
528
|
+
const cfgSet = async (domain, patch) => {
|
|
529
|
+
if (!configRpcCall) throw new Error('config RPC not available')
|
|
530
|
+
const res = await configRpcCall('set', { domain, patch })
|
|
531
|
+
return unwrap(res)
|
|
532
|
+
}
|
|
533
|
+
const saveGuard = async (patch) => {
|
|
534
|
+
setError(null)
|
|
535
|
+
const next = { ...guard, ...patch }
|
|
536
|
+
if (patch.gitProtection && guard.gitProtection) next.gitProtection = { ...guard.gitProtection, ...patch.gitProtection }
|
|
537
|
+
setGuard(next)
|
|
538
|
+
try { await cfgSet('guard', patch) } catch (e) { setError(e.message ?? String(e)) }
|
|
539
|
+
}
|
|
540
|
+
const commitBlacklistPatterns = async (text) => {
|
|
541
|
+
const patterns = text.split('\n').map(s => s.trim()).filter(Boolean)
|
|
542
|
+
setError(null)
|
|
543
|
+
try { await cfgSet('guardBlacklist', { patterns }) } catch (e) { setError(e.message ?? String(e)) }
|
|
544
|
+
}
|
|
545
|
+
const commitPlaceholders = async () => {
|
|
546
|
+
setError(null)
|
|
547
|
+
let obj = {}
|
|
548
|
+
try { obj = placeholdersText.trim() ? JSON.parse(placeholdersText) : {}; if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) throw new Error('placeholders must be JSON object') } catch (e) { setError(`placeholders JSON invalid: ${e.message ?? String(e)}`); return }
|
|
549
|
+
try { await cfgSet('guardBlacklist', { placeholders: obj }) } catch (e) { setError(e.message ?? String(e)) }
|
|
550
|
+
}
|
|
551
|
+
const saveSupervisorCfg = async (patch) => {
|
|
552
|
+
setError(null)
|
|
553
|
+
setSupervisorCfg(prev => ({ ...prev, ...patch }))
|
|
554
|
+
try { await cfgSet('supervisor', patch) } catch (e) { setError(e.message ?? String(e)) }
|
|
555
|
+
}
|
|
556
|
+
const saveNotifierCfg = async (patch) => {
|
|
557
|
+
setError(null)
|
|
558
|
+
setNotifierCfg(prev => {
|
|
559
|
+
const next = { ...prev }
|
|
560
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
561
|
+
if (k === 'telegram' && typeof v === 'object' && v !== null) next.telegram = { ...(prev.telegram ?? {}), ...v }
|
|
562
|
+
else next[k] = v
|
|
563
|
+
}
|
|
564
|
+
return next
|
|
565
|
+
})
|
|
566
|
+
try { await cfgSet('notifier', patch) } catch (e) { setError(e.message ?? String(e)) }
|
|
567
|
+
}
|
|
568
|
+
|
|
502
569
|
const refresh = async () => {
|
|
503
570
|
try { setStatus(await call(MAESTRO_ENDPOINTS.status, {})) } catch { /* transient failure, ignore */ }
|
|
504
571
|
try { setProxyStatus(await call(MAESTRO_ENDPOINTS.proxyStatus, {})) } catch { /* proxy row may be starting */ }
|
|
@@ -520,6 +587,21 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
|
|
|
520
587
|
}
|
|
521
588
|
})
|
|
522
589
|
.catch(() => { /* supervisor domain not yet set or config service unavailable */ })
|
|
590
|
+
// Task 3: load Guard/Blacklist/Supervisor/Notifier domains
|
|
591
|
+
Promise.all([
|
|
592
|
+
cfgGet('guard').catch(() => ({})),
|
|
593
|
+
cfgGet('guardBlacklist').catch(() => ({ patterns: [], placeholders: {} })),
|
|
594
|
+
cfgGet('supervisor').catch(() => ({})),
|
|
595
|
+
cfgGet('notifier').catch(() => ({})),
|
|
596
|
+
]).then(([g, bl, sup, not]) => {
|
|
597
|
+
setGuard(g ?? {})
|
|
598
|
+
const pats = Array.isArray(bl?.patterns) ? bl.patterns : []
|
|
599
|
+
const ph = bl?.placeholders && typeof bl.placeholders === 'object' ? bl.placeholders : {}
|
|
600
|
+
setPatternsText(pats.join('\n'))
|
|
601
|
+
setPlaceholdersText(JSON.stringify(ph, null, 2))
|
|
602
|
+
setSupervisorCfg(sup ?? {})
|
|
603
|
+
setNotifierCfg(not ?? {})
|
|
604
|
+
}).catch(() => {})
|
|
523
605
|
}
|
|
524
606
|
call(MAESTRO_ENDPOINTS.lanPinStatus, {})
|
|
525
607
|
.then(value => { setLanPinEnabled(value.enabled); if (value.enabled) setLanPin(value.pin ?? null) })
|
|
@@ -720,7 +802,6 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
|
|
|
720
802
|
checked: config.autoRereviewOnPush,
|
|
721
803
|
onChange: checked => saveField('autoRereviewOnPush', checked),
|
|
722
804
|
}),
|
|
723
|
-
h(ReviewHistoryPanel, { rpcCall }),
|
|
724
805
|
),
|
|
725
806
|
|
|
726
807
|
h('div', { style: sectionStyle },
|
|
@@ -754,6 +835,129 @@ export function MaestroSettingsTab({ rpcCall, configRpcCall }) {
|
|
|
754
835
|
h(ProjectMappingsEditor, { mappings: config.projectMappings ?? [], onChange: mappings => saveField('projectMappings', mappings), catalog, globalReviewModel: config.reviewModel ?? null }),
|
|
755
836
|
),
|
|
756
837
|
|
|
838
|
+
// Task 3: Guard/Blacklist/Supervisor/Notifier tabs — data-driven over guard domains
|
|
839
|
+
h('div', { style: sectionStyle },
|
|
840
|
+
h('h4', { style: headingStyle }, 'Guard / Blacklist / Supervisor / Notifier'),
|
|
841
|
+
h('div', { style: tabBarStyle },
|
|
842
|
+
h('button', { type: 'button', style: tabButtonStyle(activeTab === 'guard'), onClick: () => setActiveTab('guard') }, 'Guard'),
|
|
843
|
+
h('button', { type: 'button', style: tabButtonStyle(activeTab === 'blacklist'), onClick: () => setActiveTab('blacklist') }, 'Blacklist'),
|
|
844
|
+
h('button', { type: 'button', style: tabButtonStyle(activeTab === 'supervisor'), onClick: () => setActiveTab('supervisor') }, 'Supervisor'),
|
|
845
|
+
h('button', { type: 'button', style: tabButtonStyle(activeTab === 'notifier'), onClick: () => setActiveTab('notifier') }, 'Notifier'),
|
|
846
|
+
),
|
|
847
|
+
activeTab === 'guard' && h('div', { 'data-tab': 'guard' },
|
|
848
|
+
h('p', { style: captionStyle }, 'Enforce publish block, git protection and cwd containment.'),
|
|
849
|
+
h(ToggleField, {
|
|
850
|
+
label: 'publishBlocked',
|
|
851
|
+
caption: 'Block publish-related commands when enabled.',
|
|
852
|
+
checked: guard.publishBlocked === true,
|
|
853
|
+
onChange: v => saveGuard({ publishBlocked: v }),
|
|
854
|
+
}),
|
|
855
|
+
h(ToggleField, {
|
|
856
|
+
label: 'gitProtection.enabled',
|
|
857
|
+
caption: 'Protect pushes to protected branches.',
|
|
858
|
+
checked: guard.gitProtection?.enabled === true,
|
|
859
|
+
onChange: v => saveGuard({ gitProtection: { enabled: v, branches: guard.gitProtection?.branches ?? ['master', 'main'] } }),
|
|
860
|
+
}),
|
|
861
|
+
h('label', { style: fieldLabelStyle }, 'gitProtection.branches (comma separated)'),
|
|
862
|
+
h('input', {
|
|
863
|
+
style: inputStyle,
|
|
864
|
+
value: (guard.gitProtection?.branches ?? ['master', 'main']).join(', '),
|
|
865
|
+
placeholder: 'master, main',
|
|
866
|
+
onChange: e => {
|
|
867
|
+
const branches = e.target.value.split(',').map(s => s.trim()).filter(Boolean)
|
|
868
|
+
saveGuard({ gitProtection: { enabled: guard.gitProtection?.enabled ?? true, branches } })
|
|
869
|
+
},
|
|
870
|
+
}),
|
|
871
|
+
h(ToggleField, {
|
|
872
|
+
label: 'cwdContainment',
|
|
873
|
+
caption: 'Contain file operations to the session cwd.',
|
|
874
|
+
checked: guard.cwdContainment === true,
|
|
875
|
+
onChange: v => saveGuard({ cwdContainment: v }),
|
|
876
|
+
}),
|
|
877
|
+
h('label', { style: fieldLabelStyle }, 'credentialPaths (comma separated)'),
|
|
878
|
+
h('input', {
|
|
879
|
+
style: inputStyle,
|
|
880
|
+
value: (guard.credentialPaths ?? []).join(', '),
|
|
881
|
+
placeholder: '~/.config/credentials.yaml, ~/.config/cloudflared',
|
|
882
|
+
onChange: e => {
|
|
883
|
+
const credentialPaths = e.target.value.split(',').map(s => s.trim()).filter(Boolean)
|
|
884
|
+
saveGuard({ credentialPaths })
|
|
885
|
+
},
|
|
886
|
+
}),
|
|
887
|
+
),
|
|
888
|
+
activeTab === 'blacklist' && h('div', { 'data-tab': 'blacklist' },
|
|
889
|
+
h('p', { style: captionStyle }, 'One pattern per line. These are blocked from being committed or published.'),
|
|
890
|
+
h('label', { style: fieldLabelStyle }, 'patterns (one per line)'),
|
|
891
|
+
h('textarea', {
|
|
892
|
+
style: textareaStyle,
|
|
893
|
+
value: patternsText,
|
|
894
|
+
placeholder: 'example-project\nacme-shop',
|
|
895
|
+
onChange: e => setPatternsText(e.target.value),
|
|
896
|
+
onBlur: e => commitBlacklistPatterns(e.target.value),
|
|
897
|
+
}),
|
|
898
|
+
h('label', { style: fieldLabelStyle }, 'placeholders JSON'),
|
|
899
|
+
h('textarea', {
|
|
900
|
+
style: { ...textareaStyle, height: 90 },
|
|
901
|
+
value: placeholdersText,
|
|
902
|
+
placeholder: '{"example-project":"my-project"}',
|
|
903
|
+
onChange: e => setPlaceholdersText(e.target.value),
|
|
904
|
+
onBlur: () => commitPlaceholders(),
|
|
905
|
+
}),
|
|
906
|
+
h('p', { style: captionStyle }, 'Map blocked patterns to their placeholder suggestions.'),
|
|
907
|
+
h('button', { type: 'button', style: { ...secondaryButtonStyle, marginTop: 8 }, onClick: () => { commitBlacklistPatterns(patternsText); commitPlaceholders() } }, 'Save Blacklist'),
|
|
908
|
+
),
|
|
909
|
+
activeTab === 'supervisor' && h('div', { 'data-tab': 'supervisor' },
|
|
910
|
+
h('p', { style: captionStyle }, 'Background daemon that auto-resumes crashed sessions.'),
|
|
911
|
+
h('label', { style: fieldLabelStyle }, 'intervalMs'),
|
|
912
|
+
h('input', {
|
|
913
|
+
type: 'number',
|
|
914
|
+
style: inputStyle,
|
|
915
|
+
value: supervisorCfg.intervalMs ?? '',
|
|
916
|
+
placeholder: '5000',
|
|
917
|
+
onChange: e => { const v = e.target.value === '' ? undefined : Number(e.target.value); saveSupervisorCfg({ intervalMs: v }) },
|
|
918
|
+
}),
|
|
919
|
+
h('label', { style: fieldLabelStyle }, 'downThreshold'),
|
|
920
|
+
h('input', {
|
|
921
|
+
type: 'number',
|
|
922
|
+
style: inputStyle,
|
|
923
|
+
value: supervisorCfg.downThreshold ?? '',
|
|
924
|
+
placeholder: '3',
|
|
925
|
+
onChange: e => { const v = e.target.value === '' ? undefined : Number(e.target.value); saveSupervisorCfg({ downThreshold: v }) },
|
|
926
|
+
}),
|
|
927
|
+
h(ToggleField, {
|
|
928
|
+
label: 'autoResumeEnabled',
|
|
929
|
+
caption: 'Automatically resume down sessions.',
|
|
930
|
+
checked: supervisorCfg.autoResumeEnabled === true,
|
|
931
|
+
onChange: v => saveSupervisorCfg({ autoResumeEnabled: v }),
|
|
932
|
+
}),
|
|
933
|
+
),
|
|
934
|
+
activeTab === 'notifier' && h('div', { 'data-tab': 'notifier' },
|
|
935
|
+
h('p', { style: captionStyle }, 'Telegram notifications for Maestro events.'),
|
|
936
|
+
h('label', { style: fieldLabelStyle }, 'telegram.botToken'),
|
|
937
|
+
h('input', {
|
|
938
|
+
type: 'password',
|
|
939
|
+
autoComplete: 'off',
|
|
940
|
+
style: inputStyle,
|
|
941
|
+
value: notifierCfg.telegram?.botToken ?? '',
|
|
942
|
+
placeholder: '123456:ABC-DEF...',
|
|
943
|
+
onChange: e => saveNotifierCfg({ telegram: { botToken: e.target.value } }),
|
|
944
|
+
}),
|
|
945
|
+
h('label', { style: fieldLabelStyle }, 'telegram.chatId'),
|
|
946
|
+
h('input', {
|
|
947
|
+
style: inputStyle,
|
|
948
|
+
value: notifierCfg.telegram?.chatId ?? '',
|
|
949
|
+
placeholder: '-1001234567890',
|
|
950
|
+
onChange: e => saveNotifierCfg({ telegram: { chatId: e.target.value } }),
|
|
951
|
+
}),
|
|
952
|
+
h(ToggleField, {
|
|
953
|
+
label: 'telegram.reviewNotifications',
|
|
954
|
+
caption: 'Also notify about finished reviews.',
|
|
955
|
+
checked: notifierCfg.telegram?.reviewNotifications === true || notifierCfg.policy?.reviewNotifications === true,
|
|
956
|
+
onChange: v => saveNotifierCfg({ telegram: { reviewNotifications: v } }),
|
|
957
|
+
}),
|
|
958
|
+
),
|
|
959
|
+
),
|
|
960
|
+
|
|
757
961
|
error && h('p', { style: errorStyle }, error),
|
|
758
962
|
)
|
|
759
963
|
}
|
package/src/host/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {} from '@deepseek-ai/dsh-client-connection'
|
|
2
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
type RpcResult<T> = { ok: true; value: T } | { ok: false; error: { code: string; message: string; details: object } }
|
|
4
|
+
type RpcErrorDetailsMap = { 'bad-request': { issues: object[] } }
|
|
4
5
|
import { createMaestroConfigService, type MaestroConfigService } from './service.ts'
|
|
5
6
|
|
|
6
7
|
export const name = 'maestro-config'
|
|
@@ -11,6 +12,7 @@ const RPC_CHANNEL = '/dsh-maestro-config'
|
|
|
11
12
|
declare module '@deepseek-ai/cordis' {
|
|
12
13
|
interface Context {
|
|
13
14
|
maestroConfig: MaestroConfigService
|
|
15
|
+
connection: { rpc: { handle: (channel: string, handler: (endpoint: string, payload: unknown) => Promise<RpcResult<unknown>>, opts?: unknown) => () => void } }
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -31,7 +33,13 @@ function fail(message: string): RpcResult<never> {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Publish maestroConfig over the shared store + loopback RPC for clients.
|
|
38
|
+
* Exposes guard/guardBlacklist/supervisor/notifier domains (Task 1 validators)
|
|
39
|
+
* via generic get/set — validation is delegated to the lib's domain validators.
|
|
40
|
+
* Host also handles '/dsh-maestro-config/get' and '/dsh-maestro-config/set'
|
|
41
|
+
* style calls through the single channel with endpoint dispatch.
|
|
42
|
+
*/
|
|
35
43
|
export function apply(ctx: Context): void {
|
|
36
44
|
const svc = createMaestroConfigService()
|
|
37
45
|
ctx.provide('maestroConfig', svc)
|
|
@@ -43,6 +51,7 @@ export function apply(ctx: Context): void {
|
|
|
43
51
|
}
|
|
44
52
|
if (endpoint === 'get') {
|
|
45
53
|
if (typeof body.domain !== 'string') return fail('domain (string) is required')
|
|
54
|
+
// guard / guardBlacklist / supervisor / notifier are all valid domains here
|
|
46
55
|
return ok(await svc.get(body.domain))
|
|
47
56
|
}
|
|
48
57
|
if (endpoint === 'set') {
|