User1342 commited on
Commit
9bb9cc0
·
1 Parent(s): f5de931

Real setup steps instead of a download that never existed; mobile notice, veil and control fixes

Browse files
APPLY2.bat ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cd /d "%~dp0"
2
+ @echo off
3
+ (
4
+ echo ===== APPLY 2 =====
5
+ powershell -NoProfile -Command "Expand-Archive -LiteralPath distinct-update2.zip -DestinationPath . -Force"
6
+ echo EXPAND_EXIT=%ERRORLEVEL%
7
+ git add -A
8
+ git -c user.name=User1342 -c user.email=User1342@users.noreply.huggingface.co commit -m "Real setup steps instead of a download that never existed; mobile notice, veil and control fixes"
9
+ echo COMMIT_EXIT=%ERRORLEVEL%
10
+ git push origin main
11
+ echo PUSH_EXIT=%ERRORLEVEL%
12
+ git log --oneline -1
13
+ del /q distinct-update2.zip
14
+ ) >> push-log8.txt 2>&1
distinct_server/presentation.py CHANGED
@@ -406,19 +406,29 @@ footer,.gradio-container footer{display:none !important;}
406
  was the loudest colour on it. */
407
  .c-notice{background:var(--c-surface); border:1px solid var(--c-hairline);
408
  border-radius:var(--r-md); padding:var(--s3) var(--s4);}
409
- .c-notice__summary{display:flex; align-items:baseline; gap:var(--s3);
 
 
 
 
 
410
  flex-wrap:wrap; cursor:pointer; list-style:none;
411
  font-size:var(--t-label); color:var(--c-text-2);
412
  line-height:1.5;}
 
413
  .c-notice__summary::-webkit-details-marker{display:none;}
414
  .c-notice__summary strong{color:var(--c-text); font-weight:600;}
415
  .c-notice__mark{flex:none; width:16px; height:16px; border-radius:50%;
416
- align-self:center;
417
  border:1.5px solid var(--c-warn-rule); color:var(--c-warn-rule);
418
  font-size:10px; font-weight:700; display:grid; place-items:center;}
419
  .c-notice__toggle{margin-left:auto; white-space:nowrap; font-size:var(--t-micro);
420
  color:var(--c-muted); text-decoration:underline;
421
  text-underline-offset:2px; padding-left:var(--s3);}
 
 
 
 
422
  .c-notice__body{margin:var(--s3) 0 var(--s1) 28px; font-size:var(--t-micro);
423
  line-height:1.6; color:var(--c-text-2); max-width:var(--measure);}
424
  .c-notice__summary:focus-visible{outline:3px solid var(--c-sage); outline-offset:3px;
@@ -1290,6 +1300,29 @@ details[open] .c-libhelp__toggle::after{content:" -";}
1290
  border-radius:var(--r-md); padding:var(--s3); font-family:var(--mono);
1291
  font-size:11.5px; line-height:1.6; overflow-x:auto; white-space:pre;
1292
  color:var(--c-text-2); user-select:all;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1293
  .c-join__qr{margin-top:var(--s3);}
1294
  .c-join__qr svg{width:160px; height:160px; display:block;
1295
  border:1px solid var(--c-hairline); border-radius:var(--r-md);
@@ -1441,6 +1474,8 @@ details[open] .c-libhelp__toggle::after{content:" -";}
1441
  rather than force the panel wider than the screen. */
1442
  .c-join__cmd{white-space:pre-wrap !important; overflow-wrap:anywhere !important;}
1443
  .c-join__code{overflow-wrap:anywhere;}
 
 
1444
  .c-panel code,.c-panel pre{overflow-wrap:anywhere;}
1445
  .c-join__qr svg{width:128px; height:128px;}
1446
  }
@@ -1625,6 +1660,14 @@ details[open] .c-libhelp__toggle::after{content:" -";}
1625
  .c-veil__panel{pointer-events:auto;}
1626
  .c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-conversation > *:not(.c-veil):not(:first-child){
1627
  opacity:.45; filter:saturate(.7);}
 
 
 
 
 
 
 
 
1628
  .c-veil__panel{max-width:380px; text-align:center; background:var(--c-surface);
1629
  border:1px solid var(--c-hairline); border-radius:var(--r-lg);
1630
  box-shadow:var(--e2); padding:var(--s5);}
@@ -1632,12 +1675,26 @@ details[open] .c-libhelp__toggle::after{content:" -";}
1632
  .c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-veil{
1633
  padding:var(--s5) var(--s4);}
1634
  .c-veil__panel{padding:var(--s4);}
 
 
 
 
 
 
 
1635
  }
