Fix #958: highlight IPv6 + allow editing built-in keyword rules (#962)

Two changes addressing both halves of #958:

1. IPv6 highlighting
   The built-in 'URL, IP & MAC' rule only shipped URL, IPv4 and MAC
   patterns, so compressed IPv6 addresses such as 2001:11:22:33::5 or
   fe80::d2dd:bff:fe79:f2bb were never highlighted. Add an IPv6 regex
   covering full and compressed forms (including ::1 and leading-/trailing-
   :: variants) and merge it into the same 'ip-mac' rule's patterns. The
   normalizer's existing "fill missing defaults" path means existing users
   pick this up on next start with no migration step.

2. Editable built-in rules
   Add an optional `customized` flag to KeywordHighlightRule. When false /
   absent, normalize re-syncs the rule's label/patterns with the shipped
   defaults (so future default-pattern upgrades reach users automatically).
   When true, normalize keeps the user's label/patterns/color/enabled
   verbatim, allowing built-ins like 'ip-mac' to be tailored.

   SettingsTerminalTab:
   - Pencil icon now appears on built-ins too. Editing one routes through
     the same dialog and flips `customized` on save.
   - The pattern field becomes a Textarea so multi-pattern built-ins (e.g.
     'error' ships seven spellings) can all be edited in one go.
   - A per-rule "↺" reset icon appears on customized built-ins and restores
     the shipped label/patterns while preserving the user's color/enabled.
   - The footer's "Reset to default colors" button is broadened into
     "Reset built-ins to defaults", restoring every built-in to shipped
     label/patterns/color and clearing `customized`.

Tests:
   New domain/keywordHighlight.test.ts (6 tests) covers IPv6 matches for
   both #958 examples plus loopback and full-form, IPv4/MAC still match,
   normalize migrates legacy non-customized 'ip-mac' to include IPv6,
   normalize preserves customized patterns, and normalize keeps user
   custom rules verbatim. Full suite: 808/0/3.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
陈大猫
2026-05-12 20:35:07 +08:00
committed by GitHub
parent ea5320d94a
commit 67c5571df5
5 changed files with 287 additions and 42 deletions

View File

@@ -358,12 +358,16 @@ const en: Messages = {
'settings.terminal.scrollback.rows': 'Number of rows *',
'settings.terminal.keywordHighlight.title': 'Keyword highlighting',
'settings.terminal.keywordHighlight.resetColors': 'Reset to default colors',
'settings.terminal.keywordHighlight.resetDefaults': 'Reset built-ins to defaults',
'settings.terminal.keywordHighlight.resetBuiltIn': 'Restore default label and patterns',
'settings.terminal.keywordHighlight.addCustom': 'Add Custom Rule',
'settings.terminal.keywordHighlight.editCustom': 'Edit Rule',
'settings.terminal.keywordHighlight.editBuiltIn': 'Edit Built-in Rule',
'settings.terminal.keywordHighlight.labelField': 'Label & Color',
'settings.terminal.keywordHighlight.labelPlaceholder': 'Label (e.g., Down)',
'settings.terminal.keywordHighlight.patternField': 'Regex Pattern',
'settings.terminal.keywordHighlight.patternPlaceholder': 'Regex (e.g., \\bdown\\b)',
'settings.terminal.keywordHighlight.patternField': 'Regex Patterns',
'settings.terminal.keywordHighlight.patternPlaceholder': 'One regex per line (e.g., \\bdown\\b)',
'settings.terminal.keywordHighlight.patternHint': 'One regex per line. Patterns are matched case-insensitively with the global flag.',
'settings.terminal.keywordHighlight.invalidPattern': 'Invalid regex pattern',
'settings.terminal.keywordHighlight.preview': 'Preview',
'settings.terminal.section.localShell': 'Local Shell',

View File

@@ -1490,12 +1490,16 @@ const zhCN: Messages = {
'settings.terminal.scrollback.rows': '行数 *',
'settings.terminal.keywordHighlight.title': '关键字高亮',
'settings.terminal.keywordHighlight.resetColors': '重置为默认颜色',
'settings.terminal.keywordHighlight.resetDefaults': '把内置规则恢复为默认',
'settings.terminal.keywordHighlight.resetBuiltIn': '恢复内置标签与正则',
'settings.terminal.keywordHighlight.addCustom': '添加自定义规则',
'settings.terminal.keywordHighlight.editCustom': '编辑规则',
'settings.terminal.keywordHighlight.editBuiltIn': '编辑内置规则',
'settings.terminal.keywordHighlight.labelField': '标签与颜色',
'settings.terminal.keywordHighlight.labelPlaceholder': '标签(如 Down',
'settings.terminal.keywordHighlight.patternField': '正则表达式',
'settings.terminal.keywordHighlight.patternPlaceholder': '正则表达式(如 \\bdown\\b',
'settings.terminal.keywordHighlight.patternPlaceholder': '每行一个正则(如 \\bdown\\b',
'settings.terminal.keywordHighlight.patternHint': '每行一个正则。匹配忽略大小写,全局匹配。',
'settings.terminal.keywordHighlight.invalidPattern': '无效的正则表达式',
'settings.terminal.keywordHighlight.preview': '预览',
'settings.terminal.section.localShell': '本地 Shell',

View File

@@ -19,6 +19,7 @@ import { Button } from "../../ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../../ui/dialog";
import { Input } from "../../ui/input";
import { Label } from "../../ui/label";
import { Textarea } from "../../ui/textarea";
import { Select as ShadcnSelect, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../ui/select";
import { SectionHeader, Select, SettingsTabContent, SettingRow, Toggle } from "../settings-ui";
import { ThemeSelectModal } from "../ThemeSelectModal";
@@ -34,21 +35,25 @@ const AddCustomRuleDialog: React.FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
editRule?: KeywordHighlightRule | null;
isBuiltIn?: boolean;
onAdd: (rule: KeywordHighlightRule) => void;
}> = ({ open, onOpenChange, editRule, onAdd }) => {
}> = ({ open, onOpenChange, editRule, isBuiltIn = false, onAdd }) => {
const { t } = useI18n();
const [label, setLabel] = useState('');
const [pattern, setPattern] = useState('');
// Multi-line text: one regex pattern per line. Built-in rules typically
// ship multiple patterns (e.g. several spellings of "error"), and the user
// is allowed to add as many as they like.
const [patternsText, setPatternsText] = useState('');
const [color, setColor] = useState(DEFAULT_NEW_RULE_COLOR);
const [patternError, setPatternError] = useState<string | null>(null);
const reset = () => { setLabel(''); setPattern(''); setColor(DEFAULT_NEW_RULE_COLOR); setPatternError(null); };
const reset = () => { setLabel(''); setPatternsText(''); setColor(DEFAULT_NEW_RULE_COLOR); setPatternError(null); };
// Populate form when editing
useEffect(() => {
if (open && editRule) {
setLabel(editRule.label);
setPattern(editRule.patterns[0] || '');
setPatternsText(editRule.patterns.join('\n'));
setColor(editRule.color);
setPatternError(null);
} else if (!open) {
@@ -57,25 +62,43 @@ const AddCustomRuleDialog: React.FC<{
}, [open, editRule]);
const handleSubmit = () => {
if (!label.trim() || !pattern.trim()) return;
try { new RegExp(pattern, 'gi'); } catch {
setPatternError(t('settings.terminal.keywordHighlight.invalidPattern'));
return;
if (!label.trim()) return;
const patterns = patternsText
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0);
if (patterns.length === 0) return;
for (const p of patterns) {
try { new RegExp(p, 'gi'); } catch {
setPatternError(t('settings.terminal.keywordHighlight.invalidPattern'));
return;
}
}
// When editing, replace only the first pattern and keep any additional ones
const patterns = editRule
? [pattern, ...editRule.patterns.slice(1)]
: [pattern];
onAdd({ id: editRule?.id ?? crypto.randomUUID(), label: label.trim(), patterns, color, enabled: editRule?.enabled ?? true });
onAdd({
id: editRule?.id ?? crypto.randomUUID(),
label: label.trim(),
patterns,
color,
enabled: editRule?.enabled ?? true,
// Editing a built-in rule flips it into "user-customized" mode so the
// normalizer keeps the user's patterns across restarts.
customized: isBuiltIn ? true : editRule?.customized,
});
reset();
onOpenChange(false);
};
const dialogTitleKey = editRule
? (isBuiltIn
? 'settings.terminal.keywordHighlight.editBuiltIn'
: 'settings.terminal.keywordHighlight.editCustom')
: 'settings.terminal.keywordHighlight.addCustom';
return (
<Dialog open={open} onOpenChange={(v) => { if (!v) reset(); onOpenChange(v); }}>
<DialogContent className="sm:max-w-[400px]">
<DialogContent className="sm:max-w-[440px]">
<DialogHeader>
<DialogTitle>{editRule ? t('settings.terminal.keywordHighlight.editCustom') : t('settings.terminal.keywordHighlight.addCustom')}</DialogTitle>
<DialogTitle>{t(dialogTitleKey)}</DialogTitle>
</DialogHeader>
<div className="space-y-3 py-2">
<div className="space-y-1.5">
@@ -95,16 +118,19 @@ const AddCustomRuleDialog: React.FC<{
</div>
<div className="space-y-1.5">
<Label className="text-xs">{t('settings.terminal.keywordHighlight.patternField')}</Label>
<Input
<Textarea
placeholder={t('settings.terminal.keywordHighlight.patternPlaceholder')}
value={pattern}
onChange={(e) => { setPattern(e.target.value); if (patternError) setPatternError(null); }}
onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(); }}
className={cn("font-mono", patternError && "border-destructive")}
value={patternsText}
onChange={(e) => { setPatternsText(e.target.value); if (patternError) setPatternError(null); }}
rows={Math.max(3, Math.min(10, patternsText.split('\n').length + 1))}
className={cn("font-mono text-xs", patternError && "border-destructive")}
/>
<p className="text-[11px] text-muted-foreground">
{t('settings.terminal.keywordHighlight.patternHint')}
</p>
{patternError && <div className="text-xs text-destructive">{patternError}</div>}
</div>
{label.trim() && pattern.trim() && !patternError && (
{label.trim() && patternsText.trim() && !patternError && (
<div className="flex items-center gap-2 p-2 rounded-md bg-muted/50">
<span className="text-xs text-muted-foreground">{t('settings.terminal.keywordHighlight.preview')}:</span>
<span className="text-sm font-medium" style={{ color }}>{label}</span>
@@ -113,7 +139,7 @@ const AddCustomRuleDialog: React.FC<{
</div>
<DialogFooter>
<Button variant="outline" onClick={() => { reset(); onOpenChange(false); }}>{t('common.cancel')}</Button>
<Button onClick={handleSubmit} disabled={!label.trim() || !pattern.trim()}>{editRule ? t('common.save') : t('common.add')}</Button>
<Button onClick={handleSubmit} disabled={!label.trim() || !patternsText.trim()}>{editRule ? t('common.save') : t('common.add')}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -133,26 +159,43 @@ const KeywordHighlightRulesEditor: React.FC<{
return (
<div className="space-y-2.5">
{rules.map((rule) => {
const custom = !isBuiltIn(rule.id);
const builtIn = isBuiltIn(rule.id);
const customized = builtIn && rule.customized;
return (
<div key={rule.id} className="flex items-center gap-2 group">
<div className="flex-1 min-w-0 flex items-center gap-1.5">
<span className={cn("text-sm truncate", !rule.enabled && "text-muted-foreground line-through")} style={rule.enabled ? { color: rule.color } : undefined}>
{rule.label}
</span>
{custom && (
<>
<Pencil
size={10}
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground cursor-pointer"
onClick={() => { setEditingRule(rule); setAddDialogOpen(true); }}
/>
<Trash2
size={10}
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground cursor-pointer"
onClick={() => onChange(rules.filter((r) => r.id !== rule.id))}
/>
</>
<Pencil
size={10}
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground cursor-pointer hover:text-foreground"
onClick={() => { setEditingRule(rule); setAddDialogOpen(true); }}
/>
{!builtIn && (
<Trash2
size={10}
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground cursor-pointer hover:text-destructive"
onClick={() => onChange(rules.filter((r) => r.id !== rule.id))}
/>
)}
{customized && (
<RotateCcw
size={10}
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground cursor-pointer hover:text-foreground"
aria-label={t('settings.terminal.keywordHighlight.resetBuiltIn')}
onClick={() => {
// Drop the user's customizations and restore the shipped
// defaults for label/patterns. Color stays whatever the
// user picked (color is the only built-in property they
// can edit without flipping `customized`).
const def = DEFAULT_KEYWORD_HIGHLIGHT_RULES.find((r) => r.id === rule.id);
if (!def) return;
onChange(rules.map((r) => r.id === rule.id
? { ...def, color: r.color, enabled: r.enabled, customized: false }
: r));
}}
/>
)}
</div>
<label className="relative flex-shrink-0">
@@ -186,14 +229,18 @@ const KeywordHighlightRulesEditor: React.FC<{
size="sm"
className="flex-1 text-muted-foreground hover:text-foreground"
onClick={() => {
// Restore every built-in rule back to shipped defaults
// (label/patterns/color), drop customizations, and keep the user's
// custom rules untouched.
onChange(rules.map((rule) => {
const def = DEFAULT_KEYWORD_HIGHLIGHT_RULES.find((r) => r.id === rule.id);
return def ? { ...rule, color: def.color } : rule;
if (!def) return rule;
return { ...def, enabled: rule.enabled, customized: false };
}));
}}
>
<RotateCcw size={14} className="mr-1.5" />
{t("settings.terminal.keywordHighlight.resetColors")}
{t("settings.terminal.keywordHighlight.resetDefaults")}
</Button>
</div>
@@ -201,6 +248,7 @@ const KeywordHighlightRulesEditor: React.FC<{
open={addDialogOpen}
onOpenChange={(v) => { setAddDialogOpen(v); if (!v) setEditingRule(null); }}
editRule={editingRule}
isBuiltIn={editingRule ? isBuiltIn(editingRule.id) : false}
onAdd={(rule) => {
if (editingRule) {
onChange(rules.map((r) => r.id === editingRule.id ? rule : r));

View File

@@ -0,0 +1,151 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
DEFAULT_KEYWORD_HIGHLIGHT_RULES,
KeywordHighlightRule,
normalizeTerminalSettings,
} from "./models";
const IP_MAC_RULE = "ip-mac";
const ipMacDefault = () => {
const def = DEFAULT_KEYWORD_HIGHLIGHT_RULES.find((r) => r.id === IP_MAC_RULE);
if (!def) throw new Error("ip-mac default rule missing");
return def;
};
const getRule = (
rules: KeywordHighlightRule[],
id: string,
): KeywordHighlightRule => {
const rule = rules.find((r) => r.id === id);
if (!rule) throw new Error(`rule ${id} missing`);
return rule;
};
const matchesAny = (patterns: string[], input: string): boolean =>
patterns.some((p) => new RegExp(p, "gi").test(input));
test("ip-mac built-in rule includes IPv6 patterns by default", () => {
const def = ipMacDefault();
// Compressed mid-form (issue #958 example #1)
assert.ok(
matchesAny(def.patterns, "2001:11:22:33::5"),
"expected default ip-mac rule to match 2001:11:22:33::5",
);
// Link-local compressed (issue #958 example #2)
assert.ok(
matchesAny(def.patterns, "fe80::d2dd:bff:fe79:f2bb"),
"expected default ip-mac rule to match fe80::d2dd:bff:fe79:f2bb",
);
// Loopback
assert.ok(matchesAny(def.patterns, "::1"), "expected ::1 to match");
// Full form
assert.ok(
matchesAny(def.patterns, "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
"expected full-form IPv6 to match",
);
});
test("ip-mac IPv6 regex still matches IPv4 and MAC", () => {
const def = ipMacDefault();
assert.ok(matchesAny(def.patterns, "10.0.0.1"), "expected IPv4 still matches");
assert.ok(
matchesAny(def.patterns, "aa:bb:cc:dd:ee:ff"),
"expected MAC still matches",
);
});
test("ip-mac IPv6 regex does not match obviously-not-IPv6 hex blobs", () => {
const def = ipMacDefault();
// A single hex word without colons must not match
assert.ok(!matchesAny(def.patterns, "deadbeef"), "single hex word matched");
// A typical sha-like string with colons separating fewer than two groups
assert.ok(!matchesAny(def.patterns, "abc"), "stray hex matched");
});
test("normalize adds newly-shipped default rules to legacy saved sets", () => {
// Simulate an older save that only has 'error' and an old-shape 'ip-mac'
// (i.e. without IPv6). Because the rule is NOT marked customized, normalize
// should re-sync it with the latest shipped patterns.
const legacyIpMacPatterns = ["legacy-pattern-from-old-default"];
const saved: KeywordHighlightRule[] = [
{
id: "error",
label: "Error",
patterns: ["\\berror\\b"],
color: "#F87171",
enabled: true,
},
{
id: IP_MAC_RULE,
label: "URL, IP & MAC",
patterns: legacyIpMacPatterns,
color: "#EC4899",
enabled: true,
},
];
const settings = normalizeTerminalSettings({
keywordHighlightRules: saved,
});
const rules = settings.keywordHighlightRules;
// Every shipped default exists (warning/ok/info/debug get added).
for (const def of DEFAULT_KEYWORD_HIGHLIGHT_RULES) {
assert.ok(
rules.some((r) => r.id === def.id),
`expected normalize to include shipped rule ${def.id}`,
);
}
// ip-mac was not customized → patterns re-sync to defaults, picking up IPv6.
const ipMac = getRule(rules, IP_MAC_RULE);
assert.deepEqual(ipMac.patterns, ipMacDefault().patterns);
assert.ok(matchesAny(ipMac.patterns, "2001:11:22:33::5"));
});
test("normalize preserves user-edited patterns when rule.customized is set", () => {
const customPatterns = ["\\bMY_CUSTOM\\b", "\\bANOTHER\\b"];
const customLabel = "My Errors";
const saved: KeywordHighlightRule[] = [
{
id: "error",
label: customLabel,
patterns: customPatterns,
color: "#FF0000",
enabled: false,
customized: true,
},
];
const settings = normalizeTerminalSettings({
keywordHighlightRules: saved,
});
const rule = getRule(settings.keywordHighlightRules, "error");
assert.equal(rule.label, customLabel);
assert.deepEqual(rule.patterns, customPatterns);
assert.equal(rule.color, "#FF0000");
assert.equal(rule.enabled, false);
assert.equal(rule.customized, true);
});
test("normalize keeps custom (non-built-in) rules verbatim", () => {
const customRule: KeywordHighlightRule = {
id: "user-uuid-1",
label: "Pager",
patterns: ["\\b[A-Z]{3}-\\d+\\b"],
color: "#00FF00",
enabled: true,
};
const settings = normalizeTerminalSettings({
keywordHighlightRules: [customRule],
});
const rule = getRule(settings.keywordHighlightRules, "user-uuid-1");
assert.deepEqual(rule.patterns, customRule.patterns);
assert.equal(rule.label, customRule.label);
});

View File

@@ -464,6 +464,11 @@ export interface KeywordHighlightRule {
patterns: string[]; // Regex patterns to match
color: string; // Highlight color (hex)
enabled: boolean;
// Set to true when the user edits a built-in rule's label/patterns so
// normalize keeps the user-edited values instead of overwriting them with
// the latest shipped defaults. Absent / false means "still tracking defaults"
// and the rule picks up new built-in patterns added in later versions.
customized?: boolean;
}
export interface TerminalSettings {
@@ -560,6 +565,24 @@ const URL_HIGHLIGHT_PATTERN =
"(?:\\bhttps?:\\/\\/\\[[0-9A-Fa-f:.]+\\](?::\\d+)?(?:[/?#][^\\s<>\"'`]*)?(?<![.,;:!?\\)}])|\\b(?:https?:\\/\\/|www\\.)[^\\s<>\"'`]+(?<![.,;:!?\\])}]))";
const IPV4_HIGHLIGHT_PATTERN =
`(?<![\\w.])(?<!\\bver\\s)(?<!\\bversion\\s)(?:${STRICT_IPV4_OCTET_PATTERN}\\.){3}${STRICT_IPV4_OCTET_PATTERN}(?![\\w.])`;
// Covers full and compressed forms (1:2:3:4:5:6:7:8, fe80::1, ::1, 2001:db8::,
// etc.). Bracketed `[…]:port` URLs are matched by URL_HIGHLIGHT_PATTERN.
// Zone IDs (%eth0) and IPv4-mapped (::ffff:192.0.2.1) are intentionally out
// of scope here — add them as custom patterns if you need them.
const IPV6_HIGHLIGHT_PATTERN =
'(?<![\\w:.])' +
'(?:' +
'(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}' +
'|(?:[0-9A-Fa-f]{1,4}:){1,7}:' +
'|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}' +
'|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}' +
'|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}' +
'|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}' +
'|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}' +
'|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}' +
'|::(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}' +
')' +
'(?![\\w:.])';
const MAC_ADDRESS_HIGHLIGHT_PATTERN =
'\\b([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\\b';
@@ -569,7 +592,7 @@ export const DEFAULT_KEYWORD_HIGHLIGHT_RULES: KeywordHighlightRule[] = [
{ id: 'ok', label: 'OK', patterns: ['\\[ok\\]', '\\bok\\b', '\\bsuccess(ful)?\\b', '\\bpassed\\b', '\\bcompleted\\b', '\\bdone\\b'], color: '#34D399', enabled: true },
{ id: 'info', label: 'Info', patterns: ['\\[info\\]', '\\[notice\\]', '\\[note\\]', '\\bnotice\\b', '\\bnote\\b'], color: '#3B82F6', enabled: true },
{ id: 'debug', label: 'Debug', patterns: ['\\[debug\\]', '\\[trace\\]', '\\[verbose\\]', '\\bdebug\\b', '\\btrace\\b', '\\bverbose\\b'], color: '#A78BFA', enabled: true },
{ id: 'ip-mac', label: 'URL, IP & MAC', patterns: [URL_HIGHLIGHT_PATTERN, IPV4_HIGHLIGHT_PATTERN, MAC_ADDRESS_HIGHLIGHT_PATTERN], color: '#EC4899', enabled: true },
{ id: 'ip-mac', label: 'URL, IP & MAC', patterns: [URL_HIGHLIGHT_PATTERN, IPV4_HIGHLIGHT_PATTERN, IPV6_HIGHLIGHT_PATTERN, MAC_ADDRESS_HIGHLIGHT_PATTERN], color: '#EC4899', enabled: true },
];
const cloneKeywordHighlightRule = (rule: KeywordHighlightRule): KeywordHighlightRule => ({
@@ -594,6 +617,21 @@ const normalizeKeywordHighlightRules = (
return cloneKeywordHighlightRule(rule);
}
// A built-in rule the user has explicitly edited keeps its label/patterns;
// otherwise we re-sync with the latest defaults so newly shipped patterns
// (e.g. the IPv6 entry in `ip-mac`) propagate to existing users without
// a manual reset.
if (rule.customized) {
return {
...defaultRule,
label: rule.label,
patterns: [...rule.patterns],
color: rule.color,
enabled: rule.enabled,
customized: true,
};
}
return {
...defaultRule,
color: rule.color,