From 15b0893f7e5ad7e0a8e7ed21478a61359a2e6104 Mon Sep 17 00:00:00 2001 From: paoloredis Date: Fri, 4 Sep 2026 14:57:05 +0200 Subject: [PATCH 1/5] Show a vector element's nearest neighbours MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening an element of a vector set showed its embedding and its attributes — the two things Redis Insight shows — and stopped there. The question a vector set exists to answer is which elements are near this one, and answering it meant leaving the browser to type VSIM by hand. So the element view ends with Nearest: VSIM ELE WITHSCORES, one bar per result in the order the server ranked them. A score runs 1 (identical) to 0 (opposite), so it is already the share of the track and the list needs no axis. The element itself comes back first, scored 1 and marked "this one" — it anchors the scale and says why it is there. Clicking a neighbour opens it and asks again, which is how a reader actually explores an embedding space: hop from a point to its neighbour and see what that one is near. Two controls, both arguments of VSIM a reader would change by hand: how many to return (5/10/25 — one more is asked for, since the element itself takes a place) and a FILTER expression. The filter is the one part of a vector set whose effect cannot be seen from a reply, and a bad expression shows the server's error rather than an empty list. The command is in the READ WITH line with the others, so a reader can take it away. Verified against a live sandbox with three 2-d vectors: the near element outranks the opposite one, the opposite one scores 0, a filter on attributes cuts the list to what matches, a broken filter reports the error, walking to a neighbour re-asks from there, and the count chips change the COUNT in the command that runs. Co-Authored-By: Claude Opus 5 --- static/css/redis-workbench.css | 129 +++++++++++++++++++++++++++++++ static/js/redis-workbench.js | 136 ++++++++++++++++++++++++++++++++- 2 files changed, 264 insertions(+), 1 deletion(-) diff --git a/static/css/redis-workbench.css b/static/css/redis-workbench.css index 36ca1466ab..31c80ce533 100644 --- a/static/css/redis-workbench.css +++ b/static/css/redis-workbench.css @@ -1132,6 +1132,135 @@ body.rwb-dragging-y * { /* ---- value view ---- */ +/* ---- bars ---- + VSIM's neighbours: one row per result in the order the server returned them, + with the score printed beside the bar. A score runs 1 (identical) to 0 + (opposite), so it is already the share of the track and needs no axis. Named + for what it is rather than for VSIM — the same row suits anything the server + ranks. */ +.rwb-vsim-controls { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0.25rem 0.5rem 0.5rem; +} + +.rwb-vsim-counts { + display: flex; + flex: none; + gap: 0.25rem; +} + +/* .rwb-chip is laid out for the KEYS heading, where it is pushed to the end. */ +.rwb-vsim-counts .rwb-chip { margin-left: 0; max-width: none; } + +.rwb-chip-on { + color: var(--rwb-ink); + border-color: var(--rwb-muted); + background: var(--rwb-bg-2); +} + +.rwb-vsim-filter { + flex: 1 1 auto; + min-width: 0; + font-family: ui-monospace, "Cascadia Code", Menlo, monospace; + font-size: 12px; + line-height: 1.5; + color: var(--rwb-ink); + background: var(--rwb-bg-2); + border: 1px solid var(--rwb-line); + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; +} + +.rwb-vsim-filter::placeholder { color: var(--rwb-muted); } + +.rwb-vsim-filter:focus-visible { + outline: 2px solid var(--rwb-focus); + outline-offset: 1px; +} + +.rwb-bars { + display: flex; + flex-direction: column; + margin: 0 0.5rem; +} + +.rwb-bar-row { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.25rem 0.25rem; + font: inherit; + font-size: 12px; + color: var(--rwb-ink-2); + text-align: left; + background: none; + border: 0; + border-bottom: 1px solid var(--rwb-line-soft); + cursor: pointer; +} + +.rwb-bar-row:hover { color: var(--rwb-ink); background: var(--rwb-bg-2); } + +/* The row everything else is measured from: no walking to where you already + are. */ +.rwb-bar-marked { cursor: default; } +.rwb-bar-marked:hover { background: none; } + +.rwb-bar-name { + flex: 0 0 35%; + display: flex; + align-items: baseline; + gap: 0.375rem; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: ui-monospace, "Cascadia Code", Menlo, monospace; +} + +.rwb-bar-tag { + flex: none; + font-family: inherit; + font-size: 10px; + color: var(--rwb-muted); +} + +.rwb-bar-track { + flex: 1 1 auto; + height: 6px; + min-width: 0; + background: var(--rwb-bg-3); + border-radius: 3px; + overflow: hidden; +} + +.rwb-bar-fill { + display: block; + height: 100%; + background: #0284c7; + border-radius: 3px; +} + +/* Wide enough that a column of scores lines up, free to grow for a value that + does not fit one — "0.9993" and "8955.05 km" share this column. */ +.rwb-bar-value { + flex: none; + min-width: 6ch; + white-space: nowrap; + text-align: right; + font-variant-numeric: tabular-nums; + color: var(--rwb-muted); +} + +/* A read that failed, said in the colour of a failure. Compounded with + .rwb-text, because .rwb-json sets cyan on the same element, comes later in + this file and would win at equal specificity. */ +.rwb-text.rwb-failed { color: #fca5a5; } /* red-300 */ + + .rwb-value-head { display: flex; align-items: center; diff --git a/static/js/redis-workbench.js b/static/js/redis-workbench.js index 81ed07bc54..91b9ecc7cf 100644 --- a/static/js/redis-workbench.js +++ b/static/js/redis-workbench.js @@ -2187,15 +2187,19 @@ re-renders whatever is selected — which used to mean the element view was replaced by the key's own the moment the reader typed anything. */ this.openElement = { name: key.name, element: element }; + var search = this.vsimOptions(); var commands = [ 'VEMB ' + quote(key.name) + ' ' + quote(element), - 'VGETATTR ' + quote(key.name) + ' ' + quote(element) + 'VGETATTR ' + quote(key.name) + ' ' + quote(element), + vsimCommand(key.name, element, search) ]; this.begin('reading ' + element + '…'); return run(commands).then(function (replies) { self.renderVectorElement(key, element, { vector: ok(replies[0]), attributes: ok(replies[1]), + neighbours: replies[2], + search: search, commands: commands }); self.end(); @@ -2205,6 +2209,25 @@ }); }; + /* How many neighbours to ask for, and the filter to ask with. Kept on the dock + so it survives the re-render a sweep does, and so walking from element to + element keeps the search the reader set up. */ + var VSIM_COUNTS = [5, 10, 25]; + + dock.vsimOptions = function () { + if (!this.vsim) this.vsim = { count: VSIM_COUNTS[0], filter: '' }; + return this.vsim; + }; + + /* The element itself comes back first with a score of 1, so ask for one more + than the reader wanted and the list still holds that many neighbours. */ + function vsimCommand(name, element, search) { + var command = 'VSIM ' + quote(name) + ' ELE ' + quote(element) + + ' WITHSCORES COUNT ' + (search.count + 1); + if (search.filter) command += ' FILTER ' + quote(search.filter); + return command; + } + dock.renderVectorElement = function (key, element, detail) { var self = this; var pane = this.valuePane; @@ -2242,9 +2265,120 @@ 'None set. VSETATTR attaches JSON metadata to an element.')); } + /* The nearest elements to this one, which is the question a vector set + exists to answer. Clicking one walks to it and asks again — the way a + reader actually explores an embedding space. */ + pane.appendChild(el('div', 'rwb-group', 'Nearest')); + pane.appendChild(this.vsimControls(key, element, detail.search)); + pane.appendChild(this.renderNeighbours(key, element, detail.neighbours)); + pane.appendChild(ranNote(detail.commands)); }; + /* How many to return, and an attribute filter to return them through. The + filter is the one part of a vector set a reader cannot see the effect of + from a reply, so it gets a box rather than a paragraph. */ + dock.vsimControls = function (key, element, search) { + var self = this; + var row = el('form', 'rwb-vsim-controls'); + + var counts = el('div', 'rwb-vsim-counts'); + counts.setAttribute('role', 'group'); + counts.setAttribute('aria-label', 'How many neighbours'); + VSIM_COUNTS.forEach(function (count) { + var choice = el('button', 'rwb-chip' + (count === search.count ? ' rwb-chip-on' : ''), + String(count)); + choice.type = 'button'; + choice.title = 'Return the ' + count + ' nearest elements'; + choice.setAttribute('aria-pressed', count === search.count ? 'true' : 'false'); + choice.addEventListener('click', function () { + self.vsimOptions().count = count; + self.openVectorElement(key, element); + }); + counts.appendChild(choice); + }); + row.appendChild(counts); + + var filter = el('input', 'rwb-vsim-filter'); + filter.type = 'text'; + filter.value = search.filter; + filter.placeholder = 'FILTER, e.g. .year > 2000'; + filter.title = 'An expression over element attributes; see Filtered search'; + filter.setAttribute('aria-label', 'Filter expression'); + row.appendChild(filter); + + var go = el('button', 'rwb-btn', 'Search'); + go.type = 'submit'; + go.title = 'Run VSIM with this filter'; + row.appendChild(go); + + row.addEventListener('submit', function (event) { + event.preventDefault(); + self.vsimOptions().filter = filter.value.trim(); + self.openVectorElement(key, element); + }); + return row; + }; + + /* One bar per row, in the order given — both callers ask the server to sort, so + the bars only have to be read, not sorted. No axis: the numbers are printed + beside them and the heading says the unit. + + rows: [{name, share (0..1), label, mark, pick}] — mark names this row as the + one the others are measured against, pick makes it the next one. */ + function renderBars(rows) { + var list = el('div', 'rwb-bars'); + rows.forEach(function (row) { + var line = el(row.pick ? 'button' : 'div', + 'rwb-bar-row' + (row.mark ? ' rwb-bar-marked' : '')); + if (row.pick) { + line.type = 'button'; + line.title = row.title || row.name; + line.addEventListener('click', row.pick); + } + + var name = el('span', 'rwb-bar-name', row.name); + if (row.mark) name.appendChild(el('span', 'rwb-bar-tag', row.mark)); + line.appendChild(name); + + var track = el('span', 'rwb-bar-track'); + var fill = el('span', 'rwb-bar-fill'); + fill.style.width = (Math.max(0, Math.min(1, row.share)) * 100).toFixed(1) + '%'; + track.appendChild(fill); + line.appendChild(track); + + line.appendChild(el('span', 'rwb-bar-value', row.label)); + list.appendChild(line); + }); + return list; + } + + /* VSIM's answer. Scores run 1 (identical) to 0 (opposite), so a score is + already the share of the bar. */ + dock.renderNeighbours = function (key, element, reply) { + var self = this; + if (reply && reply.error) { + return el('p', 'rwb-text rwb-failed', '(error) ' + cellText(reply.value)); + } + var rows = pairs(ok(reply)).map(function (pair) { + return { name: cellText(pair[0]), score: Number(cellText(pair[1])) }; + }); + if (!rows.length) { + return el('p', 'rwb-empty', 'Nothing came back — no element passes this filter.'); + } + return renderBars(rows.map(function (row) { + var here = row.name === element; + return { + name: row.name, + share: row.score, + label: isNaN(row.score) ? '—' : row.score.toFixed(4), + mark: here ? 'this one' : null, + title: 'Look at ' + row.name, + pick: here ? null : function () { self.openVectorElement(key, row.name); } + }; + })); + }; + dock.openIndex = function (name) { var self = this; this.selected = name; From a9435358f3ec42a2b4b7c2a91b73e1f27e1570a2 Mon Sep 17 00:00:00 2001 From: paoloredis Date: Fri, 4 Sep 2026 15:31:18 +0200 Subject: [PATCH 2/5] Centre the count chips in their circles The 5/10/25 chips borrow .rwb-chip from the KEYS heading, where it holds a word and is padded for one. With a digit in it the number sat off centre and each chip came out a different width. One fixed size, no padding, centred on both axes. Co-Authored-By: Claude Opus 5 --- static/css/redis-workbench.css | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/static/css/redis-workbench.css b/static/css/redis-workbench.css index 31c80ce533..e11b7e3805 100644 --- a/static/css/redis-workbench.css +++ b/static/css/redis-workbench.css @@ -1151,8 +1151,22 @@ body.rwb-dragging-y * { gap: 0.25rem; } -/* .rwb-chip is laid out for the KEYS heading, where it is pushed to the end. */ -.rwb-vsim-counts .rwb-chip { margin-left: 0; max-width: none; } +/* .rwb-chip is laid out for the KEYS heading, where it is pushed to the end and + carries a word. Here it carries a number, so it is a circle of one size for + every count — a square box with the padding dropped, and the digits centred on + both axes rather than sitting where a label's padding put them. */ +.rwb-vsim-counts .rwb-chip { + margin-left: 0; + max-width: none; + justify-content: center; + width: 1.5rem; + height: 1.5rem; + padding: 0; + gap: 0; + font-size: 11px; + line-height: 1; + border-radius: 50%; +} .rwb-chip-on { color: var(--rwb-ink); From 101bc30edb3036ecf78ad2b68b02f7f7881be61c Mon Sep 17 00:00:00 2001 From: paoloredis Date: Fri, 4 Sep 2026 15:31:18 +0200 Subject: [PATCH 3/5] =?UTF-8?q?Keep=20the=20caret=20in=20the=20FILTER=20bo?= =?UTF-8?q?x,=20and=20add=20an=20=C3=97=20to=20clear=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enter re-rendered the pane, so the box the reader had just typed into was gone and the caret went with it — the same thing the JSONPath box did before DOC-7028. The reply now puts the caret back where it was. The × appears only while a filter is in force and drops it in one click, the way emptying the box by hand and pressing Enter does in two. Co-Authored-By: Claude Opus 5 --- static/css/redis-workbench.css | 15 ++++++++++++++ static/js/redis-workbench.js | 37 +++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/static/css/redis-workbench.css b/static/css/redis-workbench.css index e11b7e3805..889c785fed 100644 --- a/static/css/redis-workbench.css +++ b/static/css/redis-workbench.css @@ -1189,6 +1189,21 @@ body.rwb-dragging-y * { .rwb-vsim-filter::placeholder { color: var(--rwb-muted); } +/* The × that drops the filter. As tall as Search beside it — .rwb-btn's vertical + padding kept, its horizontal padding dropped, and the glyph set larger inside + that same box rather than growing it. Square by taking its width from that + height, so it reads as one button rather than a short wide one. */ +.rwb-vsim-clear { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 25px; /* the height this box comes to: 17px + 2 × 0.25rem */ + padding: 0.25rem 0; + font-size: 15px; + line-height: 17px; +} + .rwb-vsim-filter:focus-visible { outline: 2px solid var(--rwb-focus); outline-offset: 1px; diff --git a/static/js/redis-workbench.js b/static/js/redis-workbench.js index 91b9ecc7cf..8a85812661 100644 --- a/static/js/redis-workbench.js +++ b/static/js/redis-workbench.js @@ -2302,11 +2302,30 @@ var filter = el('input', 'rwb-vsim-filter'); filter.type = 'text'; filter.value = search.filter; + filter.setAttribute('spellcheck', 'false'); filter.placeholder = 'FILTER, e.g. .year > 2000'; filter.title = 'An expression over element attributes; see Filtered search'; filter.setAttribute('aria-label', 'Filter expression'); row.appendChild(filter); + /* One click back to the unfiltered neighbours. Only while a filter is in + force: clearing an empty box is a control that does nothing. Emptying the + box by hand and pressing Enter does the same thing — this saves the two + steps. */ + if (search.filter) { + var clear = el('button', 'rwb-btn rwb-vsim-clear', '\u00d7'); + clear.type = 'button'; + clear.title = 'Clear the filter and show every neighbour'; + clear.setAttribute('aria-label', 'Clear the filter'); + clear.addEventListener('click', function () { + self.vsimOptions().filter = ''; + self.openVectorElement(key, element).then(function () { + self.focusVsimFilter(0); + }); + }); + row.appendChild(clear); + } + var go = el('button', 'rwb-btn', 'Search'); go.type = 'submit'; go.title = 'Run VSIM with this filter'; @@ -2314,12 +2333,28 @@ row.addEventListener('submit', function (event) { event.preventDefault(); + /* Where the caret was, to put it back in the box that replaces this one: + the reply re-renders the whole pane, so the box the reader typed into is + gone by the time the neighbours are up. */ + var caret = filter.selectionStart; self.vsimOptions().filter = filter.value.trim(); - self.openVectorElement(key, element); + self.openVectorElement(key, element).then(function () { + self.focusVsimFilter(caret); + }); }); return row; }; + dock.focusVsimFilter = function (caret) { + if (!this.valuePane) return; + var box = this.valuePane.querySelector('.rwb-vsim-filter'); + if (!box) return; + box.focus({ preventScroll: true }); + var at = typeof caret === 'number' ? Math.min(caret, box.value.length) + : box.value.length; + box.setSelectionRange(at, at); + }; + /* One bar per row, in the order given — both callers ask the server to sort, so the bars only have to be read, not sorted. No axis: the numbers are printed beside them and the heading says the unit. From fdbe87259c4fe7462a114dffcbd629605a3096e9 Mon Sep 17 00:00:00 2001 From: paoloredis Date: Fri, 4 Sep 2026 16:24:44 +0200 Subject: [PATCH 4/5] Two review findings in the neighbours list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A filter can leave the element itself out of its own VSIM reply — failing the expression, or having no attributes at all, is enough. The command asks for one more than the chip on the assumption that first row is the element, so without it the list showed one neighbour more than was asked for. Trimmed when the element is absent. A count chip only wrote the count, so the pane it rebuilt filled the filter box from the last submitted filter and dropped whatever had been typed since. The chip now searches with the box as it stands, which is what Search does. Co-Authored-By: Claude Opus 5 --- static/js/redis-workbench.js | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/static/js/redis-workbench.js b/static/js/redis-workbench.js index 8a85812661..3501f52c3c 100644 --- a/static/js/redis-workbench.js +++ b/static/js/redis-workbench.js @@ -2270,7 +2270,8 @@ reader actually explores an embedding space. */ pane.appendChild(el('div', 'rwb-group', 'Nearest')); pane.appendChild(this.vsimControls(key, element, detail.search)); - pane.appendChild(this.renderNeighbours(key, element, detail.neighbours)); + pane.appendChild(this.renderNeighbours(key, element, detail.neighbours, + detail.search)); pane.appendChild(ranNote(detail.commands)); }; @@ -2282,6 +2283,16 @@ var self = this; var row = el('form', 'rwb-vsim-controls'); + /* Built before the chips, which read it: a chip is another way to run this + search, so it takes the filter as typed rather than as last submitted. */ + var filter = el('input', 'rwb-vsim-filter'); + filter.type = 'text'; + filter.value = search.filter; + filter.setAttribute('spellcheck', 'false'); + filter.placeholder = 'FILTER, e.g. .year > 2000'; + filter.title = 'An expression over element attributes; see Filtered search'; + filter.setAttribute('aria-label', 'Filter expression'); + var counts = el('div', 'rwb-vsim-counts'); counts.setAttribute('role', 'group'); counts.setAttribute('aria-label', 'How many neighbours'); @@ -2292,20 +2303,15 @@ choice.title = 'Return the ' + count + ' nearest elements'; choice.setAttribute('aria-pressed', count === search.count ? 'true' : 'false'); choice.addEventListener('click', function () { + /* The reply rebuilds this row from what was searched with, so a filter + the reader had typed but not run would be wiped by the new box. */ + self.vsimOptions().filter = filter.value.trim(); self.vsimOptions().count = count; self.openVectorElement(key, element); }); counts.appendChild(choice); }); row.appendChild(counts); - - var filter = el('input', 'rwb-vsim-filter'); - filter.type = 'text'; - filter.value = search.filter; - filter.setAttribute('spellcheck', 'false'); - filter.placeholder = 'FILTER, e.g. .year > 2000'; - filter.title = 'An expression over element attributes; see Filtered search'; - filter.setAttribute('aria-label', 'Filter expression'); row.appendChild(filter); /* One click back to the unfiltered neighbours. Only while a filter is in @@ -2390,7 +2396,7 @@ /* VSIM's answer. Scores run 1 (identical) to 0 (opposite), so a score is already the share of the bar. */ - dock.renderNeighbours = function (key, element, reply) { + dock.renderNeighbours = function (key, element, reply, search) { var self = this; if (reply && reply.error) { return el('p', 'rwb-text rwb-failed', '(error) ' + cellText(reply.value)); @@ -2398,6 +2404,11 @@ var rows = pairs(ok(reply)).map(function (pair) { return { name: cellText(pair[0]), score: Number(cellText(pair[1])) }; }); + /* One more than the chip was asked for, because the element itself normally + comes back first. A filter it fails — no attributes at all is enough — + leaves it out, and then that spare row is a neighbour nobody asked for. */ + var mine = rows.some(function (row) { return row.name === element; }); + if (!mine && search) rows = rows.slice(0, search.count); if (!rows.length) { return el('p', 'rwb-empty', 'Nothing came back — no element passes this filter.'); } From bf70bf40712b0c1e372044a70db235ed470f14cd Mon Sep 17 00:00:00 2001 From: paoloredis Date: Mon, 7 Sep 2026 14:18:57 +0200 Subject: [PATCH 5/5] Keep a FILTER with the set it was written for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The search was held on the dock with no key attached, so opening another vector set ran VSIM with the previous FILTER. An expression over one set's attributes means nothing to another's — .side on a set of years matches nothing — so the reader got "no element passes this filter" on a set they had only just opened, against a filter they never typed for it. The filter now belongs to the key. How many neighbours to return still follows the reader from set to set: that is a preference about the list, not something written against the data. Co-Authored-By: Claude Opus 5 --- static/js/redis-workbench.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/static/js/redis-workbench.js b/static/js/redis-workbench.js index 3501f52c3c..e26f1a0b8b 100644 --- a/static/js/redis-workbench.js +++ b/static/js/redis-workbench.js @@ -2187,7 +2187,7 @@ re-renders whatever is selected — which used to mean the element view was replaced by the key's own the moment the reader typed anything. */ this.openElement = { name: key.name, element: element }; - var search = this.vsimOptions(); + var search = this.vsimOptions(key.name); var commands = [ 'VEMB ' + quote(key.name) + ' ' + quote(element), 'VGETATTR ' + quote(key.name) + ' ' + quote(element), @@ -2214,8 +2214,17 @@ element keeps the search the reader set up. */ var VSIM_COUNTS = [5, 10, 25]; - dock.vsimOptions = function () { - if (!this.vsim) this.vsim = { count: VSIM_COUNTS[0], filter: '' }; + dock.vsimOptions = function (name) { + if (!this.vsim) this.vsim = { name: name, count: VSIM_COUNTS[0], filter: '' }; + /* A filter is written against one set's attributes: `.year > 2000` means + nothing to a set whose elements carry `.side`, and carried over it hid + every neighbour on a set the reader had only just opened. How many to + return is a preference about the list rather than about the data, so that + one follows the reader from set to set. */ + if (this.vsim.name !== name) { + this.vsim.name = name; + this.vsim.filter = ''; + } return this.vsim; }; @@ -2305,8 +2314,8 @@ choice.addEventListener('click', function () { /* The reply rebuilds this row from what was searched with, so a filter the reader had typed but not run would be wiped by the new box. */ - self.vsimOptions().filter = filter.value.trim(); - self.vsimOptions().count = count; + self.vsimOptions(key.name).filter = filter.value.trim(); + self.vsimOptions(key.name).count = count; self.openVectorElement(key, element); }); counts.appendChild(choice); @@ -2324,7 +2333,7 @@ clear.title = 'Clear the filter and show every neighbour'; clear.setAttribute('aria-label', 'Clear the filter'); clear.addEventListener('click', function () { - self.vsimOptions().filter = ''; + self.vsimOptions(key.name).filter = ''; self.openVectorElement(key, element).then(function () { self.focusVsimFilter(0); }); @@ -2343,7 +2352,7 @@ the reply re-renders the whole pane, so the box the reader typed into is gone by the time the neighbours are up. */ var caret = filter.selectionStart; - self.vsimOptions().filter = filter.value.trim(); + self.vsimOptions(key.name).filter = filter.value.trim(); self.openVectorElement(key, element).then(function () { self.focusVsimFilter(caret); });