1636
  .c-veil__title{margin:0 0 6px; font-weight:650; font-size:var(--t-lg); color:var(--c-text);}
1637
  .c-veil__body{margin:0 0 var(--s4); color:var(--c-text-2); line-height:1.55;}
1638
- .c-veil__action{display:inline-block; text-decoration:none; background:var(--c-accent);
1639
  color:#fff; padding:10px 22px; border-radius:var(--r-md); font-weight:600;}
1640
- .c-veil__action:hover{background:var(--c-accent-hover);}
 
 
 
 
 
 
 
1641
 
1642
  /* ---- Professional polish pass ----------------------------------------- */
1643
  /* Calmer sections: the tail accordions read as pages, not widgets. */
 
406
  was the loudest colour on it. */
407
  .c-notice{background:var(--c-surface); border:1px solid var(--c-hairline);
408
  border-radius:var(--r-md); padding:var(--s3) var(--s4);}
409
+ /* The mark belongs to the sentence, so it must never be able to wrap away
410
+ from it. On a phone the row wrapped into three lines and left a bare "!"
411
+ floating above the text like a rendering fault. The mark and the sentence
412
+ are now one non-breaking pair that shrinks together; only the "What is
413
+ kept" toggle is allowed to drop to its own line. */
414
+ .c-notice__summary{display:flex; align-items:flex-start; gap:var(--s3);
415
  flex-wrap:wrap; cursor:pointer; list-style:none;
416
  font-size:var(--t-label); color:var(--c-text-2);
417
  line-height:1.5;}
418
+ .c-notice__summary>span:nth-child(2){flex:1 1 180px; min-width:0;}
419
  .c-notice__summary::-webkit-details-marker{display:none;}
420
  .c-notice__summary strong{color:var(--c-text); font-weight:600;}
421
  .c-notice__mark{flex:none; width:16px; height:16px; border-radius:50%;
422
+ margin-top:2px;
423
  border:1.5px solid var(--c-warn-rule); color:var(--c-warn-rule);
424
  font-size:10px; font-weight:700; display:grid; place-items:center;}
425
  .c-notice__toggle{margin-left:auto; white-space:nowrap; font-size:var(--t-micro);
426
  color:var(--c-muted); text-decoration:underline;
427
  text-underline-offset:2px; padding-left:var(--s3);}
428
+ @media (max-width:700px){
429
+ /* Its own line, aligned with the sentence it opens rather than the mark. */
430
+ .c-notice__toggle{flex:1 0 100%; margin:var(--s2) 0 0 28px; padding-left:0;}
431
+ }
432
  .c-notice__body{margin:var(--s3) 0 var(--s1) 28px; font-size:var(--t-micro);
433
  line-height:1.6; color:var(--c-text-2); max-width:var(--measure);}
434
  .c-notice__summary:focus-visible{outline:3px solid var(--c-sage); outline-offset:3px;
 
1300
  border-radius:var(--r-md); padding:var(--s3); font-family:var(--mono);
1301
  font-size:11.5px; line-height:1.6; overflow-x:auto; white-space:pre;
1302
  color:var(--c-text-2); user-select:all;}
1303
+ .c-join__note{margin:0; font-size:var(--t-micro); color:var(--c-muted);
1304
+ line-height:1.55; max-width:56ch;}
1305
+ .c-join__note code{font-family:var(--mono); font-size:11px;
1306
+ background:var(--c-fill); border-radius:4px; padding:1px 5px;}
1307
+ .c-join__note .c-join__code{display:inline-block; padding:2px 8px;
1308
+ font-size:11.5px; margin-top:4px;}
1309
+
1310
+ /* SETTING UP A WORKER, AS NUMBERED STEPS.
1311
+ Three things happen in order and each can fail on its own, so each is
1312
+ drawn as its own step with its own command. Told as one paragraph, a
1313
+ volunteer who got half way could not tell which half they were in. */
1314
+ .c-steps-num{list-style:none; margin:0; padding:0; display:flex;
1315
+ flex-direction:column; gap:var(--s5);}
1316
+ .c-step-num{display:flex; gap:var(--s3); align-items:flex-start;}
1317
+ .c-step-num__n{flex:none; width:24px; height:24px; border-radius:50%;
1318
+ display:grid; place-items:center; margin-top:1px;
1319
+ background:var(--c-mint); color:var(--c-sage-deep);
1320
+ font-size:12px; font-weight:700;}
1321
+ .c-step-num__body{flex:1 1 auto; min-width:0; display:flex;
1322
+ flex-direction:column; gap:var(--s2);}
1323
+ .c-step-num__head{margin:0; font-size:var(--t-body); font-weight:650;
1324
+ color:var(--c-text); letter-spacing:-.01em;}
1325
+
1326
  .c-join__qr{margin-top:var(--s3);}
1327
  .c-join__qr svg{width:160px; height:160px; display:block;
1328
  border:1px solid var(--c-hairline); border-radius:var(--r-md);
 
1474
  rather than force the panel wider than the screen. */
1475
  .c-join__cmd{white-space:pre-wrap !important; overflow-wrap:anywhere !important;}
1476
  .c-join__code{overflow-wrap:anywhere;}
1477
+ .c-steps-num{gap:var(--s4);}
1478
+ .c-step-num{gap:var(--s2);}
1479
  .c-panel code,.c-panel pre{overflow-wrap:anywhere;}
1480
  .c-join__qr svg{width:128px; height:128px;}
1481
  }
 
1660
  .c-veil__panel{pointer-events:auto;}
1661
  .c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-conversation > *:not(.c-veil):not(:first-child){
1662
  opacity:.45; filter:saturate(.7);}
1663
+ /* THE VEIL REPLACES THE EMPTY STATE, IT DOES NOT SIT ON TOP OF IT.
1664
+ "No agents available" and "No runs yet · pick a model and a worker" are the
1665
+ same news told twice, and on a phone the panel landed across the second
1666
+ one and cut a sentence in half. While the veil is up the transcript's own
1667
+ placeholder stands down. It is hidden rather than removed, so the column
1668
+ keeps its height and nothing jumps when a worker appears. */
1669
+ .c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-chat .c-empty{
1670
+ visibility:hidden;}
1671
  .c-veil__panel{max-width:380px; text-align:center; background:var(--c-surface);
1672
  border:1px solid var(--c-hairline); border-radius:var(--r-lg);
1673
  box-shadow:var(--e2); padding:var(--s5);}
 
1675
  .c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-veil{
1676
  padding:var(--s5) var(--s4);}
1677
  .c-veil__panel{padding:var(--s4);}
1678
+ /* With the placeholder stood down there is nothing behind the veil, and a
1679
+ transcript sized for a conversation it cannot yet hold left a phone
1680
+ screen of blank card under the panel. It keeps only the height the panel
1681
+ needs until a worker arrives. `!important` because the minimum is an
1682
+ inline style the component writes for itself. */
1683
+ .c-shell:has(.c-identity:not(:empty)):has(.c-noworker-hook) .c-chat{
1684
+ min-height:300px !important; max-height:300px !important;}
1685
  }
