@ddtcorex/dsh-maestro-review 0.10.0 → 0.11.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/README.md +10 -8
- package/cordis.patch.yml +4 -4
- package/lib/client.js +484 -0
- package/lib/config-store.d.ts +1 -1
- package/lib/config-store.d.ts.map +1 -1
- package/lib/config-store.js +43 -3
- package/lib/config-store.js.map +1 -1
- package/lib/govard-lint-detection.d.ts +20 -0
- package/lib/govard-lint-detection.d.ts.map +1 -0
- package/lib/govard-lint-detection.js +48 -0
- package/lib/govard-lint-detection.js.map +1 -0
- package/lib/orchestrator.d.ts +45 -9
- package/lib/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator.js +79 -22
- package/lib/orchestrator.js.map +1 -1
- package/lib/search-tool.d.ts.map +1 -1
- package/lib/search-tool.js.map +1 -1
- package/lib/settings-rpc.d.ts +13 -11
- package/lib/settings-rpc.d.ts.map +1 -1
- package/lib/settings-rpc.js +45 -142
- package/lib/settings-rpc.js.map +1 -1
- package/lib/types/client/BrandMark.d.ts +22 -0
- package/lib/types/client/BrandMark.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +12 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/maestro-mark.d.ts +20 -0
- package/lib/types/client/maestro-mark.d.ts.map +1 -0
- package/lib/types/client/review/ReviewSettings.d.ts +15 -0
- package/lib/types/client/review/ReviewSettings.d.ts.map +1 -0
- package/lib/types/client/review/styles.d.ts +11 -0
- package/lib/types/client/review/styles.d.ts.map +1 -0
- package/lib/types/client/settings-nav-icon.d.ts +20 -0
- package/lib/types/client/settings-nav-icon.d.ts.map +1 -0
- package/lib/vendor/store.d.ts +60 -0
- package/lib/vendor/store.d.ts.map +1 -0
- package/lib/vendor/store.js +500 -0
- package/lib/vendor/store.js.map +1 -0
- package/lib/webhook-secret.d.ts +31 -0
- package/lib/webhook-secret.d.ts.map +1 -0
- package/lib/webhook-secret.js +38 -0
- package/lib/webhook-secret.js.map +1 -0
- package/package.json +24 -6
- package/presets/maestro-auditor.patch.yml +7 -4
- package/presets/maestro-coder/agent.cordis.yml +3 -11
- package/presets/maestro-coder/preset.yml +1 -1
- package/profiles/reviewer-ci/cordis.patch.yml +5 -4
- package/profiles/reviewer-ci/package.json +3 -3
- package/profiles/reviewer-ci/pnpm-lock.yaml +3524 -3239
- package/skills/dsh-native-tools/SKILL.md +5 -5
- package/src/client/BrandMark.tsx +42 -0
- package/src/client/index.tsx +109 -0
- package/src/client/maestro-mark.ts +21 -0
- package/src/client/review/ReviewSettings.tsx +236 -0
- package/src/client/review/styles.ts +108 -0
- package/src/client/settings-nav-icon.ts +68 -0
- package/src/host/config-store.ts +42 -3
- package/src/host/govard-lint-detection.ts +47 -0
- package/src/host/orchestrator.ts +95 -23
- package/src/host/search-tool.ts +3 -4
- package/src/host/settings-rpc.ts +48 -145
- package/src/host/vendor/store.ts +531 -0
- package/src/host/webhook-secret.ts +43 -0
- package/lib/govard-audit-lint-tool.d.ts +0 -95
- package/lib/govard-audit-lint-tool.d.ts.map +0 -1
- package/lib/govard-audit-lint-tool.js +0 -286
- package/lib/govard-audit-lint-tool.js.map +0 -1
- package/lib/govard-tool.d.ts +0 -11
- package/lib/govard-tool.d.ts.map +0 -1
- package/lib/govard-tool.js +0 -95
- package/lib/govard-tool.js.map +0 -1
- package/lib/workspace-tool.d.ts +0 -10
- package/lib/workspace-tool.d.ts.map +0 -1
- package/lib/workspace-tool.js +0 -92
- package/lib/workspace-tool.js.map +0 -1
- package/src/host/govard-audit-lint-tool.ts +0 -286
- package/src/host/govard-tool.ts +0 -114
- package/src/host/workspace-tool.ts +0 -108
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dsh-native-tools
|
|
3
|
-
description: Use when running a Maestro skill inside DeepSeek Harness and its text says "native ... when provided" — this maps each such capability (query-log stats, lint, deploy preflight, layout extraction, theme inspection, review scope split, escape scan, module check, MR diff) to the tool
|
|
3
|
+
description: Use when running a Maestro skill inside DeepSeek Harness and its text says "native ... when provided" — this maps each such capability (query-log stats, lint, deploy preflight, layout extraction, theme inspection, review scope split, escape scan, module check, MR diff) to the DSH tool or the govard CLI command, run with bash, that provides it
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# DSH Native Tools
|
|
@@ -47,15 +47,15 @@ Left column is the phrase the public skill uses; right column is what to call.
|
|
|
47
47
|
| Public skill says | Tool and signature |
|
|
48
48
|
|---|---|
|
|
49
49
|
| native query-log stats tool | `maestro_perf_log_stats {topN?, repeatThreshold?, timeThresholdMs?}` → streaming and bounded; never hand-grep a 16–50k line log or open it as a spreadsheet |
|
|
50
|
-
| native lint tool | `
|
|
51
|
-
| native deploy preflight | `
|
|
50
|
+
| native lint tool | `govard audit run --checks lint --format json --mode auto --timeout auto --lint-provider govard --scope diff --base <sha>` run with `bash` in the review worktree (`--scope project` drops `--base`); `govard audit run --checks integrity --format json` is the container-free analysis. Read the JSON; exit `0` is clean, `1` findings, `3` a missing capability. A missing `govard` binary or Docker is not a finding: report lint as unavailable |
|
|
51
|
+
| native deploy preflight | `govard deploy plan <remote>` (add `--json` for machine-readable) prints the pipeline without connecting; `govard deploy check <remote>` preflights connectivity and the release layout. Both run with `bash`, `remote` is a remote from `.govard.yml`, and running the deploy itself stays in the terminal |
|
|
52
52
|
| native layout extraction | `layout_xml_extract {changedFiles:<MR layout files>}` → handles/blocks/moves + templateExists/parseError |
|
|
53
53
|
| native theme inspection | `hyva_theme_inspect {classes:["<class-from-diff>"]}` → `{themes[],tailwind:{major,...},hyvaPackages[]}`; a null field means downgrade to a question |
|
|
54
54
|
| native review scope split | `maestro_review_scope_split {diffStats:{files,addedLinesPerFile}, mode}` → `{split:{quick,deep},reason,estimatedSavingsTokens}`; run quick checks only on quick files |
|
|
55
55
|
| native escape scan | `phtml_escape_scan {scope:"diff", paths:<changed phtml>}` → `{findings[],scannedFiles,truncated}` with confidence + M2-SEC-xxx; `hyva_csp_scan {maxFiles?}` covers CSP work and truncates at `maxFiles` with no `truncated` flag — size the input yourself |
|
|
56
56
|
| native module check | `magento_module_check {modulePath:"app/code/Vendor/Module"}` → `{modules[],scannedModules,truncated}`; always check `scannedModules` against what you expected |
|
|
57
57
|
| native MR diff | `gitlab_get_mr_diff`, `gitlab_list_own_review_threads`, `gitlab_post_inline_comment` — diff plus own threads plus inline comments in one call |
|
|
58
|
-
| local
|
|
58
|
+
| local environment inside a review worktree | `govard env up` brings it up, `govard shell -c "<command>"` runs one command in the container, `govard env down -v` tears it down (always, even after a failure). All three run with `bash`; the worktree carries its own project name so it never collides with the primary checkout |
|
|
59
59
|
|
|
60
60
|
The `maestro_get_skills` tool searches this same skill catalogue by keyword and
|
|
61
61
|
returns full content — useful when a review profile has not preloaded a skill.
|
|
@@ -75,7 +75,7 @@ returns full content — useful when a review profile has not preloaded a skill.
|
|
|
75
75
|
## Maintenance
|
|
76
76
|
|
|
77
77
|
- Public skills stay tool-neutral. Never copy a tool name back into
|
|
78
|
-
`maestro-skills`: its catalog test rejects `maestro_*`, `
|
|
78
|
+
`maestro-skills`: its catalog test rejects `maestro_*`, `DSH`
|
|
79
79
|
and `DeepSeek Harness` anywhere in skill content, `compatibility: dsh` frontmatter
|
|
80
80
|
excepted.
|
|
81
81
|
- Renaming a tool in `src/host` must fail `tests/dsh-native-tools-skill.test.ts`, so
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Maestro M badge this package's settings header draws.
|
|
3
|
+
*
|
|
4
|
+
* A copy of `dsh-maestro-core/src/client/config/components/BrandMark.tsx`, for
|
|
5
|
+
* the structural reason its own header explains: one plugin's client bundle
|
|
6
|
+
* cannot import another's. The glyph and tile come from `./maestro-mark.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createElement as h } from 'react'
|
|
10
|
+
import {
|
|
11
|
+
MAESTRO_BRAND_TILE,
|
|
12
|
+
MAESTRO_MARK_PATH,
|
|
13
|
+
MAESTRO_MARK_STROKE_WIDTH,
|
|
14
|
+
MAESTRO_MARK_VIEWBOX,
|
|
15
|
+
} from './maestro-mark.js'
|
|
16
|
+
|
|
17
|
+
export function MaestroMark(props: { size?: number }) {
|
|
18
|
+
const s = props.size ?? 16
|
|
19
|
+
return h('svg', { width: s, height: s, viewBox: MAESTRO_MARK_VIEWBOX, fill: 'none', 'aria-hidden': 'true' } as any,
|
|
20
|
+
h('path', { d: MAESTRO_MARK_PATH, stroke: 'currentColor', strokeWidth: MAESTRO_MARK_STROKE_WIDTH, strokeLinecap: 'round', strokeLinejoin: 'round' } as any),
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The 28px brand tile. Its colour, border, shadow and radius are the brand and
|
|
26
|
+
* are not negotiable — a token-only background resolves to near-white on some
|
|
27
|
+
* dark themes, which is why the fixed tile exists.
|
|
28
|
+
*/
|
|
29
|
+
export function BrandBadge(props: { size?: number; outer?: number; radius?: number; style?: Record<string, unknown> }) {
|
|
30
|
+
const outer = props.outer ?? 28
|
|
31
|
+
const size = props.size ?? 16
|
|
32
|
+
const radius = props.radius ?? 8
|
|
33
|
+
return h('span', {
|
|
34
|
+
'data-maestro-logo': '',
|
|
35
|
+
style: {
|
|
36
|
+
width: outer, height: outer, borderRadius: radius, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
37
|
+
background: `var(--dsw-alias-brand-primary, ${MAESTRO_BRAND_TILE})`, backgroundColor: MAESTRO_BRAND_TILE, color: '#fff', flex: 'none',
|
|
38
|
+
border: '1px solid rgba(0,0,0,0.08)', boxShadow: '0 0 0 1px var(--dsw-alias-border-l1)', boxSizing: 'border-box' as any,
|
|
39
|
+
...props.style,
|
|
40
|
+
},
|
|
41
|
+
} as any, h(MaestroMark as any, { size }))
|
|
42
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-maestro-review — client bundle entry.
|
|
3
|
+
*
|
|
4
|
+
* Registers `settings.section` id `maestro-review` (order 32): the GitLab and
|
|
5
|
+
* Review keys this package owns.
|
|
6
|
+
*
|
|
7
|
+
* Every field here is a key review still saves. The tunnel, proxy, PIN and
|
|
8
|
+
* telegram fields left with their owners in wave 2, and the Secrets tab that
|
|
9
|
+
* used to render the Telegram token went with them: a plugin that cannot save
|
|
10
|
+
* a value has no business rendering its field.
|
|
11
|
+
*/
|
|
12
|
+
import * as React from 'react'
|
|
13
|
+
import { ReviewSettings } from './review/ReviewSettings.js'
|
|
14
|
+
import { REVIEW_CSS } from './review/styles.js'
|
|
15
|
+
import { registerSettingsNavIcon, SETTINGS_NAV_MARKER } from './settings-nav-icon.js'
|
|
16
|
+
|
|
17
|
+
/** This package's own channel; the endpoints it serves. */
|
|
18
|
+
export const REVIEW_CHANNEL = '/dsh-maestro-review'
|
|
19
|
+
|
|
20
|
+
export type RpcCall = (endpoint: string, payload?: unknown) => Promise<any>
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Unwrap the `RpcResult` envelope. A bare `{ ok: true }` is not a usable
|
|
24
|
+
* answer: the value is what the form renders, so an endpoint that forgets it
|
|
25
|
+
* produces an empty card that saves nothing and reports no error.
|
|
26
|
+
*/
|
|
27
|
+
function unwrap(res: any): any {
|
|
28
|
+
return res && typeof res === 'object' && 'ok' in res ? (res.ok ? res.value : null) : res
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function makeRpcCall(ctx: any): RpcCall {
|
|
32
|
+
return async (endpoint, payload) => {
|
|
33
|
+
const conn = ctx.get?.('connection')
|
|
34
|
+
if (conn?.rpc?.call === undefined) throw new Error('RPC not available')
|
|
35
|
+
const res = await conn.rpc.call(REVIEW_CHANNEL, endpoint, payload ?? {})
|
|
36
|
+
const value = unwrap(res)
|
|
37
|
+
if (value === null) {
|
|
38
|
+
throw new Error(String(res?.error?.message ?? res?.error ?? endpoint))
|
|
39
|
+
}
|
|
40
|
+
return value
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const REVIEW_NAV_CSS = `
|
|
45
|
+
[${SETTINGS_NAV_MARKER}] > svg:first-child,
|
|
46
|
+
[${SETTINGS_NAV_MARKER}] > svg.zWKi1a_navIcon {
|
|
47
|
+
display: none !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[${SETTINGS_NAV_MARKER}]::before {
|
|
51
|
+
content: '';
|
|
52
|
+
flex: none;
|
|
53
|
+
width: 16px;
|
|
54
|
+
height: 16px;
|
|
55
|
+
display: inline-block;
|
|
56
|
+
background: currentColor;
|
|
57
|
+
-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.6' 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;
|
|
58
|
+
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.6' 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;
|
|
59
|
+
}
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
function installStyleTag(css: string, pluginCss: string): () => void {
|
|
63
|
+
if (typeof document === 'undefined') return () => {}
|
|
64
|
+
const tag = document.createElement('style')
|
|
65
|
+
tag.dataset.plugin = '@ddtcorex/dsh-maestro-review'
|
|
66
|
+
tag.dataset.pluginCss = pluginCss
|
|
67
|
+
tag.textContent = css
|
|
68
|
+
document.head.appendChild(tag)
|
|
69
|
+
return () => {
|
|
70
|
+
document.querySelector(`style[data-plugin-css="${pluginCss}"]`)?.remove()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const inject = ['slots', 'connection'] as const
|
|
75
|
+
|
|
76
|
+
export function apply(ctx: any): void {
|
|
77
|
+
const slots = ctx.get?.('slots')
|
|
78
|
+
if (!slots?.inject || !slots?.register) return
|
|
79
|
+
|
|
80
|
+
const rpcCall = makeRpcCall(ctx)
|
|
81
|
+
|
|
82
|
+
ctx.effect(() => registerSettingsNavIcon(() => 'Maestro Review'), 'maestro-review: settings nav icon')
|
|
83
|
+
ctx.effect(() => installStyleTag(REVIEW_NAV_CSS, 'maestro-review/settings-nav.css'), 'maestro-review: settings nav css')
|
|
84
|
+
ctx.effect(() => installStyleTag(REVIEW_CSS, 'maestro-review/settings.css'), 'maestro-review: settings css')
|
|
85
|
+
|
|
86
|
+
ctx.effect(() => {
|
|
87
|
+
const dispose = slots.inject('settings.section', () =>
|
|
88
|
+
slots.register(
|
|
89
|
+
{
|
|
90
|
+
name: 'settings.section',
|
|
91
|
+
id: 'maestro-review',
|
|
92
|
+
order: 32,
|
|
93
|
+
label: () => 'Maestro Review',
|
|
94
|
+
inject: () => ({ rpcCall }),
|
|
95
|
+
},
|
|
96
|
+
(props: { rpcCall: RpcCall }) => React.createElement(ReviewSettings, props),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
return () => {
|
|
100
|
+
try {
|
|
101
|
+
;(dispose as any)?.()
|
|
102
|
+
} catch {
|
|
103
|
+
// Teardown must never throw.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, 'maestro-review: settings')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default { inject, apply }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro M logo — the react-free half of this package's copy.
|
|
3
|
+
*
|
|
4
|
+
* This is a verbatim copy of
|
|
5
|
+
* `dsh-maestro-core/src/client/config/maestro-mark.ts`, which is the source of
|
|
6
|
+
* record for the house pattern. The duplication is structural, not accidental:
|
|
7
|
+
* every Maestro package declares its own `dsh.client` entry and builds its own
|
|
8
|
+
* `lib/client.js`, so one plugin's client module cannot import another's.
|
|
9
|
+
*
|
|
10
|
+
* The react half lives in `BrandMark.tsx` and cannot be imported from vitest at
|
|
11
|
+
* all — `react` is a client-bundler EXTERNAL with no entry in this package's
|
|
12
|
+
* `package.json`. Splitting the glyph declaration out is what lets a test assert
|
|
13
|
+
* these exact bytes instead of grepping source text.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const MAESTRO_MARK_PATH = 'M2 11 L5 4 L8 9 L11 4 L14 11'
|
|
17
|
+
export const MAESTRO_MARK_VIEWBOX = '0 0 16 16'
|
|
18
|
+
export const MAESTRO_MARK_STROKE_WIDTH = 1.6
|
|
19
|
+
|
|
20
|
+
/** Badge tile colour — part of the house pattern documented in AGENTS.md. */
|
|
21
|
+
export const MAESTRO_BRAND_TILE = '#0A84FF'
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GitLab and Review keys `dsh-maestro-review` owns, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* Secrets follow a three-state rule and are never rendered back: the server
|
|
5
|
+
* answers `hasGitlabToken` / `hasWebhookSecret` instead of the value, a blank
|
|
6
|
+
* field keeps what is stored, and a filled field replaces it.
|
|
7
|
+
*/
|
|
8
|
+
import * as React from 'react'
|
|
9
|
+
import type { RpcCall } from '../index.js'
|
|
10
|
+
import { BrandBadge } from '../BrandMark.js'
|
|
11
|
+
|
|
12
|
+
interface Cfg {
|
|
13
|
+
gitlabBaseUrl?: string
|
|
14
|
+
botUsername?: string
|
|
15
|
+
webhookPort?: string | number
|
|
16
|
+
autoRereviewOnPush?: boolean
|
|
17
|
+
autoReviewOnAssign?: boolean
|
|
18
|
+
agentTimeoutMs?: number
|
|
19
|
+
reviewSessionRetentionDays?: number
|
|
20
|
+
hasGitlabToken?: boolean
|
|
21
|
+
hasWebhookSecret?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What each setting is called on screen.
|
|
26
|
+
*
|
|
27
|
+
* The page used to label every field with `String(key)`, so it read
|
|
28
|
+
* "gitlabBaseUrl" and "reviewSessionRetentionDays" to anyone who had not
|
|
29
|
+
* opened the store. The key stays as the `data-review-input` value and the
|
|
30
|
+
* wire name; only the visible label changes.
|
|
31
|
+
*/
|
|
32
|
+
const FIELD_LABELS: Partial<Record<keyof Cfg, string>> = {
|
|
33
|
+
gitlabBaseUrl: 'GitLab base URL',
|
|
34
|
+
botUsername: 'Bot username',
|
|
35
|
+
webhookPort: 'Webhook port',
|
|
36
|
+
autoRereviewOnPush: 'Re-review on push',
|
|
37
|
+
autoReviewOnAssign: 'Review on assign',
|
|
38
|
+
agentTimeoutMs: 'Agent timeout (ms)',
|
|
39
|
+
reviewSessionRetentionDays: 'Session retention (days)',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* One setting, in the house pattern's two-column row: label and hint left,
|
|
44
|
+
* control held right. `htmlFor`/`id` are what give the control its accessible
|
|
45
|
+
* name — the previous markup rendered a bare `<label>` beside an input with no
|
|
46
|
+
* `id`, so nothing was associated.
|
|
47
|
+
*/
|
|
48
|
+
function Field(props: { id: string; label: string; hint?: React.ReactNode; children?: React.ReactNode }) {
|
|
49
|
+
return React.createElement(
|
|
50
|
+
'div',
|
|
51
|
+
{ 'data-review-row': '' },
|
|
52
|
+
React.createElement(
|
|
53
|
+
'div',
|
|
54
|
+
{ 'data-review-row-text': '' },
|
|
55
|
+
React.createElement('label', { 'data-review-label': '', htmlFor: props.id }, props.label),
|
|
56
|
+
props.hint ? React.createElement('p', { 'data-review-hint': '' }, props.hint) : null,
|
|
57
|
+
),
|
|
58
|
+
React.createElement('div', { 'data-review-control': '' }, props.children),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function SecretField(props: {
|
|
63
|
+
id: string
|
|
64
|
+
name: string
|
|
65
|
+
label: string
|
|
66
|
+
present: boolean
|
|
67
|
+
value: string
|
|
68
|
+
onChange: (v: string) => void
|
|
69
|
+
onSave: () => void
|
|
70
|
+
disabled: boolean
|
|
71
|
+
}) {
|
|
72
|
+
return Field({
|
|
73
|
+
id: props.id,
|
|
74
|
+
label: props.label,
|
|
75
|
+
hint: props.present ? 'A value is stored. Leave blank to keep it.' : 'Not set.',
|
|
76
|
+
children: React.createElement(
|
|
77
|
+
'div',
|
|
78
|
+
{ 'data-review-secret-group': '' },
|
|
79
|
+
React.createElement('input', {
|
|
80
|
+
id: props.id,
|
|
81
|
+
type: 'password',
|
|
82
|
+
value: props.value,
|
|
83
|
+
placeholder: props.present ? 'stored — type to replace' : 'not set',
|
|
84
|
+
disabled: props.disabled,
|
|
85
|
+
autoComplete: 'new-password',
|
|
86
|
+
'data-review-secret': '',
|
|
87
|
+
onChange: (e: any) => props.onChange(e.target.value),
|
|
88
|
+
}),
|
|
89
|
+
// The save belongs to this field: it stays disabled until the field
|
|
90
|
+
// holds something, and floating it below the row left it orphaned.
|
|
91
|
+
React.createElement('button', {
|
|
92
|
+
type: 'button',
|
|
93
|
+
disabled: props.disabled || props.value === '',
|
|
94
|
+
'data-review-save-secret': props.name,
|
|
95
|
+
onClick: props.onSave,
|
|
96
|
+
}, 'Save'),
|
|
97
|
+
),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function ReviewSettings(props: { rpcCall: RpcCall }) {
|
|
102
|
+
const { rpcCall } = props
|
|
103
|
+
const [cfg, setCfg] = React.useState<Cfg>({})
|
|
104
|
+
const [gitlabToken, setGitlabToken] = React.useState('')
|
|
105
|
+
const [webhookSecret, setWebhookSecret] = React.useState('')
|
|
106
|
+
const [busy, setBusy] = React.useState(false)
|
|
107
|
+
const [notice, setNotice] = React.useState<{ tone: 'ok' | 'bad'; text: string } | null>(null)
|
|
108
|
+
|
|
109
|
+
const fail = React.useCallback((e: unknown) => {
|
|
110
|
+
setNotice({ tone: 'bad', text: e instanceof Error ? e.message : String(e) })
|
|
111
|
+
}, [])
|
|
112
|
+
|
|
113
|
+
const refresh = React.useCallback(async () => {
|
|
114
|
+
setBusy(true)
|
|
115
|
+
try {
|
|
116
|
+
setCfg((await rpcCall('maestro.getConfig')) ?? {})
|
|
117
|
+
} catch (e) {
|
|
118
|
+
fail(e)
|
|
119
|
+
} finally {
|
|
120
|
+
setBusy(false)
|
|
121
|
+
}
|
|
122
|
+
}, [rpcCall, fail])
|
|
123
|
+
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
void refresh()
|
|
126
|
+
}, [refresh])
|
|
127
|
+
|
|
128
|
+
const save = React.useCallback(
|
|
129
|
+
async (patch: Record<string, unknown>) => {
|
|
130
|
+
setBusy(true)
|
|
131
|
+
try {
|
|
132
|
+
await rpcCall('maestro.saveConfig', patch)
|
|
133
|
+
// Blank means keep, so the local field clears only after the save
|
|
134
|
+
// landed; otherwise a failed save would silently drop what was typed.
|
|
135
|
+
if ('gitlabToken' in patch) setGitlabToken('')
|
|
136
|
+
if ('webhookSecret' in patch) setWebhookSecret('')
|
|
137
|
+
setNotice({ tone: 'ok', text: 'Saved.' })
|
|
138
|
+
await refresh()
|
|
139
|
+
} catch (e) {
|
|
140
|
+
fail(e)
|
|
141
|
+
} finally {
|
|
142
|
+
setBusy(false)
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
[rpcCall, refresh, fail],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const text = (key: keyof Cfg, placeholder = '') =>
|
|
149
|
+
Field({
|
|
150
|
+
id: `review-${String(key)}`,
|
|
151
|
+
label: FIELD_LABELS[key] ?? String(key),
|
|
152
|
+
children: React.createElement('input', {
|
|
153
|
+
id: `review-${String(key)}`,
|
|
154
|
+
type: 'text',
|
|
155
|
+
value: (cfg[key] as string | number | undefined) ?? '',
|
|
156
|
+
placeholder,
|
|
157
|
+
disabled: busy,
|
|
158
|
+
'data-review-input': String(key),
|
|
159
|
+
onChange: (e: any) => setCfg((c) => ({ ...c, [key]: e.target.value })),
|
|
160
|
+
onBlur: (e: any) => void save({ [key]: e.target.value }),
|
|
161
|
+
}),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const toggle = (key: keyof Cfg) =>
|
|
165
|
+
Field({
|
|
166
|
+
id: `review-${String(key)}`,
|
|
167
|
+
label: FIELD_LABELS[key] ?? String(key),
|
|
168
|
+
children: React.createElement('input', {
|
|
169
|
+
id: `review-${String(key)}`,
|
|
170
|
+
type: 'checkbox',
|
|
171
|
+
checked: cfg[key] === true,
|
|
172
|
+
disabled: busy,
|
|
173
|
+
'data-review-toggle': String(key),
|
|
174
|
+
onChange: (e: any) => void save({ [key]: e.target.checked }),
|
|
175
|
+
}),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
return React.createElement(
|
|
179
|
+
'div',
|
|
180
|
+
{ 'data-review-root': '' },
|
|
181
|
+
// House header: badge, title, one-line status. The notice lives here so it
|
|
182
|
+
// reports without pushing the rows down.
|
|
183
|
+
React.createElement(
|
|
184
|
+
'div',
|
|
185
|
+
{ 'data-review-header': '' },
|
|
186
|
+
React.createElement(BrandBadge as any, { style: { alignSelf: 'flex-start', marginTop: 2 } }),
|
|
187
|
+
React.createElement(
|
|
188
|
+
'div',
|
|
189
|
+
{ 'data-review-heading': '' },
|
|
190
|
+
React.createElement('h2', { 'data-review-title': '' }, 'GitLab and Review'),
|
|
191
|
+
React.createElement(
|
|
192
|
+
'div',
|
|
193
|
+
{ 'data-review-status': '' },
|
|
194
|
+
notice
|
|
195
|
+
? React.createElement('p', { 'data-review-notice': '', 'data-tone': notice.tone, role: 'status' }, notice.text)
|
|
196
|
+
: React.createElement('span', null, 'GitLab credentials and review triggers.'),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
),
|
|
200
|
+
|
|
201
|
+
React.createElement('div', { 'data-review-actions': '' },
|
|
202
|
+
React.createElement('button', { type: 'button', disabled: busy, onClick: () => void refresh() }, 'Refresh'),
|
|
203
|
+
),
|
|
204
|
+
|
|
205
|
+
text('gitlabBaseUrl', 'https://gitlab.example.com'),
|
|
206
|
+
text('botUsername', 'maestro-bot'),
|
|
207
|
+
|
|
208
|
+
SecretField({
|
|
209
|
+
id: 'review-gitlab-token',
|
|
210
|
+
name: 'gitlabToken',
|
|
211
|
+
label: 'GitLab token',
|
|
212
|
+
present: cfg.hasGitlabToken === true,
|
|
213
|
+
value: gitlabToken,
|
|
214
|
+
disabled: busy,
|
|
215
|
+
onChange: setGitlabToken,
|
|
216
|
+
onSave: () => void save({ gitlabToken }),
|
|
217
|
+
}),
|
|
218
|
+
|
|
219
|
+
SecretField({
|
|
220
|
+
id: 'review-webhook-secret',
|
|
221
|
+
name: 'webhookSecret',
|
|
222
|
+
label: 'Webhook secret',
|
|
223
|
+
present: cfg.hasWebhookSecret === true,
|
|
224
|
+
value: webhookSecret,
|
|
225
|
+
disabled: busy,
|
|
226
|
+
onChange: setWebhookSecret,
|
|
227
|
+
onSave: () => void save({ webhookSecret }),
|
|
228
|
+
}),
|
|
229
|
+
|
|
230
|
+
text('webhookPort', '3081'),
|
|
231
|
+
toggle('autoRereviewOnPush'),
|
|
232
|
+
toggle('autoReviewOnAssign'),
|
|
233
|
+
text('agentTimeoutMs', '600000'),
|
|
234
|
+
text('reviewSessionRetentionDays', '30'),
|
|
235
|
+
)
|
|
236
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-native styling for the review settings section. Every colour and
|
|
3
|
+
* radius resolves through a `--dsw-*` alias, so the section follows the shell
|
|
4
|
+
* theme instead of carrying a palette of its own.
|
|
5
|
+
*
|
|
6
|
+
* The iOS block holds text fields at 16px on coarse pointers: iOS WebKit
|
|
7
|
+
* magnifies the viewport for a focused field below that size, and a magnified
|
|
8
|
+
* sheet never blurs back.
|
|
9
|
+
*/
|
|
10
|
+
export const REVIEW_CSS = `
|
|
11
|
+
[data-review-root], [data-review-root] * { box-sizing: border-box; }
|
|
12
|
+
[data-review-root] {
|
|
13
|
+
display: flex; flex-direction: column; gap: 12px;
|
|
14
|
+
color: var(--dsw-alias-label-primary);
|
|
15
|
+
font-size: 13px; line-height: 1.5;
|
|
16
|
+
min-width: 0; width: 100%; max-width: 640px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* House header: badge + title + one-line status. */
|
|
20
|
+
[data-review-header] { display: flex; gap: 10px; align-items: flex-start; padding: 2px 2px 8px; }
|
|
21
|
+
[data-review-heading] { display: flex; flex-direction: column; min-width: 0; }
|
|
22
|
+
[data-review-title] { margin: 0; font-size: 15px; font-weight: 600; line-height: 22px; }
|
|
23
|
+
[data-review-status] { font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
|
|
24
|
+
[data-review-status] [data-review-notice] { margin: 0; }
|
|
25
|
+
[data-review-status] [data-review-notice][data-tone="ok"] { color: var(--dsw-alias-state-success-primary); }
|
|
26
|
+
[data-review-status] [data-review-notice][data-tone="bad"] { color: var(--dsw-alias-state-error-primary); }
|
|
27
|
+
|
|
28
|
+
[data-review-actions] { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
29
|
+
[data-review-actions] button {
|
|
30
|
+
min-height: 32px; padding: 0 12px; border-radius: 8px;
|
|
31
|
+
border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1);
|
|
32
|
+
color: inherit; font: inherit; cursor: pointer;
|
|
33
|
+
}
|
|
34
|
+
[data-review-actions] button:disabled { opacity: 0.55; cursor: default; }
|
|
35
|
+
[data-review-actions] button:focus-visible { outline: 2px solid var(--dsw-alias-border-l2); outline-offset: 2px; }
|
|
36
|
+
|
|
37
|
+
/* House row: label and hint left, control held right, hairline between. */
|
|
38
|
+
[data-review-row] {
|
|
39
|
+
display: flex; align-items: center; gap: 8px;
|
|
40
|
+
padding: 16px 0; border-bottom: 1px solid var(--dsw-alias-border-l2); min-width: 0;
|
|
41
|
+
}
|
|
42
|
+
[data-review-row]:last-of-type { border-bottom: none; }
|
|
43
|
+
[data-review-row-text] { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; padding-right: 48px; }
|
|
44
|
+
[data-review-control] { flex: none; display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-height: 36px; }
|
|
45
|
+
/* Declared once. These two rules previously existed twice — the 12px/600 pair
|
|
46
|
+
sat AFTER the house values and won on source order at equal specificity, so
|
|
47
|
+
the whole tab rendered at the old weight and the old hint size. */
|
|
48
|
+
[data-review-label] { font-size: 14px; font-weight: 400; line-height: 22px; color: var(--dsw-alias-label-primary); }
|
|
49
|
+
[data-review-hint] { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
50
|
+
|
|
51
|
+
/* A secret whose value is committed by a button: input and save travel
|
|
52
|
+
together, the save under the input it writes. */
|
|
53
|
+
[data-review-secret-group] { display: flex; flex-direction: column; align-items: stretch; gap: 8px; min-width: 220px; }
|
|
54
|
+
[data-review-secret-group] button {
|
|
55
|
+
align-self: flex-end; min-height: 32px; padding: 0 12px; border-radius: 8px;
|
|
56
|
+
border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1);
|
|
57
|
+
color: inherit; font: inherit; cursor: pointer;
|
|
58
|
+
}
|
|
59
|
+
[data-review-secret-group] button:disabled { opacity: 0.55; cursor: default; }
|
|
60
|
+
[data-review-secret-group] button:focus-visible { outline: 2px solid var(--dsw-alias-border-l2); outline-offset: 2px; }
|
|
61
|
+
|
|
62
|
+
/* Every row control gets the same box. The password type belongs in this list:
|
|
63
|
+
without it the two secret fields rendered at the UA default — 21px tall, no
|
|
64
|
+
radius — beside 32px fields with an 8px radius in the same column. */
|
|
65
|
+
[data-review-control] input[type="text"], [data-review-control] input[type="password"], [data-review-control] select {
|
|
66
|
+
min-height: 32px; padding: 0 10px; border-radius: 8px;
|
|
67
|
+
border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1);
|
|
68
|
+
color: inherit; font: inherit;
|
|
69
|
+
}
|
|
70
|
+
[data-review-control] input:focus-visible, [data-review-control] select:focus-visible {
|
|
71
|
+
outline: 2px solid var(--dsw-alias-border-l2); outline-offset: 2px;
|
|
72
|
+
}
|
|
73
|
+
/* The checkbox is the one control the harness draws at its own size; without
|
|
74
|
+
this it rendered at the UA's 13px. Same values as the reference row. */
|
|
75
|
+
[data-review-control] input[type="checkbox"] {
|
|
76
|
+
width: 16px; height: 16px; margin: 0; flex: none;
|
|
77
|
+
accent-color: var(--dsw-alias-brand-primary, #0A84FF);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[data-review-pin], [data-review-lan] { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
81
|
+
[data-review-pin] code, [data-review-lan] code {
|
|
82
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
83
|
+
padding: 2px 6px; border-radius: 6px; background: var(--dsw-alias-bg-layer-2);
|
|
84
|
+
}
|
|
85
|
+
[data-review-pin] button, [data-review-lan] button {
|
|
86
|
+
min-height: 32px; padding: 0 10px; border-radius: 8px;
|
|
87
|
+
border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1);
|
|
88
|
+
color: inherit; font: inherit; cursor: pointer;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media (max-width: 640px) {
|
|
92
|
+
/* Below the measure the row stacks: a right-held control has no room left,
|
|
93
|
+
and a wrapped one reads as a third column. */
|
|
94
|
+
[data-review-row] { flex-direction: column; align-items: stretch; gap: 8px; }
|
|
95
|
+
[data-review-row-text] { padding-right: 0; }
|
|
96
|
+
[data-review-control] { justify-content: flex-start; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@media (max-width: 480px) {
|
|
100
|
+
[data-review-actions] button { flex: 1 1 auto; min-height: 40px; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* iOS 16px field floor: the magnifier fires below this on a focused field. */
|
|
104
|
+
@media (max-width: 1023px) and (pointer: coarse) {
|
|
105
|
+
html[data-mobile-nav-ios] [data-review-root] input,
|
|
106
|
+
html[data-mobile-nav-ios] [data-review-root] select { font-size: 16px !important; }
|
|
107
|
+
}
|
|
108
|
+
`
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mark this plugin's row in the DSH settings navigation so a style block can
|
|
3
|
+
* replace the shell's fallback gear with the maestro glyph.
|
|
4
|
+
*
|
|
5
|
+
* DSH 0.1.x projects only `id`, `order`, and `label` from a `settings.section`
|
|
6
|
+
* registration and picks icons from a closed built-in list, so each external
|
|
7
|
+
* plugin identifies its own localized row by visible text after the dialog
|
|
8
|
+
* mounts. The marker owns no shell structure and is removed on disposal.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const SETTINGS_NAV_MARKER = 'data-maestro-review-settings-nav';
|
|
12
|
+
|
|
13
|
+
/** Minimal DOM surface used here; tests inject a stub instead of `document`. */
|
|
14
|
+
type NodeSeq = Iterable<Element> & { forEach(fn: (el: Element) => void): unknown };
|
|
15
|
+
|
|
16
|
+
interface DomScope {
|
|
17
|
+
querySelectorAll(selector: string): NodeSeq
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type ElementLike = Element & {
|
|
21
|
+
setAttribute(name: string, value: string): void
|
|
22
|
+
removeAttribute(name: string): void
|
|
23
|
+
hasAttribute(name: string): boolean
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function registerSettingsNavIcon(
|
|
27
|
+
label: () => string,
|
|
28
|
+
root?: DomScope,
|
|
29
|
+
): () => void {
|
|
30
|
+
if (typeof document === 'undefined' && root === undefined) {
|
|
31
|
+
// Node-side import safety (tests inject a stub root instead).
|
|
32
|
+
return () => {};
|
|
33
|
+
}
|
|
34
|
+
const scope = (root ?? document) as DomScope;
|
|
35
|
+
|
|
36
|
+
let disposed = false;
|
|
37
|
+
|
|
38
|
+
const sync = () => {
|
|
39
|
+
if (disposed) return;
|
|
40
|
+
const currentLabel = label().trim();
|
|
41
|
+
const buttons = scope.querySelectorAll('[role="dialog"] nav button');
|
|
42
|
+
for (const button of Array.from(buttons)) {
|
|
43
|
+
const el = button as ElementLike;
|
|
44
|
+
const matches =
|
|
45
|
+
currentLabel.length > 0 &&
|
|
46
|
+
button.textContent != null &&
|
|
47
|
+
button.textContent.trim() === currentLabel;
|
|
48
|
+
if (matches) el.setAttribute(SETTINGS_NAV_MARKER, '');
|
|
49
|
+
else if (el.hasAttribute(SETTINGS_NAV_MARKER)) el.removeAttribute(SETTINGS_NAV_MARKER);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
sync();
|
|
54
|
+
|
|
55
|
+
let observer: MutationObserver | null = null;
|
|
56
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
57
|
+
observer = new MutationObserver(sync);
|
|
58
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
disposed = true;
|
|
63
|
+
if (observer !== null) observer.disconnect();
|
|
64
|
+
for (const element of Array.from(scope.querySelectorAll(`[${SETTINGS_NAV_MARKER}]`))) {
|
|
65
|
+
(element as ElementLike).removeAttribute(SETTINGS_NAV_MARKER);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|