From e54647a0dc359c61616dc507f9cf85dc61bcca9b Mon Sep 17 00:00:00 2001 From: paoloredis Date: Wed, 2 Sep 2026 15:03:29 +0200 Subject: [PATCH 1/7] Add a JSONPath box to the workbench's JSON view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /develop/data-types/json/path is a page of JSONPath syntax with nowhere to try it: a reader can read about `$..name` and `$.list[*]` and then has to go and build a JSON.GET by hand. Opening a JSON key in the workbench showed the whole document and nothing else. So the path that produced what is on screen is now editable. Enter runs JSON.GET at that path and the value below is the answer, with the number of matches beside it — JSONPath replies with an array of everything it matched, which is the thing readers get wrong first. Wrong paths are the point, not an accident: a path matching nothing answers `[]` and a path that does not parse answers with the server's error, shown in red rather than swallowed. That is how the syntax gets learned. The box starts empty rather than prefilled with `$`. The root is already on screen, so a prefilled path is a control that does nothing, and a lone "$" in a box reads as an empty one; the placeholder carries the syntax instead. quote() now prefers single quotes. redis-cli takes a single-quoted token literally, so a path keeps its `$` and its brackets as written — with double quotes this escaped the `$` like a shell and the command under the value read `"\$[?(@.a==1)]"`: correct, and not what anyone would type. Double quotes stay as the fallback for a value containing a quote. Verified against a live sandbox: the box appears only for ReJSON-RL keys, reads a field, counts matches, reports a bad path in red, and shows `JSON.GET bike '$.colors[?(@=="black")]'` unescaped for copying. Co-Authored-By: Claude Opus 5 --- static/css/redis-workbench.css | 45 ++++++++++++++++++ static/js/redis-workbench.js | 83 +++++++++++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/static/css/redis-workbench.css b/static/css/redis-workbench.css index 36ca1466ab..b3396342bf 100644 --- a/static/css/redis-workbench.css +++ b/static/css/redis-workbench.css @@ -1132,6 +1132,51 @@ body.rwb-dragging-y * { /* ---- value view ---- */ +/* The path box over a JSON document: the query that produced what is below. Laid + out as a control strip rather than a label jammed against a field — a single + "$" in a cramped box reads as an empty one. */ +.rwb-path { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0 0.5rem 0.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--rwb-line-soft); +} + +.rwb-path-label { + flex: none; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--rwb-muted); +} + +.rwb-path-input { + 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-path-input::placeholder { color: var(--rwb-muted); } + +.rwb-path-input:focus-visible { + outline: 2px solid var(--rwb-focus); + outline-offset: 1px; +} + +.rwb-path-run { flex: none; } + +/* A read that failed, said in the colour of a failure. */ +.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..5f86fe8fdf 100644 --- a/static/js/redis-workbench.js +++ b/static/js/redis-workbench.js @@ -214,6 +214,13 @@ function quote(arg) { var value = String(arg); if (/^[A-Za-z0-9_:.@\-+*$#\/{}\[\]]+$/.test(value)) return value; + /* Single quotes where they will do. redis-cli treats a single-quoted token as + literal, so a JSONPath keeps its `$` and its brackets as written — where + double quotes made this escape the `$` like a shell, and the command shown + under a value read `"\$[?(@.a==1)]"`: correct, and not what anyone would + type. Double quotes remain the fallback for a value with a quote of its + own, which is the case single quotes cannot carry. */ + if (value.indexOf("'") === -1) return "'" + value + "'"; return '"' + value.replace(/([\\"$`])/g, '\\$1') + '"'; } @@ -867,6 +874,7 @@ indexDocs: null, /* Page setups already run in this sandbox session, by name. */ setupRan: {}, + jsonPath: null, selected: null, truncated: false, /* command batches seen while closed, discovered at first open */ @@ -2245,6 +2253,70 @@ pane.appendChild(ranNote(detail.commands)); }; + /* "Path: $" over a JSON document, and what it matched underneath. Enter runs + it; the root is what openKey already showed, so an untouched box changes + nothing. */ + dock.jsonPathRow = function (key) { + var self = this; + var row = el('form', 'rwb-path'); + row.appendChild(el('label', 'rwb-path-label', 'Path')); + var input = el('input', 'rwb-path-input'); + input.type = 'text'; + /* Empty, not "$": the root is already on screen, so a prefilled path is a + control that does nothing. The placeholder says what to type. */ + input.value = this.jsonPath && this.jsonPath.name === key.name + ? this.jsonPath.path : ''; + input.setAttribute('spellcheck', 'false'); + input.setAttribute('aria-label', 'JSONPath to read from ' + key.name); + input.placeholder = '$.field, $.list[*], $..name'; + row.appendChild(input); + var go = el('button', 'rwb-btn rwb-path-run', 'Run'); + go.type = 'submit'; + go.title = 'Read this path with JSON.GET'; + row.appendChild(go); + row.addEventListener('submit', function (event) { + event.preventDefault(); + self.readJsonPath(key, input.value.trim()); + }); + return row; + }; + + /* JSON.GET at a path. A path that matches nothing answers with an empty array + and a path that does not parse answers with an error, and both are worth + seeing: getting them wrong is how the syntax is learned. */ + dock.readJsonPath = function (key, path) { + var self = this; + /* Remembered as typed, so an empty box stays empty when this redraws; the + root is what gets read either way. */ + this.jsonPath = { name: key.name, path: path }; + var command = 'JSON.GET ' + quote(key.name) + ' ' + quote(path || '$'); + this.begin('reading ' + (path || '$') + '…'); + return run([command]).then(function (replies) { + var reply = replies[0]; + var view; + if (reply && reply.error) { + view = { kind: 'text', mono: true, failed: true, + text: '(error) ' + cellText(reply.value) }; + } else { + var raw = ok(reply); + var text = typeof raw === 'string' ? raw : cellText(raw); + var matches = null; + try { + var parsed = JSON.parse(text); + text = JSON.stringify(parsed, null, 2); + if (Array.isArray(parsed)) matches = parsed.length; + } catch (err) { /* not parseable: show it as returned */ } + view = { kind: 'text', text: text, mono: true, + facts: matches === null ? [] : [{ + text: plural(matches, 'match', 'matches'), + title: 'JSONPath answers with an array of everything it matched' + }] }; + } + self.renderValue(key, { commands: [command], view: view }); + self.end(); + }, function () { self.end(); }); + }; + dock.openIndex = function (name) { var self = this; this.selected = name; @@ -2431,6 +2503,14 @@ head.appendChild(facts); pane.appendChild(head); + /* A JSON document is the one value a reader is expected to *query* rather + than read: /develop/data-types/json/path is a page of JSONPath syntax with + nowhere to try it. So the path that produced what is shown is editable, and + running it is what redraws the value below. */ + if (key.type === 'ReJSON-RL') { + pane.appendChild(this.jsonPathRow(key)); + } + if (!detail.view) { pane.appendChild(el('p', 'rwb-empty', 'This type stores no enumerable value, so there is nothing to preview.')); @@ -2450,7 +2530,8 @@ function renderView(view, onOpenRow) { if (view.kind === 'text') { - return el('pre', 'rwb-text' + (view.mono ? ' rwb-json' : ''), view.text); + return el('pre', 'rwb-text' + (view.mono ? ' rwb-json' : '') + + (view.failed ? ' rwb-failed' : ''), view.text); } if (view.kind === 'table') { return renderTable(view.head, view.rows, onOpenRow); From 2e1631d52ed8492cf1d897e1fa85f16c3ab15a6f Mon Sep 17 00:00:00 2001 From: paoloredis Date: Thu, 3 Sep 2026 16:20:57 +0200 Subject: [PATCH 2/7] Two review findings on the JSONPath box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Path box desynced from the value. jsonPath is restored whenever that key is drawn, but openKey re-read the document at the root, so after a sweep — any command re-renders whatever is open — the box said `$.model` while the value and the "Read with" line were the whole document. openKey now re-reads at the reader's path when there is one for that key, which is what the vector-set element view already does: a sweep keeps showing what the reader had open rather than throwing it away. A different key still starts with an empty box. The error colour never applied. A failed read is `.rwb-text .rwb-json .rwb-failed` on one
, and .rwb-json sets cyan 120 lines further
down the file — same specificity, later wins — so "(error) …" rendered as
a JSON value. Compounded the selector as .rwb-text.rwb-failed, which wins
on specificity rather than on order.

Verified: the error is rgb(252, 165, 165), and a command run while a path
is open leaves the box, the value and the command line all naming that
path.

Co-Authored-By: Claude Opus 5 
---
 static/css/redis-workbench.css | 6 ++++--
 static/js/redis-workbench.js   | 9 +++++++++
 2 files changed, 13 insertions(+), 2 deletions(-)

diff --git a/static/css/redis-workbench.css b/static/css/redis-workbench.css
index b3396342bf..83e04e15f8 100644
--- a/static/css/redis-workbench.css
+++ b/static/css/redis-workbench.css
@@ -1174,8 +1174,10 @@ body.rwb-dragging-y * {
 
 .rwb-path-run { flex: none; }
 
-/* A read that failed, said in the colour of a failure. */
-.rwb-failed { color: #fca5a5; }                   /* red-300 */
+/* A read that failed, said in the colour of a failure. Compounded with
+   .rwb-text, because a failed JSONPath read is also .rwb-json — which sets cyan,
+   comes later in this file and, at equal specificity, was winning. */
+.rwb-text.rwb-failed { color: #fca5a5; }          /* red-300 */
 
 .rwb-value-head {
   display: flex;
diff --git a/static/js/redis-workbench.js b/static/js/redis-workbench.js
index 5f86fe8fdf..8e3f569d53 100644
--- a/static/js/redis-workbench.js
+++ b/static/js/redis-workbench.js
@@ -2148,6 +2148,15 @@
     this.selected = name;
     this.renderKeys();
 
+    /* A JSON document the reader has queried is re-read at their path, not at the
+       root. This runs on every sweep — any command re-renders whatever is open —
+       and reading the root while the Path box still showed `$.model` left the box,
+       the value and the "Read with" line disagreeing about what was on screen. */
+    var queried = this.jsonPath;
+    if (key.type === 'ReJSON-RL' && queried && queried.name === name && queried.path) {
+      return this.readJsonPath(key, queried.path);
+    }
+
     var probe = valueProbe(name, key.type, key.size);
     /* Neither MEMORY USAGE nor OBJECT ENCODING. Both describe what is actually
        stored, and what is actually stored here is not what a reader would have on

From 1cc82c478a6173dcca71a0151d29519ed8059f32 Mon Sep 17 00:00:00 2001
From: paoloredis 
Date: Thu, 3 Sep 2026 17:04:56 +0200
Subject: [PATCH 3/7] =?UTF-8?q?Add=20an=20=C3=97=20to=20drop=20the=20JSONP?=
 =?UTF-8?q?ath?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Review: clearing the filter took three steps — select the text, delete
it, then Enter or Run. Now one click on an × beside the field empties the
box and reads the whole document back.

Shown only while a path is in force. An × over an empty box would be a
control that does nothing, which is the same reason the box starts empty
rather than prefilled with `$`.

As tall as Run next to it: .rwb-btn's vertical padding and a 17px line
box are what make that 25px, so the glyph is set larger inside the same
box rather than being allowed to grow it.

Verified: the × appears with a path and goes away without one, one click
restores the root document and the `JSON.GET bike $` line under it, and a
sweep re-reads the path with its × intact.

Co-Authored-By: Claude Opus 5 
---
 static/css/redis-workbench.css | 17 +++++++++++++++++
 static/js/redis-workbench.js   | 15 +++++++++++++++
 2 files changed, 32 insertions(+)

diff --git a/static/css/redis-workbench.css b/static/css/redis-workbench.css
index 83e04e15f8..206aec4ec3 100644
--- a/static/css/redis-workbench.css
+++ b/static/css/redis-workbench.css
@@ -1174,6 +1174,23 @@ body.rwb-dragging-y * {
 
 .rwb-path-run { flex: none; }
 
+/* The × that drops the path. As tall as Run beside it — the padding that makes
+   .rwb-btn that height assumes a line of text, and a single glyph on line-height
+   1 came up short. Squared off by matching the width to the height. */
+.rwb-path-clear {
+  flex: none;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 1.75rem;
+  /* .rwb-btn's vertical padding kept, its horizontal padding dropped for the
+     square: that padding plus this line box is what makes Run 25px, and the
+     glyph is set larger inside the same box rather than growing it. */
+  padding: 0.25rem 0;
+  font-size: 15px;
+  line-height: 17px;
+}
+
 /* A read that failed, said in the colour of a failure. Compounded with
    .rwb-text, because a failed JSONPath read is also .rwb-json — which sets cyan,
    comes later in this file and, at equal specificity, was winning. */
diff --git a/static/js/redis-workbench.js b/static/js/redis-workbench.js
index 8e3f569d53..37691bc8e8 100644
--- a/static/js/redis-workbench.js
+++ b/static/js/redis-workbench.js
@@ -2279,6 +2279,21 @@
     input.setAttribute('aria-label', 'JSONPath to read from ' + key.name);
     input.placeholder = '$.field, $.list[*], $..name';
     row.appendChild(input);
+    /* One click back to the whole document. Only while a path is in force:
+       clearing an empty box is a control that does nothing, and this row already
+       leaves out what it cannot act on. Emptying the box by hand and pressing
+       Enter does the same thing — this saves the two steps. */
+    if (input.value) {
+      var clear = el('button', 'rwb-btn rwb-path-clear', '\u00d7');
+      clear.type = 'button';
+      clear.title = 'Clear the path and show the whole document';
+      clear.setAttribute('aria-label', 'Clear the path');
+      clear.addEventListener('click', function () {
+        input.value = '';
+        self.readJsonPath(key, '');
+      });
+      row.appendChild(clear);
+    }
     var go = el('button', 'rwb-btn rwb-path-run', 'Run');
     go.type = 'submit';
     go.title = 'Read this path with JSON.GET';

From ee8a8855e41dd5b4b4de497e71b369b94535d662 Mon Sep 17 00:00:00 2001
From: paoloredis 
Date: Thu, 3 Sep 2026 17:11:44 +0200
Subject: [PATCH 4/7] Stay in the Path box after running a path
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reading a path redraws the value pane, so the box the reader pressed
Enter in no longer exists by the time the answer is on screen and focus
falls back to the document. Trying a path is usually trying several —
`$.a`, then `$.a[0]`, then `$..a` — and each one meant clicking back into
the box first.

Focus returns to the box that replaced it, caret where they left it, so
editing continues straight away. The × does the same, into an empty box.

Only after a read the reader asked for. The same redraw runs on every
sweep, and taking the keyboard because a command finished in the terminal
would be worse than the problem.

Verified: Enter leaves focus in the box with the caret at 7 of `$.brand`,
a second path can be typed and run without touching the mouse, the ×
leaves an empty focused box, and a command run in the terminal leaves
focus in the terminal.

Co-Authored-By: Claude Opus 5 
---
 static/js/redis-workbench.js | 27 +++++++++++++++++++++++++--
 1 file changed, 25 insertions(+), 2 deletions(-)

diff --git a/static/js/redis-workbench.js b/static/js/redis-workbench.js
index 37691bc8e8..e1b971473e 100644
--- a/static/js/redis-workbench.js
+++ b/static/js/redis-workbench.js
@@ -2290,7 +2290,7 @@
       clear.setAttribute('aria-label', 'Clear the path');
       clear.addEventListener('click', function () {
         input.value = '';
-        self.readJsonPath(key, '');
+        self.readJsonPath(key, '').then(function () { self.focusJsonPath(0); });
       });
       row.appendChild(clear);
     }
@@ -2300,11 +2300,34 @@
     row.appendChild(go);
     row.addEventListener('submit', function (event) {
       event.preventDefault();
-      self.readJsonPath(key, input.value.trim());
+      /* Where the caret was, to put it back in the box that replaces this one. */
+      var caret = input.selectionStart;
+      self.readJsonPath(key, input.value.trim())
+        .then(function () { self.focusJsonPath(caret); });
     });
     return row;
   };
 
+  /* Back in the Path box after a read, caret where the reader left it.
+
+     Reading redraws the whole value pane, so the box they pressed Enter in is
+     gone by the time the answer is on screen and focus has fallen back to the
+     document. Trying a path is usually trying several — `$.a`, then `$.a[0]`,
+     then `$..a` — and each one meant clicking back into the box first.
+
+     Only after a read the reader asked for. The same redraw runs on every sweep,
+     and grabbing focus because a command finished elsewhere would take the
+     keyboard away from whatever they were doing. */
+  dock.focusJsonPath = function (caret) {
+    if (!this.valuePane) return;
+    var box = this.valuePane.querySelector('.rwb-path-input');
+    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);
+  };
+
   /* JSON.GET at a path. A path that matches nothing answers with an empty array
      and a path that does not parse answers with an error, and both are worth
      seeing: getting them wrong is how the syntax is learned. */

From b629d1b8dd5843aadb69d1d0f5442b3896eb9275 Mon Sep 17 00:00:00 2001
From: paoloredis 
Date: Mon, 7 Sep 2026 10:46:03 +0200
Subject: [PATCH 5/7] Forget a path when its key is gone
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

openKey re-reads a JSON document at the path the reader queried, and nothing
dropped that path when the key it was read from went away. A key that came back
under the same name — deleted and rewritten, expired, or recreated by the page
setup after a flush — was then read at a path nobody asked for on it, and the
Path box showed a path the reader had never typed for it.

The path now goes with its key: on a flush, on an expiry the TTL ticker notices,
and on any sweep that finds the key no longer there. A living key keeps its
path, which is what the re-read is for.

Co-Authored-By: Claude Opus 5 
---
 static/js/redis-workbench.js | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/static/js/redis-workbench.js b/static/js/redis-workbench.js
index e1b971473e..08be43009f 100644
--- a/static/js/redis-workbench.js
+++ b/static/js/redis-workbench.js
@@ -1614,6 +1614,7 @@
     this.truncated = false;
     this.expiredName = null;
     this.openElement = null;
+    this.jsonPath = null;
     /* Nothing left to filter by. */
     this.indexFilter = null;
     this.indexDocs = null;
@@ -1801,6 +1802,7 @@
       self.keys = result.keys;
       self.indexes = result.indexes;
       self.indexDocs = result.docs;
+      self.forgetGonePath();
       /* An index the reader is looking at may hold documents the dock never saw
          a command touch — written before it was open, or by the page's own
          inline terminals. Adopt them, so the filtered list is the index's
@@ -1933,6 +1935,7 @@
     this.keys = this.keys.filter(function (key) {
       return expired.indexOf(key.name) === -1;
     });
+    this.forgetGonePath();
     if (this.selected && expired.indexOf(this.selected) >= 0) {
       /* Recorded rather than written straight into the panel: the sweep that
          follows re-renders, and renderKeys() resets an unselected value column —
@@ -2139,6 +2142,14 @@
 
   /* `quiet` is gone: it used to suppress switching to the Value tab, and there is
      no tab to switch to now that the value has a column of its own. */
+  /* A path belongs to the key it was read from. When that key goes — deleted,
+     expired, flushed — the path goes with it: a key that comes back under the
+     same name is a different document, and openKey would otherwise read it at a
+     path the reader never asked for and show the empty reply that follows. */
+  dock.forgetGonePath = function () {
+    if (this.jsonPath && !this.find(this.jsonPath.name)) this.jsonPath = null;
+  };
+
   dock.openKey = function (name) {
     var self = this;
     var key = this.find(name);

From 5f393b9648745e1594eca52f8ae7cdc5b22d4cc1 Mon Sep 17 00:00:00 2001
From: paoloredis 
Date: Mon, 7 Sep 2026 11:23:19 +0200
Subject: [PATCH 6/7] Two review findings in the Path box
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A path being typed no longer disappears when the pane is redrawn. Any command
on the page starts a sweep, the sweep re-reads whatever is open, and the box
was rebuilt from the last path that was run — so a half-written path was
emptied out mid-word. The box now keeps what is in it, and the caret goes back
only if it was there, since running a command puts it in the terminal.

An empty path is handed back to openKey rather than read as "$". Reading the
root through here produced the same document the key's own view shows, plus a
"1 match" fact that view has no reason to show — a chip that then vanished on
the next sweep.

The reviewer also called an older reply landing last. It cannot: the widget
sends every batch through one promise chain (executeQueue in cli.js), so the
second read is not sent until the first has answered. Held a JSON.GET back for
four seconds to check, and the pane ended on the newer path both times.

Co-Authored-By: Claude Opus 5 
---
 static/js/redis-workbench.js | 46 +++++++++++++++++++++++++++++++-----
 1 file changed, 40 insertions(+), 6 deletions(-)

diff --git a/static/js/redis-workbench.js b/static/js/redis-workbench.js
index 08be43009f..7e9ce820e1 100644
--- a/static/js/redis-workbench.js
+++ b/static/js/redis-workbench.js
@@ -2282,10 +2282,22 @@
     row.appendChild(el('label', 'rwb-path-label', 'Path'));
     var input = el('input', 'rwb-path-input');
     input.type = 'text';
+    input.dataset.rwbKey = key.name;
     /* Empty, not "$": the root is already on screen, so a prefilled path is a
        control that does nothing. The placeholder says what to type. */
-    input.value = this.jsonPath && this.jsonPath.name === key.name
+    var applied = this.jsonPath && this.jsonPath.name === key.name
       ? this.jsonPath.path : '';
+    /* A path the reader was still typing when this pane was redrawn outranks the
+       one that was last run: they are mid-word, and the value below already says
+       which path produced it. */
+    var draft = this.pathDraft;
+    this.pathDraft = null;
+    if (draft && draft.name === key.name && draft.text !== applied) {
+      input.value = draft.text;
+      if (draft.focused) this.pathCarried = draft;
+    } else {
+      input.value = applied;
+    }
     input.setAttribute('spellcheck', 'false');
     input.setAttribute('aria-label', 'JSONPath to read from ' + key.name);
     input.placeholder = '$.field, $.list[*], $..name';
@@ -2294,7 +2306,7 @@
        clearing an empty box is a control that does nothing, and this row already
        leaves out what it cannot act on. Emptying the box by hand and pressing
        Enter does the same thing — this saves the two steps. */
-    if (input.value) {
+    if (applied) {
       var clear = el('button', 'rwb-btn rwb-path-clear', '\u00d7');
       clear.type = 'button';
       clear.title = 'Clear the path and show the whole document';
@@ -2344,11 +2356,17 @@
      seeing: getting them wrong is how the syntax is learned. */
   dock.readJsonPath = function (key, path) {
     var self = this;
-    /* Remembered as typed, so an empty box stays empty when this redraws; the
-       root is what gets read either way. */
+    /* An empty box is not a path: the reader asked for the document back, and
+       that is the key's own view. Reading "$" here instead produced the same
+       document with a "1 match" fact the key's view has no reason to show — and
+       so a chip that vanished on the next sweep. */
+    if (!path) {
+      this.jsonPath = null;
+      return Promise.resolve(this.openKey(key.name));
+    }
     this.jsonPath = { name: key.name, path: path };
-    var command = 'JSON.GET ' + quote(key.name) + ' ' + quote(path || '$');
-    this.begin('reading ' + (path || '$') + '…');
+    var command = 'JSON.GET ' + quote(key.name) + ' ' + quote(path);
+    this.begin('reading ' + path + '…');
     return run([command]).then(function (replies) {
       var reply = replies[0];
       var view;
@@ -2535,6 +2553,17 @@
     var self = this;
     var meta = TYPES[key.type] || { label: key.type, tone: 'other' };
     var pane = this.valuePane;
+    /* What is in the Path box, if there is a box: this pane is redrawn on every
+       sweep, and any command on the page starts one — so a half-written path was
+       emptied out from under the reader. A text box keeps what was typed in it
+       until something is done with it, and this one is no different. Whether it
+       had focus is recorded too: the caret only goes back if it was already
+       there, since running a command puts it in the terminal instead. */
+    var typing = pane.querySelector('.rwb-path-input');
+    this.pathDraft = typing
+      ? { name: typing.dataset.rwbKey, text: typing.value,
+          caret: typing.selectionStart, focused: document.activeElement === typing }
+      : null;
     pane.replaceChildren();
 
     var head = el('div', 'rwb-value-head');
@@ -2584,6 +2613,11 @@
       }
     }
     if (detail.commands.length) pane.appendChild(ranNote(detail.commands));
+    if (this.pathCarried) {
+      var caret = this.pathCarried.caret;
+      this.pathCarried = null;
+      this.focusJsonPath(caret);
+    }
   };
 
   function renderView(view, onOpenRow) {

From 18bd80a380045c3e8d7b13bdfa1a8f62487c9e67 Mon Sep 17 00:00:00 2001
From: paoloredis 
Date: Mon, 7 Sep 2026 11:53:37 +0200
Subject: [PATCH 7/7] Two more review findings in the Path box
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Only a JSONPath gets a match count. A legacy path — no leading $ — answers with
the value at one place, so a path landing on an array field was reporting that
array's length as matches, with a tooltip about JSONPath, right next to the
distinction this box exists to teach.

The caret goes back whenever it was in the box. It was only restored when the
text had changed, so a sweep landing while the reader sat in the box with the
path they had just run moved them out of it.

Co-Authored-By: Claude Opus 5 
---
 static/js/redis-workbench.js | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/static/js/redis-workbench.js b/static/js/redis-workbench.js
index 7e9ce820e1..cb0f8defe8 100644
--- a/static/js/redis-workbench.js
+++ b/static/js/redis-workbench.js
@@ -2294,10 +2294,13 @@
     this.pathDraft = null;
     if (draft && draft.name === key.name && draft.text !== applied) {
       input.value = draft.text;
-      if (draft.focused) this.pathCarried = draft;
     } else {
       input.value = applied;
     }
+    /* The caret goes back whenever it was in the box, whether or not the text
+       changed: a sweep that lands while the reader sits in the box with the path
+       they just ran should not put them somewhere else. */
+    if (draft && draft.name === key.name && draft.focused) this.pathCarried = draft;
     input.setAttribute('spellcheck', 'false');
     input.setAttribute('aria-label', 'JSONPath to read from ' + key.name);
     input.placeholder = '$.field, $.list[*], $..name';
@@ -2380,7 +2383,12 @@
         try {
           var parsed = JSON.parse(text);
           text = JSON.stringify(parsed, null, 2);
-          if (Array.isArray(parsed)) matches = parsed.length;
+          /* Only a JSONPath answers with a list of what it matched. A legacy
+             path — no leading $ — answers with the value at that one place, so
+             counting an array value's members as matches would put the wrong
+             number, and the wrong idea, next to the very distinction this box
+             is here to teach. */
+          if (Array.isArray(parsed) && path.charAt(0) === '$') matches = parsed.length;
         } catch (err) { /* not parseable: show it as returned */ }
         view = { kind: 'text', text: text, mono: true,
           facts: matches === null ? [] : [{