1686
  .c-veil__title{margin:0 0 6px; font-weight:650; font-size:var(--t-lg); color:var(--c-text);}
1687
  .c-veil__body{margin:0 0 var(--s4); color:var(--c-text-2); line-height:1.55;}
1688
+ .c-veil__action{display:inline-block; background:var(--c-accent);
1689
  color:#fff; padding:10px 22px; border-radius:var(--r-md); font-weight:600;}
1690
+ .c-veil__action:hover{background:var(--c-accent-hover); color:#fff;}
1691
+ /* A control is a control: the label inside it is never underlined. Gradio's
1692
+ prose styles underline every anchor, which turned the primary action into
1693
+ underlined text on a dark slab and the nav pills into links wearing a
1694
+ button's clothes. */
1695
+ .c-veil__action,.c-veil__action:visited,
1696
+ .c-quicknav__link,.c-quicknav__link:visited,
1697
+ .c-libcard a,.gradio-container button a{text-decoration:none !important;}
1698
 
1699
  /* ---- Professional polish pass ----------------------------------------- */
1700
  /* Calmer sections: the tail accordions read as pages, not widgets. */
distinct_server/render.py CHANGED
@@ -177,13 +177,22 @@ def no_tools_note(notice_text: str) -> str:
177
  )
178
 
179
 
180
- def join_this_server(*, addresses, code: str, qr: str = "", tools: str = "local") -> str:
181
- """What to point an agent at, with the address filled in.
 
 
 
 
 
 
 
 
 
182
 
183
- This panel exists because the previous instruction was a command with
184
- ``SERVER`` written in the middle of it, which asks the reader to go and
185
- find something this process already knows. A server knows its own port and
186
- can look up its own addresses, so it says them.
187
 
188
  ``addresses`` empty is a real state and is reported as one: a server bound
189
  to loopback is reachable by this machine and nothing else, and an agent on
@@ -204,16 +213,41 @@ def join_this_server(*, addresses, code: str, qr: str = "", tools: str = "local"
204
  f'<li class="c-join__alt">{esc(url)}</li>' for url in list(addresses)[1:]
205
  )
206
  alternates = (
207
- f'<p class="c-join__lead">Also reachable at:</p><ul class="c-join__alts">{others}</ul>'
208
  if others
209
  else ""
210
  )
211
- code_line = (
212
- f'<p class="c-join__lead">Pairing code, one use and ten minutes:</p>'
213
- f'<code class="c-join__code">{esc(code)}</code>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  if code
215
- else '<p class="c-join__lead">Generate a pairing code below, then run the '
216
- "command with it.</p>"
217
  )
218
  # One line on purpose. The previous form used backslash continuations,
219
  # which are bash syntax: pasted into PowerShell they became stray
@@ -226,12 +260,22 @@ def join_this_server(*, addresses, code: str, qr: str = "", tools: str = "local"
226
  picture = f'<div class="c-join__qr">{qr}</div>' if qr else ""
227
  return (
228
  '<div class="c-join">'
229
- '<p class="c-join__lead">Point an agent at this address:</p>'
230
- f'<code class="c-join__address">{esc(first)}</code>'
231
- f"{alternates}{code_line}"
232
- f'<p class="c-join__lead">On the machine that will run the model:</p>'
 
 
 
 
 
 
233
  f'<pre class="c-join__cmd">{esc(command)}</pre>'
234
- f"{picture}"
 
 
 
 
235
  "</div>"
236
  )
237
 
 
177
  )
178
 
179
 
180
+ def join_this_server(
181
+ *, addresses, code: str, qr: str = "", tools: str = "local", source: str = ""
182
+ ) -> str:
183
+ """The whole path from nothing to a running worker, in the order it happens.
184
+
185
+ This panel used to be an address, a code and one command, sitting beside a
186
+ "Download agent" button that pointed at release artifacts nobody builds.
187
+ The button could only ever fail, and the command it accompanied assumed
188
+ the code was already on the machine. What a volunteer actually needs is
189
+ the three steps in between, so those are what this says: get the code,
190
+ install it, run it.
191
 
192
+ ``source`` is where the code lives — this Space's own repository when the
193
+ server knows it. Absent, the step says so plainly instead of guessing at
194
+ a URL, because sending somebody to a repository that may not exist is
195
+ worse than admitting the server cannot name one.
196
 
197
  ``addresses`` empty is a real state and is reported as one: a server bound
198
  to loopback is reachable by this machine and nothing else, and an agent on
 
213
  f'<li class="c-join__alt">{esc(url)}</li>' for url in list(addresses)[1:]
214
  )
215
  alternates = (
216
+ f'<p class="c-join__note">Also reachable at {", ".join(esc(str(u)) for u in list(addresses)[1:])}.</p>'
217
  if others
218
  else ""
219
  )
220
+
221
+ # STEP ONE: the code. A git clone, because a Space is a git repository and
222
+ # a clone is the one form that also updates later with `git pull`.
223
+ if source:
224
+ get_it = (
225
+ f'<pre class="c-join__cmd">git clone {esc(source)} distinct</pre>'
226
+ '<p class="c-join__note">Or use "Download repository" on the Files tab '
227
+ "and unzip it. Either way you end up with a folder of Python.</p>"
228
+ )
229
+ else:
230
+ get_it = (
231
+ '<p class="c-join__note">This server does not know its own repository '
232
+ "address. Ask whoever runs it where the code lives.</p>"
233
+ )
234
+
235
+ # STEP TWO: the install. `-e` so the worker runs from the folder that was
236
+ # cloned, which is also the folder `git pull` updates.
237
+ install = (
238
+ '<pre class="c-join__cmd">cd distinct\npython -m pip install -e ".[agent]"</pre>'
239
+ '<p class="c-join__note">Python 3.10 or newer. On Windows use '
240
+ "<code>py -3.12 -m pip</code> if <code>python</code> is an older one.</p>"
241
+ )
242
+
243
+ # STEP THREE: the run, with this server's address and this browser's own
244
+ # library defaults already in it.
245
+ code_note = (
246
+ f'<p class="c-join__note">Pairing code, one use and ten minutes: '
247
+ f'<code class="c-join__code">{esc(code)}</code></p>'
248
  if code
249
+ else '<p class="c-join__note">Generate a pairing code below, then run the '
250
+ "command with it in place of YOUR-CODE.</p>"
251
  )
