feat(dashboard): configurable text link patterns in transcripts - #8302
feat(dashboard): configurable text link patterns in transcripts#8302jingchaodev wants to merge 1 commit into
Conversation
|
Attribution for the two red checks on head 17b9cc1 — neither originates in this PR's diff: Backend Tests (Windows) (1) — five failures, all in Dependency Audit / Audit Production Dependencies — Plan: once the #8201 regression is fixed on main, I'll re-roll this head over a byte-identical tree to re-run both lanes. |
|
E2E (stub ACP backend, offline) — base-mismatch artifact, not a defect in this branch (same class as the #1364 round-8 finding). The failing step is the i18n render vs-base gate. This run resolved Verified by running the identical gate locally against the real branch point: Plan unchanged: when the |
17b9cc1 to
17e9cde
Compare
|
Audit update for head 17e9cde: |
17e9cde to
5f81938
Compare
UX Review (Fable 5, fork) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS The whole-match code chip silently trades its copy action for open-in-new-tab, and no artifact in this lane shows any of the new controls. Watch
Evidence gaps
Suggestions
[UX-REVIEWED] 856002c |
First Principles Review (Fable 5, fork) — ✅ PASSPremise-level review of All claims verified against the base: the autolink rule engine ( First-Principles-Verdict: PASS Operator regex-to-link rules land on the existing autolink engine; every guard names the boundary it protects, and nothing duplicates an existing mechanism. What this change shipsIntent: let operators turn work-item ids in chat transcripts into clickable tracker links via their own regex-to-URL rules — an ADDITION.
The Python Subtractions
[FIRST-PRINCIPLES-REVIEWED] 856002c |
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of The doc isn't touched in the patch (no Design-Verdict: CONCERNS Sound feature on the right seam, but it ships a hand-rolled regex safety analyzer whose own history shows it's a bypass treadmill. Watch
Suggestions
[DESIGN-REVIEWED] 856002c |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed |
5f81938 to
b2ff8fd
Compare
|
Dispositions for the two blocking reviews, addressed on head b2ff8fd: First Principles — core finding ACCEPTED and applied. First Principles — InlineCode subtraction DECLINED, with the named user. This feature's originating report is precisely a transcript full of backticked ticket ids that were copy-only (PR body, first paragraph). Agents backtick work-item ids constantly; GPT — feature map: FIXED. The GPT — blur deletes a stored rule: FIXED. A half-edited row (cleared URL, kept pattern) now blocks the save on blur instead of persisting the filtered list; deletion only happens through the remove button. GPT — zero-width regex freeze and tilde fences: MOOT by the rewire. Both findings target the deleted |
b2ff8fd to
b4eef3f
Compare
|
GPT round-2 dispositions, addressed on head b4eef3f: Operator-regex ReDoS — FIXED.
Function-local import — FIXED. The three caps import at module level from |
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
b4eef3f to
ee34794
Compare
|
Status on head ee34794 — review-ready:
Remaining gate is human review — over to the maintainers. |
6a60513 to
9a320a5
Compare
|
Round-13 fixes on GPT security-class blocking (external updates erase an in-progress rule draft) — legitimate, FIXED. The adopt effect replaced local rows wholesale, so a rule being typed when another client changed the config was silently discarded. Now the adopt diffs the outgoing baseline (parsed from the adopted watermark, which every assignment stores as serialized rules): rows typed since the last sync are carried across the adopt and rendered beside the adopted rules — that visible coexistence is the conflict surface — while a clean row the external change deleted is in the baseline and still goes. Two tests ( CI reds — all three were mine from round 12, fixed: hand-written pseudolocale entries regenerated via Rebased onto |
9a320a5 to
f095766
Compare
|
Round-14 fix on Live-probed before patching: Fix: Gates: tsc 0, vitest autolinkRules 51/51 + editor/renderer 13/13 + i18n 93/93, i18n-check 0 FAIL, eslint, added-strings gate OK. |
f095766 to
0877f56
Compare
|
Round-15 fix on Live-probed the exact scenario first: Fix: a per-document aggregate wall-clock budget ( Re-ran the attack scenario through the real pipeline with the fix live: 73ms (from 7841ms). Two new tests — zero budget stops config rules while edition rules still link; the budget resets per document — mutation-proven (bypassing the budget check fails the first). Gates: tsc 0, vitest 66/66 across the editor/rules/renderer suites, eslint clean. |
0877f56 to
d42e1ce
Compare
|
Round-16 on UX (all three Watch items fixed):
First Principles (both taken): Design (declined, with reasons):
Gates: frontend 165/165 (editor/rules/renderer/i18n/registry), backend 97/97, tsc 0, i18n gates 0 FAIL, eslint, black. |
d42e1ce to
f17b60a
Compare
|
Round-17 on GPT finding. Probed first: ten 2000-char code spans × the same 50 gate-passing rules measured 7764ms through Frontend Tests (1) — Gates: tsc 0, vitest 173/173 (editor/rules/renderer/i18n/registry/settings-coverage), backend 97/97, eslint, black. |
f17b60a to
d208f1c
Compare
|
Round-18 disposition (head
Windows shard (3) red on Gates: backend 97/97, frontend 75+93+7, tsc, eslint, flake8/isort/mypy (1289 files), black gate, i18n string gates. Screenshots re-pinned. |
d208f1c to
c376678
Compare
|
Rebased onto |
|
Frontend Tests (1) red on |
|
Round 20 (GPT) — fixed on F1 (blocking, security-class): legitimate — fixed. The single-pump exemption let Fix follows the gate's established syntactic lineage (no overlap analysis to get wrong): fixed-width Mutation-proven: disabling the fixed-width inclusion fails the new test (55/56), restored 56/56. The editor's inline warning ("keep one repeating part") already describes this refusal class — no message change. Note the adjudicator's own severity flag (operator-only, polynomial, self-recovering) is on record; fixing anyway was cheaper than a human override and strictly strengthens the gate. Gates: autolink 56/56, frontend suites 118/118, backend 97/97, tsc 0, both i18n gates, screenshots re-pinned. |
|
Rebased onto upstream |
|
Round 21 (GPT) — all three findings fixed on F1 (blocking, security-class): legitimate — fixed. The budget's "one parse = one document" assumption broke against F2 (editor URL check weaker than registration): fixed, one layer deeper than asked. The inline check now IS the registry's acceptance rule — new F3 (stale comment): fixed — the whole-match chip comment now says operator-configured only. Gates: autolink 57/57 (incl. new pins), editor + renderer suites green, frontend 177/177 across 8 files, backend 97/97, tsc 0, both i18n gates, screenshots re-pinned. The |
|
Round 22 (GPT) — fixed on F1 (blocking, security-class): legitimate — fixed exactly as suggested. Both server sites ( Consequence handled: patterns differing only in edge whitespace are DIFFERENT regexes, so the dedup (server 400 + client flag) now compares exact text — the twins Gates: backend 98/98 (+ flake8, mypy, black), frontend 178/178 across 8 files, tsc 0, both i18n gates, screenshots re-pinned. For the record, the adjudicator's LOW-harm flag (rendering-only, self-correcting, visible) is noted — fixed anyway, same reasoning as rounds 20/21: a code fix is cheaper than a human fence override and the preservation semantics are simply correct. |
|
Fast Gate red on |
Operator-defined regex -> URL template rules (dashboard.link_patterns) rewrite matching plain text into links at render time: prose matches become markdown anchors, and an inline-code span whose whole text matches renders as a link chip instead of the copy-only chip. Masking keeps code blocks, existing links, autolinks and bare URLs untouched; templates are http(s)-only and matched text is URL-encoded. Rules are edited in Settings -> Chat and served through the dashboard config API, so old transcripts linkify retroactively at display time.
|
All five lanes went green on Also took GPT's one remaining non-blocking finding in the same push: the PUT's scheme check was case-sensitive while the editor validates with the browser's case-insensitive URL parser, so |
Problem / Motivation
Chat transcripts are full of work-item identifiers -- ticket ids, issue keys, change-request numbers -- that render as plain text or as copy-only inline-code chips. The renderer has no way to know
PROJ-123maps to a tracker URL, so none of them are clickable: the user copies the id, opens a browser tab, and pastes it into their tracker by hand, for every id, in every message.Why it matters
For anyone whose agent works with an issue tracker (Jira, YouTrack, Bugzilla, an internal system), every mention is a navigation dead end. The fix cannot be hardcoded upstream because the id-to-URL mapping is operator-specific -- which is exactly what made
dashboard.jira_hostsa config key.What changed (motivation → approach → change)
Goal: let operators declare their own text-to-link mappings once, and have every transcript -- including already-stored ones -- render them as links.
Approach: a new
dashboard.link_patternsconfig key (list of{pattern, url}rules: JavaScript regex + http(s) URL template in which{match}receives the matched text, percent-encoded), applied at render time only. Rules register into the EXISTING autolink rule engine (utils/autolinkRules.ts+ theremarkAutolinkRulesmasking plugin) that editions already register vocabulary on — no parallel transform. This followsdashboard.jira_hostsfor operator link config flowing config → GET → React context. Rendering-only means stored messages never change and old transcripts linkify retroactively.What was built:
LinkPatternRuledataclass +_coerce_link_patterns(skip-invalid, capped at 50 rules, http(s)-only templates) inconfig/sections.py; loader wiring; PUT validation (400 withcode: invalid_link_patterns) and GET exposure in the dashboard config handler;config-baseline.jsonregenerated.utils/autolinkRules.ts):setConfigAutolinkRulescompiles operator rules into the shared autolink registry with registration-time safety gates — structural ReDoS refusal (hasCatastrophicShape, polynomial shapes, a pump budget capping quantifier products at 16, a 100-atom cap on width-fixed brace demand) plus a bounded execution-time ladder — andlink_pattern_url_ok-equivalent template checks (two-canary origin stability, no userinfo,{match}outside the authority) mirrored server-side. Config rules scan at most 2000 chars per text node; rendering caps at 200 links per node. Rules that fail a gate are skipped, and the editor shows an inline warning for the refused class (at most one wide quantifier). The inline-code link chip is scoped to operator-config rules: edition-registered vocabulary keeps its shipped copy-only chip.remarkAutolinkRulesplugin masks fenced + inline code, existing links, autolinks and raw HTML, then rewrites prose matches to markdown links;InlineCoderenders a whole-match span as a link chip (anchor wrapping the code element, target disclosed in the native title) instead of the copy-only chip.LinkPatternsEditorrow editor (composite primitive, registered in the settings registry withconfigKey="dashboard.link_patterns"), committing on blur/add/remove, stacking narrow-first (smbreakpoint). Inline flags: invalid regex, scan-unsafe pattern, duplicate pattern, and incomplete URL template — the four mistakes that would otherwise fail silently or only at the server. Local rows are guarded by settlement-aware watermarks so a rejected save's rollback never erases typed rows and a confirmed save still adopts later external changes. 12 i18n keys across all 12 catalogs + pseudolocale.test_config_schema.py's annotation resolution now evals in the defining module's namespace (sections), since the loader facade's re-export list is frozen and a post-split dataclass type could never resolve there.Tests
website/src/test/autolinkRules.test.ts: registration skips (invalid regex, bad scheme, missing{match}, placeholder-in-authority, userinfo, empty-match), the structural ReDoS rejection suite (canonical(a+)+$shapes, ambiguous alternations,[^]class-lexing bypass variants, over-budget pump chains) with safe expressive shapes preserved, subject/hit caps, and{match}percent-encoded substitution.website/src/test/MarkdownRenderer.linkPatterns.test.tsx: prose match renders an anchor at the resolved template; whole-match inline code becomes a link chip (target=_blank,rel=noopener noreferrer); partial-match code spans keep the copy chip; fenced code and existing links are untouched; per-node hit and subject caps hold; no rules = unchanged render.website/src/test/LinkPatternsEditor.test.tsx: a rejected save's rollback preserves typed rows; a genuine external change is adopted; an external restore of the pre-save value after a CONFIRMED save is adopted (settlement watermark); rejection clears only the echo mark.test/test_dashboard_files_coverage.py: PUT/GET round-trip; nine malformed-body rejects (non-list, non-dict entry, empty pattern,javascript:template, >50 rules, >300-char pattern, duplicate patterns,{match}in host, userinfo) all 400 withinvalid_link_patternsand persist nothing.Manual verification
website/scripts/capture-link-patterns.mjs(committed) drives the real built SPA with the rules served through the real/api/dashboard/configshape and asserts from the live DOM before shooting: exactly 5 tracker anchors across prose and both message roles, the inline-code chip is an anchor wrapping<code>, and the fenced code block contains zero injected anchors -- in dark and light. Local gates: full MarkdownRenderer vitest family (487), i18n suite incl. all 12 style gates (666), settings/ChatPanel suites,tsc -b, eslint on changed files, jscpd (0 clones), pytest for the config/dashboard surface, isort/flake8/black-gate/mypy (1282 files), scrub-lint, brand, harness-parity, vendor-manifest, docs-lint.Screenshots / video
Prose ids link, the backticked id renders as a link chip, and the fenced code block stays plain (dark):
The Settings → Chat rule editor:
Light theme
Related Issues
N/A -- no open issue tracks this; closest neighbors are #5729 (bare-URL linkify, merged) and #6574 (chip config knob), neither of which covers operator-defined patterns.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement