3f97b357c5967755f1e5c914663ce9c7cc385e70
max
  Wed Aug 26 03:01:34 2026 -0700
Share a link: create the session only when the user asks, and copy in one click

Opening the top-right "Share a link" dialog used to save a session right away, so
just looking at the dialog left an unused link in the user's session list.  The
dialog now explains what the link is and offers one "Create link & copy" button
that saves the session and copies the URL in a single step.

The random name for a machine-generated session is now made client-side and sent
with the request, so there is one convention in one place: a leading underscore
(kept verbatim by the short-link encoder, so /s/<user>/_XXXXXXXX stays clean)
plus eight URL-safe characters.  hgSession.c no longer auto-names; a logged-in
save with no name is an error, which is what the callers already guarantee.
refs #10138

diff --git src/hg/hgSession/hgSession.c src/hg/hgSession/hgSession.c
index 56c7a75a81d..f94bfdbb1df 100644
--- src/hg/hgSession/hgSession.c
+++ src/hg/hgSession/hgSession.c
@@ -1037,70 +1037,73 @@
  * sessionName is the human-readable (decoded) name; the client uses it as the rename "old name". */
 {
 struct dyString *dyUrl = dyStringNew(0);
 addSessionLink(dyUrl, encUserName, encSessionName, FALSE, TRUE);
 puts("Content-Type:application/json\n");
 printf("{\"name\": \"%s\", \"url\": \"%s\"}\n",
        jsonStringEscape(sessionName), jsonStringEscape(dyUrl->string));
 dyStringFree(&dyUrl);
 hDisconnectCentral(&conn);
 }
 
 void doSaveSessionJson(char *userName)
 /* AJAX endpoint behind the "Share a link" menu button.  Save the current cart as a named session
  * and print JSON {"name": <session name>, "url": <shareable link>}.  When the user is not logged
  * in (or hgsShareAnon is set), save under the reserved anonymous user "l" with a random token
- * name.  When logged in with no name given, generate a short random name.  Saved shared by link so
- * the link works for anyone.  Reuses saveCartAsSession() and addSessionLink(). */
+ * name.  When logged in, the caller supplies the name (a typed name, or a random internal
+ * "_XXXXXXXX" name generated client-side).  Saved shared by link so the link works for anyone.
+ * Reuses saveCartAsSession() and addSessionLink(). */
 {
 struct sqlConnection *conn = hConnectCentral();
 if (!sqlTableExists(conn, namedSessionTable))
     {
     saveSessionJsonError(conn, "Required session table does not exist in the central database.");
     return;
     }
 
 boolean anon = isEmpty(userName) || cgiBoolean(hgsShareAnon);
 // Read the requested name from the request, not the cart (hgSession's Save form leaves a sticky
 // value in the cart under this same variable that would otherwise shadow ours).
 char *sessionName = trimSpaces(cloneString(cgiUsualString(hgsNewSessionName, "")));
 
 /* Keep our control variables out of the saved session contents and the user's own cart. */
 cartRemove(cart, hgsDoSaveSessionJson);
 cartRemove(cart, hgsShareAnon);
 cartRemove(cart, hgsNewSessionName);
 cartRemove(cart, hgsNewSessionShare);
 
 char *encUserName = NULL;
 char *encSessionName = NULL;
 if (anon)
     {
     encUserName = "l";                    /* reserved anonymous user -> short link /s/l/<token> */
-    sessionName = makeRandomKey(96);      /* 16 URL-safe alphanumeric chars; no encoding needed */
-    encSessionName = sessionName;
+    /* The caller may supply a client-generated token so the Share dialog can show the exact link
+     * before the user commits; fall back to a server-generated token when none is given (e.g. the
+     * hgc BLAT "Share a link" button). */
+    if (isEmpty(sessionName))
+        sessionName = makeRandomKey(96);  /* 16 URL-safe alphanumeric chars */
+    encSessionName = cgiEncodeFull(sessionName);
     }
 else
     {
+    /* Logged-in callers always supply a name: the caller either typed one or generated a random
+     * internal "_XXXXXXXX" name client-side (sessRandomShareName in hgSession.js, shared by the
+     * top-right "Share a link" menu in topLinks.js), so we no longer auto-name here. */
     if (isEmpty(sessionName))
         {
-        /* One-click share: auto-name the session.  "_" is kept verbatim by cgiEncodeFull (unlike
-         * "-"), so the short link /s/<user>/<name> stays clean. */
-        char randName[32];
-        char *rk = makeRandomKey(48);     /* 8 URL-safe alphanumeric chars */
-        safef(randName, sizeof randName, "share_%s", rk);
-        freeMem(rk);
-        sessionName = cloneString(randName);
+        saveSessionJsonError(conn, "Please provide a name for this session.");
+        return;
         }
     encUserName = cgiEncodeFull(userName);
     encSessionName = cgiEncodeFull(sessionName);
     }
 
 saveCartAsSession(conn, encUserName, encSessionName, 1);  /* shared by link */
 saveSessionJsonResult(conn, encUserName, encSessionName, sessionName);
 }
 
 void doRenameSessionJson(char *userName)
 /* AJAX endpoint for the "Specify name" step of the Share dialog: rename an existing session
  * (hgsOldSessionName -> hgsNewSessionName) under the current user.  Logged-in only.  Rejects a
  * name already in use rather than overwriting it.  Returns {"name","url"} or {"error"}. */
 {
 struct sqlConnection *conn = hConnectCentral();