252
  # One line on purpose. The previous form used backslash continuations,
253
  # which are bash syntax: pasted into PowerShell they became stray
 
260
  picture = f'<div class="c-join__qr">{qr}</div>' if qr else ""
261
  return (
262
  '<div class="c-join">'
263
+ '<ol class="c-steps-num">'
264
+ '<li class="c-step-num"><span class="c-step-num__n">1</span>'
265
+ '<div class="c-step-num__body"><p class="c-step-num__head">Get the code</p>'
266
+ f"{get_it}</div></li>"
267
+ '<li class="c-step-num"><span class="c-step-num__n">2</span>'
268
+ '<div class="c-step-num__body"><p class="c-step-num__head">Install it</p>'
269
+ f"{install}</div></li>"
270
+ '<li class="c-step-num"><span class="c-step-num__n">3</span>'
271
+ '<div class="c-step-num__body"><p class="c-step-num__head">Point it at this server</p>'
272
+ f'<code class="c-join__address">{esc(first)}</code>{alternates}{code_note}'
273
  f'<pre class="c-join__cmd">{esc(command)}</pre>'
274
+ "<p class=\"c-join__note\">It prints an access code when it pairs. Give that to "
275
+ "the people who should be allowed to use your machine — anyone holding it can "
276
+ "send this worker requests.</p>"
277
+ f"{picture}</div></li>"
278
+ "</ol>"
279
  "</div>"
280
  )
281
 
distinct_server/ui.py CHANGED
@@ -1342,12 +1342,7 @@ def _identity_from_request(request: Optional[Any]) -> Optional[str]:
1342
 
1343
 
1344
  class DistinctUI:
1345
- def __init__(
1346
- self,
1347
- control_plane: Optional[ControlPlane] = None,
1348
- *,
1349
- artifact_dir: Path = Path("agent-artifacts"),
1350
- ) -> None:
1351
  self.control = control_plane or ControlPlane()
1352
  if not self.control.catalogue:
1353
  # The consent handshake needs the server to state what it may ask
@@ -1355,7 +1350,6 @@ class DistinctUI:
1355
  self.control.catalogue = _default_server_catalogue()
1356
  self.agent_api = AgentApi(self.control)
1357
  self.sessions = SessionAuthorizer()
1358
- self.artifact_dir = artifact_dir.resolve()
1359
  # Where claimed run artifacts are stored, per session, so the Outputs
1360
  # panel can offer them for download. Session-scoped and deleted with
1361
  # the session.
@@ -3055,7 +3049,15 @@ class DistinctUI:
3055
  # page, so what the person copies is what their runs will expect.
3056
  chosen = _library_defaults(state) if state is not None else ()
3057
  tools = ",".join(chosen) if chosen else "local"
3058
- return render.join_this_server(addresses=addresses, code=code, qr=qr, tools=tools)
 
 
 
 
 
 
 
 
3059
 
3060
  # Pairing/downloads ------------------------------------------------
3061
 
@@ -3072,35 +3074,12 @@ class DistinctUI:
3072
  except (ControlPlaneError, LoginRequired, ValueError) as exc:
3073
  raise gr.Error(str(exc)) from exc
3074
 
