Skip to content

feat: Multi-line editable regex patterns for categories (frontend-only, no backend changes)#868

Open
MrKyomoto wants to merge 1 commit into
ActivityWatch:masterfrom
MrKyomoto:feat/category-multi-regex-frontend
Open

feat: Multi-line editable regex patterns for categories (frontend-only, no backend changes)#868
MrKyomoto wants to merge 1 commit into
ActivityWatch:masterfrom
MrKyomoto:feat/category-multi-regex-frontend

Conversation

@MrKyomoto

Copy link
Copy Markdown

Summary

The original single-line regex input creates poor UX when multiple match patterns are needed — users must repeatedly move the cursor to the line end to append new pipe-separated rules.

This PR implements multi-line list editing and vertical display for category regex patterns, all logic contained in frontend with zero backend schema, API, or storage changes.
The backend continues to store one pipe-delimited regex string (pattern1|pattern2|pattern3); splitting/rejoining between string and pattern list happens entirely client-side.

Preview

Old UI:
image
image

New UI:
image
image

Changes

  1. src/util/validate.ts
    Add utility functions for pipe split/join and pattern validation
    splitRegexPipe(regex: string): string[]: Split stored pipe string into individual pattern list
    joinRegexPipe(patterns: string[]): string: Filter empty entries then join array back to single pipe regex
    validatePatternList(patterns: string[]): boolean: Run regex syntax check for every entry in list
    isPatternListBroad(patterns: string[]): boolean: Check if patterns are overly broad wildcard matches (existing logic ported to list)
  2. src/components/CategoryEditModal.vue
    Replace single-line regex text input with dynamic editable pattern list
    UI: Add add-row button, delete per-row button for each regex entry; if only one pattern exists, the delete button for that entry is disabled to prevent empty rule state
    On modal open: Call splitRegexPipe to parse backend rule.regex into array for list rendering
    On save/submit: Validate all patterns via validatePatternList, filter blanks, use joinRegexPipe to rebuild pipe string for backend payload
    Per-line real-time regex syntax validation feedback matching original validation behavior
  3. src/components/CategoryEditTree.vue
    Rule display updated: Render each split pattern as separate line item
    Each pattern renders on its own block line, matching list edit preview
    Data Flow (No Backend Modifications)
    Backend store: regex: "GitHub|Stack Overflow|vim"
    Modal mount: splitRegexPipe("GitHub|Stack Overflow|vim") → ["GitHub", "Stack Overflow", "vim"]
    User edits list (add/remove/edit individual patterns in UI)
    Save trigger: Validate all patterns → filter empty strings → joinRegexPipe(array) → "GitHub|Stack Overflow|vim"
    Send unchanged API payload shape ({ regex: string }) back to backend

Compatibility & Safety

Full backward compatibility with existing saved categories: old single regex strings split cleanly to one-item lists
Old clients / other frontends function normally with the same backend data
Matching logic unchanged: backend still evaluates one combined A|B|C regex exactly as before
No database migration, no API version bump, no server code edits required

Visual Notes

List items stack vertically, no inline crowding

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the single-line regex input in CategoryEditModal with a multi-line pattern list, splitting the stored pipe-delimited string client-side and rejoining on save so the backend payload is unchanged. CategoryEditTree is also updated to render each split pattern on its own line.

  • src/util/validate.ts: Adds splitRegexPipe (depth-tracked pipe splitter), joinRegexPipe, validatePatternList, and isPatternListBroad; the splitter doesn't account for character classes ([...]), which can cause valid patterns like [a-z|0-9] to be displayed as broken fragments in the edit modal.
  • src/components/CategoryEditModal.vue: Replaces the single b-form-input with a dynamic v-for list of per-pattern inputs; validation error display has a logic gap where both the generic red message and the per-index yellow message fire simultaneously for the same invalid pattern.
  • src/components/CategoryEditTree.vue: Renders each split pattern in a div.rule-item; uses !important inside a Vue :style object binding, which browsers silently ignore.

Confidence Score: 3/5

The data round-trip is safe (split → edit → join preserves the backend string), but the modal UI misbehaves for any existing rule whose regex contains a pipe inside a character class, and the error-message display logic fires two conflicting messages simultaneously.

Two distinct defects affect existing users: character-class pipes in stored regexes cause the edit modal to display corrupted, invalid-looking patterns on open; and any invalid pattern produces a red 'Invalid pattern(s)' message AND a yellow 'Pattern(s) X invalid' message at the same time, undermining the validation UX. The !important binding in the tree view means the intended colour for pattern codes will never apply. None of these corrupt persisted data, but the first two meaningfully degrade the editing experience for real inputs.

src/util/validate.ts (splitRegexPipe character-class handling) and src/components/CategoryEditModal.vue (error-message display conditions) need the most attention before merging.

Important Files Changed

Filename Overview
src/util/validate.ts Adds splitRegexPipe/joinRegexPipe/validatePatternList/isPatternListBroad; splitRegexPipe has a correctness gap (character classes not tracked), and two of the four exported functions are unused dead code.
src/components/CategoryEditModal.vue Replaces single regex input with multi-pattern list; has duplicate/conflicting error messages and the save path doesn't enforce validation (checkFormValidity always returns true).
src/components/CategoryEditTree.vue Adds per-pattern display in tree view using splitRegexPipe; has !important in inline style that is silently ignored by browsers.

Sequence Diagram

