From c111abf2fc46eddc26074b5d40117a29a9590c04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:22:14 +0000 Subject: [PATCH 1/4] Initial plan From f85b5db97c30fde8e62d097fb07ecb828e278a72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:33:04 +0000 Subject: [PATCH 2/4] feat: integrate Monaco editor for profile filter configuration with JSON schema validation Co-authored-by: NecatiMeral <7882753+NecatiMeral@users.noreply.github.com> --- .../settings/profiles/pr-filter-schema.ts | 131 ++++++++++++++++++ .../profile-filter-editor.component.tsx | 78 +++++++++++ .../profiles/settings-profiles.component.tsx | 25 +--- app/index.d.ts | 1 + app/renderer.tsx | 15 ++ package-lock.json | 106 ++++++++++++-- package.json | 4 +- 7 files changed, 325 insertions(+), 35 deletions(-) create mode 100644 app/components/settings/profiles/pr-filter-schema.ts create mode 100644 app/components/settings/profiles/profile-filter-editor.component.tsx diff --git a/app/components/settings/profiles/pr-filter-schema.ts b/app/components/settings/profiles/pr-filter-schema.ts new file mode 100644 index 0000000..410d7aa --- /dev/null +++ b/app/components/settings/profiles/pr-filter-schema.ts @@ -0,0 +1,131 @@ +export const PR_FILTER_SCHEMA = { + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'PullRequestFilter', + description: 'Filter configuration for pull request profiles', + type: 'object', + required: ['op', 'filters'], + additionalProperties: false, + properties: { + op: { + type: 'string', + enum: ['AND', 'OR'], + description: 'Logical operator to combine filter nodes', + }, + filters: { + type: 'array', + description: 'List of filter nodes to evaluate', + items: { + $ref: '#/definitions/FilterNode', + }, + }, + }, + definitions: { + FilterNode: { + type: 'object', + description: 'A single filter condition', + additionalProperties: false, + properties: { + isDraft: { + type: 'boolean', + description: 'Filter by draft status', + }, + author: { + description: 'Filter by PR author', + type: 'object', + required: ['op', 'filters'], + additionalProperties: false, + properties: { + op: { type: 'string', enum: ['AND', 'OR'] }, + filters: { + type: 'array', + items: { $ref: '#/definitions/UserFilter' }, + }, + }, + }, + reviewers: { + description: 'Filter by PR reviewers', + type: 'object', + required: ['op', 'filters'], + additionalProperties: false, + properties: { + op: { type: 'string', enum: ['AND', 'OR'] }, + filters: { + type: 'array', + items: { $ref: '#/definitions/ReviewerFilter' }, + }, + }, + }, + targetBranch: { + description: 'Filter by target branch name', + type: 'object', + required: ['op', 'filters'], + additionalProperties: false, + properties: { + op: { type: 'string', enum: ['AND', 'OR'] }, + filters: { + type: 'array', + items: { type: 'string' }, + }, + }, + }, + sourceBranch: { + description: 'Filter by source branch name', + type: 'object', + required: ['op', 'filters'], + additionalProperties: false, + properties: { + op: { type: 'string', enum: ['AND', 'OR'] }, + filters: { + type: 'array', + items: { type: 'string' }, + }, + }, + }, + }, + }, + UserFilter: { + type: 'object', + description: 'Filter for a user', + additionalProperties: false, + properties: { + label: { + type: 'string', + description: 'User display name', + }, + id: { + type: 'string', + description: 'User unique identifier', + }, + isBot: { + type: 'boolean', + description: 'Is the user a bot', + }, + isMySelf: { + type: 'boolean', + description: 'Is the user the current user', + }, + }, + }, + ReviewerFilter: { + type: 'object', + description: 'Filter for a reviewer', + additionalProperties: false, + properties: { + user: { + $ref: '#/definitions/UserFilter', + description: 'Reviewer user filter', + }, + isRequired: { + type: 'boolean', + description: 'Is the reviewer required', + }, + vote: { + type: 'integer', + enum: [10, 5, 0, -5, -10], + description: + 'Reviewer vote: Approved (10), ApprovedWithSuggestions (5), NoVote (0), WaitingForAuthor (-5), Rejected (-10)', + }, + }, + }, + }, +} diff --git a/app/components/settings/profiles/profile-filter-editor.component.tsx b/app/components/settings/profiles/profile-filter-editor.component.tsx new file mode 100644 index 0000000..9f71b6c --- /dev/null +++ b/app/components/settings/profiles/profile-filter-editor.component.tsx @@ -0,0 +1,78 @@ +import Editor, { BeforeMount } from '@monaco-editor/react' +import { useEffect, useState } from 'react' +import { PR_FILTER_SCHEMA } from './pr-filter-schema' + +const SCHEMA_URI = 'https://pullrequest-manager.local/profile-filter-schema.json' +const SCHEMA_PATH_PREFIX = 'pullrequest-manager:///profile-filter/' + +const handleBeforeMount: BeforeMount = (monaco) => { + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + allowComments: false, + schemas: [ + { + uri: SCHEMA_URI, + fileMatch: [`${SCHEMA_PATH_PREFIX}*.json`], + schema: PR_FILTER_SCHEMA, + }, + ], + }) +} + +function useMonacoTheme(): string { + const [appTheme, setAppTheme] = useState('system') + const [osDark, setOsDark] = useState( + globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false, + ) + + useEffect(() => { + globalThis.api.invoke('get-theme').then((t: string) => setAppTheme(t)) + const unsubTheme = globalThis.api.receive('theme-changed', (t: string) => setAppTheme(t)) + const unsubNative = globalThis.api.receive('nativeThemeChanged', () => { + setOsDark(globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false) + }) + return () => { + unsubTheme?.() + unsubNative?.() + } + }, []) + + const isDark = appTheme === 'dark' || (appTheme === 'system' && osDark) + return isDark ? 'vs-dark' : 'vs' +} + +interface Props { + profileId: string + value?: string + onChange: (value: string) => void +} + +export default function ProfileFilterEditor({ profileId, value, onChange }: Readonly) { + const monacoTheme = useMonacoTheme() + const schemaPath = `${SCHEMA_PATH_PREFIX}${profileId}.json` + + return ( +
+ onChange(val ?? '')} + options={{ + minimap: { enabled: false }, + scrollBeyondLastLine: false, + wordWrap: 'on', + formatOnPaste: true, + formatOnType: true, + tabSize: 2, + lineNumbers: 'off', + folding: true, + automaticLayout: true, + }} + /> +
+ ) +} diff --git a/app/components/settings/profiles/settings-profiles.component.tsx b/app/components/settings/profiles/settings-profiles.component.tsx index bba9c7b..530f0ea 100644 --- a/app/components/settings/profiles/settings-profiles.component.tsx +++ b/app/components/settings/profiles/settings-profiles.component.tsx @@ -10,7 +10,6 @@ import { PopoverSurface, PopoverTrigger, Select, - Textarea, Title1, tokens, } from '@fluentui/react-components' @@ -18,6 +17,7 @@ import { AddFilled, BinRecycleRegular, ClipboardPasteFilled, CopyRegular } from import { useContext, useState } from 'react' import { useParams } from 'react-router-dom' import { SettingsContext } from '../context' +import ProfileFilterEditor from './profile-filter-editor.component' const useStackClassName = makeResetStyles({ display: 'flex', @@ -34,17 +34,6 @@ const useStyles = makeStyles({ flex: 1, height: '100%', }, - height: { - height: '100%', - flexGrow: 1, - display: 'flex', - flexDirection: 'column', - }, - textarea: { - maxHeight: 'unset', - height: '100%', - flexGrow: 1, - }, textareaField: { flexGrow: 1, display: 'flex', @@ -232,19 +221,15 @@ export default function ProfileSettings() { validationMessage={profile.filterValid ? 'JSON schema is valid.' : 'Invalid schema! Please check your JSON syntax.'} className={styles.textareaField} > -