3075
- def download_agent(
3076
- self,
3077
- platform_name: str,
3078
- state: Mapping[str, Any],
3079
- profile: gr.OAuthProfile | None,
3080
- request: gr.Request | None = None,
3081
- ) -> str:
3082
- try:
3083
- self._authorize(state, profile, request)
3084
- patterns = {
3085
- "Windows x86-64": "distinct-agent-v*-windows-x86_64.zip",
3086
- "Linux x86-64": "distinct-agent-v*-linux-x86_64.tar.gz",
3087
- }
3088
- pattern = patterns.get(platform_name)
3089
- if pattern is None:
3090
- raise ValueError("select a supported agent build")
3091
- candidates = sorted(self.artifact_dir.glob(pattern), reverse=True)
3092
- if not candidates:
3093
- raise ValueError(
3094
- f"{platform_name} artifact has not been built for this Space revision"
3095
- )
3096
- target = candidates[0].resolve()
3097
- if target.parent != self.artifact_dir or not target.is_file():
3098
- raise ValueError(
3099
- f"{platform_name} artifact has not been built for this Space revision"
3100
- )
3101
- return str(target)
3102
- except (ControlPlaneError, LoginRequired, ValueError) as exc:
3103
- raise gr.Error(str(exc)) from exc
3104
 
3105
  # Component graph --------------------------------------------------
3106
 
@@ -3161,16 +3140,9 @@ class DistinctUI:
3161
  # library is a destination rather than a section, so its
3162
  # control is a button that swaps the view; "Run an agent" is
3163
  # still a section of this page, so it stays an anchor.
3164
- with gr.Row(elem_classes="c-quicknav"):
3165
- open_library = gr.Button(
3166
- "Library", size="sm", variant="secondary",
3167
- elem_classes="c-quicknav__btn", scale=0,
3168
- )
3169
- gr.HTML(
3170
- '<a class="c-quicknav__link" href="#join-agent">Run an agent</a>'
3171
- )
3172
-
3173
- # THE SIGN-IN CONTROL, AND WHY IT IS CONDITIONAL.
3174
  #
3175
  # On a Space, Gradio only wires real OAuth when the interface
3176
  # contains a `LoginButton`: `gradio.routes` gates the whole
@@ -3189,8 +3161,23 @@ class DistinctUI:
3189
  # self-hosted deployment uses the `/auth/login` route in
3190
  # `auth_routes.py` instead, which is a link rather than a
3191
  # component because it is an ordinary HTTP redirect.
3192
- if gradio_oauth_is_real():
3193
- gr.LoginButton("Sign in with Hugging Face", size="sm")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3194
 
3195
  gr.HTML(render.notice())
3196
 
@@ -3491,20 +3478,22 @@ class DistinctUI:
3491
  # which asked the reader to find something this
3492
  # process already knew.
3493
  join_panel = gr.HTML(self._join_panel())
3494
- # Three controls spread across the full page width read
3495
- # as three unrelated controls. Held to a measure they
3496
- # read as one step.
 
 
 
 
 
 
 
 
 
3497
  with gr.Row(elem_classes="c-tail__row"):
3498
- platform_name = gr.Dropdown(
3499
- choices=["Windows x86-64", "Linux x86-64"],
3500
- value="Windows x86-64",
3501
- label="Agent build",
3502
  )
3503
- download = gr.DownloadButton(
3504
- "Download agent", variant="secondary"
3505
- )
3506
- create_code = gr.Button("Generate pairing code")
3507
- gr.Markdown()
3508
 
3509
  with gr.Accordion("Model assessments and their sources", open=False):
3510
  gr.HTML(
@@ -3811,14 +3800,6 @@ class DistinctUI:
3811
  queue=False,
3812
  api_visibility="private",
3813
  )
3814
- download.click(
3815
- self.download_agent,
3816
- inputs=[platform_name, session_state],
3817
- outputs=[download],
3818
- queue=False,
3819
- api_visibility="private",
3820
- )
3821
-
3822
  # Two seconds, not five. The activity panel counts elapsed time
3823
  # and streams the worker's steps, and a spinner that updates
3824
  # every five seconds reads as a stall. The refresh is in-memory
 
1342
 
1343
 
1344
  class DistinctUI:
1345
+ def __init__(self, control_plane: Optional[ControlPlane] = None) -> None:
 
 
 
 
 
1346
  self.control = control_plane or ControlPlane()
1347
  if not self.control.catalogue:
1348
  # The consent handshake needs the server to state what it may ask
 
1350
  self.control.catalogue = _default_server_catalogue()
1351
  self.agent_api = AgentApi(self.control)
1352
  self.sessions = SessionAuthorizer()
 
1353
  # Where claimed run artifacts are stored, per session, so the Outputs
1354
  # panel can offer them for download. Session-scoped and deleted with
1355
  # the session.
 
3049
  # page, so what the person copies is what their runs will expect.
3050
  chosen = _library_defaults(state) if state is not None else ()
3051
  tools = ",".join(chosen) if chosen else "local"
3052
+ # Where the code lives, taken from the platform rather than written
3053
+ # down here. A hard-coded URL is a URL that goes stale the first time
3054
+ # this is deployed anywhere else, and the panel would then send
3055
+ # volunteers to somebody else's repository.
3056
+ space_id = os.environ.get("SPACE_ID", "").strip()
3057
+ source = f"https://huggingface.co/spaces/{space_id}" if space_id else ""
3058
+ return render.join_this_server(
3059
+ addresses=addresses, code=code, qr=qr, tools=tools, source=source
3060
+ )
3061
 
3062
  # Pairing/downloads ------------------------------------------------
3063
 
 
3074
  except (ControlPlaneError, LoginRequired, ValueError) as exc:
3075
  raise gr.Error(str(exc)) from exc
3076
 
3077
+ # The worker is Python, distributed as this repository's own source, so
3078
+ # there is no build artifact to serve and no `download_agent` handler any
3079
+ # more. The setup panel says where the code is and how to install it; see
3080
+ # `render.join_this_server`. If frozen builds ever ship, they belong on a
3081
+ # releases page, not behind a control that fails silently when the page
3082
+ # for this revision was never built.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3083
 
3084
  # Component graph --------------------------------------------------
3085
 
 
3140
  # library is a destination rather than a section, so its
3141
  # control is a button that swaps the view; "Run an agent" is
3142
  # still a section of this page, so it stays an anchor.
3143
+ #
3144
+ # THE SIGN-IN CONTROL SITS IN THIS ROW, AND WHY IT IS
3145
+ # CONDITIONAL.
 
 
 
 
 
 
 
3146
  #
3147
  # On a Space, Gradio only wires real OAuth when the interface
3148
  # contains a `LoginButton`: `gradio.routes` gates the whole
 
3161
  # self-hosted deployment uses the `/auth/login` route in
3162
  # `auth_routes.py` instead, which is a link rather than a
3163
  # component because it is an ordinary HTTP redirect.
3164
+ #
3165
+ # It is inside the nav row rather than under it because it is
3166
+ # navigation: left on its own line it read as a stray control
3167
+ # sitting between the page's chrome and its content.
3168
+ with gr.Row(elem_classes="c-quicknav"):
3169
+ open_library = gr.Button(
3170
+ "Library", size="sm", variant="secondary",
3171
+ elem_classes="c-quicknav__btn", scale=0,
3172
+ )
3173
+ gr.HTML(
3174
+ '<a class="c-quicknav__link" href="#join-agent">Run an agent</a>'
3175
+ )
3176
+ if gradio_oauth_is_real():
3177
+ gr.LoginButton(
3178
+ "Sign in with Hugging Face", size="sm",
3179
+ elem_classes="c-quicknav__auth", scale=0,
3180
+ )
3181
 
3182
  gr.HTML(render.notice())
3183
 
 
3478
  # which asked the reader to find something this
3479
  # process already knew.
3480
  join_panel = gr.HTML(self._join_panel())
3481
+ # ONE CONTROL, BECAUSE THERE IS ONE THING TO DO HERE.
3482
+ #
3483
+ # This row used to offer "Agent build: Windows x86-64"
3484
+ # and a Download button beside it. No such build is
3485
+ # produced for this Space, so the button's only
3486
+ # possible outcome was an error telling the volunteer
3487
+ # that an artifact "has not been built for this Space
3488
+ # revision" — a sentence about our release process,
3489
+ # delivered to somebody who wanted to help. The worker
3490
+ # is Python and always was; the panel above now says
3491
+ # so, and the only control left is the one that mints
3492
+ # the code those instructions need.
3493
  with gr.Row(elem_classes="c-tail__row"):
3494
+ create_code = gr.Button(
3495
+ "Generate pairing code", variant="primary", scale=0
 
 
3496
  )
 
 
 
 
 
3497
 
3498
  with gr.Accordion("Model assessments and their sources", open=False):
3499
  gr.HTML(
 
3800
  queue=False,
3801
  api_visibility="private",
3802
  )
 
 
 
 
 
 
 
 
3803
  # Two seconds, not five. The activity panel counts elapsed time
3804
  # and streams the worker's steps, and a spinner that updates
3805
  # every five seconds reads as a stall. The refresh is in-memory
pyproject.toml CHANGED
@@ -11,6 +11,11 @@ dependencies = ["gradio[oauth]==6.24.0"]
11
 
12
  [project.optional-dependencies]
13
  agent = [
 
 
 
 
 
14
  "dspy[deno]==3.3.0",
15
  "gradio_client==2.6.0",
16
  "pyRAPL==0.2.3.1; sys_platform == 'linux'",
 
11
 
12
  [project.optional-dependencies]
13
  agent = [
14
+ # Ed25519, for the detached snapshot signature. A direct requirement, not
15
+ # a transitive one: `distinct_protocol.handshake` imports it and refuses
16
+ # to fall back to HMAC, so an agent install that happens to miss it fails
17
+ # at the moment it pairs rather than at install time.
18
+ "cryptography>=42",
19
  "dspy[deno]==3.3.0",
20
  "gradio_client==2.6.0",
21
  "pyRAPL==0.2.3.1; sys_platform == 'linux'",
scripts/demo_server.py CHANGED
@@ -25,11 +25,11 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
25
 
26
  from distinct_protocol.netpolicy import announce_bind, launch_kwargs # noqa: E402
27
  from distinct_server import ControlPlane # noqa: E402
28
- from distinct_server.presentation import CSS, HEAD # noqa: E402
29
  from distinct_server.identity import ( # noqa: E402
30
  NotConfigured,
31
  refuse_mocked_oauth,
32
  )
 
33
  from distinct_server.ui import build_app, distinct_theme # noqa: E402
34
 
35
 
 
25
 
26
  from distinct_protocol.netpolicy import announce_bind, launch_kwargs # noqa: E402
27
  from distinct_server import ControlPlane # noqa: E402
 
28
  from distinct_server.identity import ( # noqa: E402
29
  NotConfigured,
30
  refuse_mocked_oauth,
31
  )
32
+ from distinct_server.presentation import CSS, HEAD # noqa: E402
33
  from distinct_server.ui import build_app, distinct_theme # noqa: E402
34
 
35
 
scripts/live_e2e.py CHANGED
@@ -292,10 +292,18 @@ def capture(playwright, server_url: str, shots: Shots, worker_started) -> None:
292
 
293
  open_section(other, "Run a community agent")
294
  other.wait_for_timeout(2000)
 
 
 
 
 
 
 
 
295
  shots.take(
296
  other,
297
- "download-the-agent",
298
- "Where a volunteer gets the worker for their own machine, Windows or Linux.",
299
  )
300
 
301
  worker_screen(browser, shots)
@@ -503,7 +511,7 @@ def main() -> int:
503
  found = re.search(r"\b([A-Z0-9]{5}(?:-[A-Z0-9]{4,5}){5,8})\b", line)
504
  if found:
505
  threading.Thread(
506
- target=lambda: [print(f" worker | {l.rstrip()}") for l in process.stdout],
507
  daemon=True,
508
  ).start()
509
  return found.group(1)
 
292
 
293
  open_section(other, "Run a community agent")
294
  other.wait_for_timeout(2000)
295
+ # The panel is now three steps rather than a build to download, so the
296
+ # screenshot is checked for what it must say. A caption promising a
297
+ # Windows build outlived the button that offered one; this asserts
298
+ # instead of describing.
299
+ setup = other.locator(".c-join").inner_text()
300
+ for needle in ("Get the code", "Install it", "pip install -e", "Point it at this server"):
301
+ if needle not in setup:
302
+ raise AssertionError(f"the setup panel no longer says {needle!r}")
303
  shots.take(
304
  other,
305
+ "set-up-the-agent",
306
+ "How a volunteer gets the worker running: the code, the install, the command.",
307
  )
308
 
309
  worker_screen(browser, shots)
 
511
  found = re.search(r"\b([A-Z0-9]{5}(?:-[A-Z0-9]{4,5}){5,8})\b", line)
512
  if found:
513
  threading.Thread(
514
+ target=lambda: [print(f" worker | {line.rstrip()}") for line in process.stdout],
515
  daemon=True,
516
  ).start()
517
  return found.group(1)