sequenceDiagram
    participant Backend as Backend Store (regex: string)
    participant Split as splitRegexPipe()
    participant UI as Pattern List UI
    participant Join as joinRegexPipe()

    Backend->>Split: "GitHub|Stack Overflow|vim"
    Split->>UI: ["GitHub", "Stack Overflow", "vim"]
    Note over UI: User adds/removes/edits patterns
    UI->>Join: ["GitHub", "VS Code", "vim"]
    Join->>Backend: "GitHub|VS Code|vim"

    Note over Split: ⚠️ [a-z|0-9] is split as ["[a-z", "0-9]"] (char classes ignored)
Loading

Reviews (1): Last reviewed commit: "feat: frontend multi regex rule for cate..." | Re-trigger Greptile

Comment thread src/util/validate.ts
Comment on lines +42 to +68
const parts: string[] = [];
let depth = 0; // paren-nesting depth — skip | when inside a group
let escaping = false; // simple \-escape for \|
let start = 0;
for (let i = 0; i < regex.length; i++) {
const ch = regex[i];
if (escaping) {
escaping = false;
continue;
}
if (ch === '\\') {
escaping = true;
continue;
}
if (ch === '(') {
depth++;
continue;
}
if (ch === ')') {
if (depth > 0) depth--;
continue;
}
if (ch === '|' && depth === 0) {
parts.push(regex.slice(start, i));
start = i + 1;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Character classes ([...]) are not tracked, so a pipe inside one (e.g. [a-z|0-9]) triggers a split at depth 0 and produces ["[a-z", "0-9]"] — two syntactically broken patterns. When a user re-opens a modal for a rule that contains such a pattern, they'll see false validation errors, and the intended regex is visually mangled (though the round-trip through joinRegexPipe happens to reconstruct the original string on save). Adding an inClass flag that mirrors the depth logic for ( / ) fixes this.

Suggested change
const parts: string[] = [];
let depth = 0; // paren-nesting depth — skip | when inside a group
let escaping = false; // simple \-escape for \|
let start = 0;
for (let i = 0; i < regex.length; i++) {
const ch = regex[i];
if (escaping) {
escaping = false;
continue;
}
if (ch === '\\') {
escaping = true;
continue;
}
if (ch === '(') {
depth++;
continue;
}
if (ch === ')') {
if (depth > 0) depth--;
continue;
}
if (ch === '|' && depth === 0) {
parts.push(regex.slice(start, i));
start = i + 1;
}
}
const parts: string[] = [];
let depth = 0; // paren-nesting depth — skip | when inside a group
let inClass = false; // inside a character class [...] — | is literal here
let escaping = false; // simple \-escape for \|
let start = 0;
for (let i = 0; i < regex.length; i++) {
const ch = regex[i];
if (escaping) {
escaping = false;
continue;
}
if (ch === '\\') {
escaping = true;
continue;
}
if (ch === '[' && !inClass) {
inClass = true;
continue;
}
if (ch === ']' && inClass) {
inClass = false;
continue;
}
if (inClass) continue; // any character inside [...] is literal
if (ch === '(') {
depth++;
continue;
}
if (ch === ')') {
if (depth > 0) depth--;
continue;
}
if (ch === '|' && depth === 0) {
parts.push(regex.slice(start, i));
start = i + 1;
}
}

Comment on lines +38 to +41
div.text-danger(v-if="!validPattern") Invalid pattern(s)
div.text-warning(v-if="validPattern && broad_pattern && !patternErrors.length") Pattern(s) too broad
div.text-warning(v-if="patternErrors.length > 0")
| Pattern(s) {{ patternErrors.join(', ') }} invalid

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Duplicate error messages shown simultaneously

Both !validPattern (red "Invalid pattern(s)") and patternErrors.length > 0 (yellow "Pattern(s) X invalid") fire for exactly the same condition — any invalid entry makes validPattern false and also populates patternErrors. A user typing a broken regex sees both messages at once, which is redundant and visually contradictory (red danger + yellow warning for the same fact). The generic red message should only show when patternErrors is empty but validation still fails (i.e. !validPattern && !patternErrors.length).

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

span(v-if="_class.rule.type === 'regex'")
| Rule ({{_class.rule.type}}):
div.rule-item(v-for="(pat, indx) in splitRegex(_class.rule.regex)" :key="indx")
code(:style='{ color: "#d63384 !important" }') {{ pat }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 !important inside a Vue :style object binding is silently ignored by browsers. Vue sets individual CSS properties via element.style.setProperty(prop, value) where the value "#d63384 !important" is treated as an invalid value and discarded, so the colour override will never take effect. Use a scoped CSS rule or the string form of :style if you need !important.

Suggested change
code(:style='{ color: "#d63384 !important" }') {{ pat }}
code.rule-code {{ pat }}

Comment thread src/util/validate.ts
Comment on lines +88 to +109
export function validatePatternList(patterns: string[]): {
allValid: boolean;
results: { valid: boolean; broad: boolean }[];
} {
const results = patterns
.filter(p => (p || '').trim().length > 0)
.map(p => ({
valid: validateRegex(p.trim()),
broad: isRegexBroad(p.trim()),
}));
return {
allValid: results.every(r => r.valid),
results,
};
}

/**
* Returns true if *any* pattern in the list is overly broad.
*/
export function isPatternListBroad(patterns: string[]): boolean {
return patterns.some(p => isRegexBroad(p));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 validatePatternList and isPatternListBroad are exported but never called

Both functions were introduced in this PR but the modal uses inline logic (validateSinglePattern, validPattern computed, broad_pattern computed) instead. They are dead code as written. Either wire them into the component to consolidate the validation logic, or remove them to keep the surface clean.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant