Skip to content

feat(cli): add moss playground command with WASM-based query UI - #496

Open
msranjana wants to merge 29 commits into
usemoss:mainfrom
msranjana:feat/local-playground-ui
Open

feat(cli): add moss playground command with WASM-based query UI#496
msranjana wants to merge 29 commits into
usemoss:mainfrom
msranjana:feat/local-playground-ui

Conversation

@msranjana

@msranjana msranjana commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Pull Request Checklist

Please ensure that your PR meets the following requirements:

  • I have read the [CONTRIBUTING](CONTRIBUTING.md) guide.
  • I have updated the documentation (if applicable).
  • My code follows the style guidelines of this project.
  • I have performed a self-review of my own code.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.

Description

Adds a local playground for interactively exploring Moss indexes from the browser.

The new moss playground command starts a lightweight local HTTP server that serves a browser-based playground. The UI uses @moss-dev/moss-web (WASM) to execute searches entirely in the browser after an index is loaded.

Usage

moss playground

The server starts on 127.0.0.1:8765 (or the next available port if occupied).

What's Included

  • Added playground.py command to start a local HTTP server.

  • Added a browser-based playground UI (playground/index.html).

  • Added API endpoints:

    • GET /api/indexes – lists available cloud indexes.
    • GET /api/index?name=... – returns index metadata.
  • Supports automatic credential resolution from CLI flags, environment variables, or configuration.

  • Loads @moss-dev/moss-web via WASM using an importmap with the unpkg CDN.

  • Allows users to:

    • Select and load an index.
    • Execute queries locally in the browser.
    • Adjust Top-K.
    • Adjust Alpha.
    • View score, document ID, text snippet, and query latency.
  • Falls back to a manual connection form when credentials are not injected by the server.

Fixes #434

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a local browser-based Playground for browsing indexes and running semantic searches.
    • Added the moss playground command with configurable ports, credential profiles, and optional automatic browser launch.
    • Added result metadata, scores, timing details, tokenized access, and clear loading, empty, and error states.
    • Added secure local access for loading indexes and querying results.
  • Documentation

    • Documented Playground setup, controls, index browsing, searching, and API behavior.
  • Tests

    • Added coverage for packaged Playground assets, authentication, validation, and request handling.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codex review

The PR adds a useful local playground, but the current implementation has a credential-trust issue and stale client state when switching connections.

@msranjana
msranjana force-pushed the feat/local-playground-ui branch from 627bea6 to ce2f2ff Compare July 29, 2026 03:22
- New playground.py command that starts a local HTTP server on :8765
- Serves index.html with importmap pointing to unpkg CDN (avoids esm.sh
  WASM loading issues with module.require shims)
- Includes /api/indexes and /api/index endpoints backed by MossClient
- Browser loads @moss-dev/moss-web via WASM; queries run locally with
  adjustable Top-K and Alpha controls
- Fixes: favicon.ico returns 204, log_message handles variable args
@msranjana
msranjana force-pushed the feat/local-playground-ui branch from ce2f2ff to 24e8ba3 Compare July 29, 2026 03:24
@Sravan1011

Copy link
Copy Markdown
Contributor

@msranjana address these comments

msranjana added 20 commits July 29, 2026 22:33
…er, fix stale results, fix alpha-zero, fix load-btn state, remove --index-dir
…d starts

I have updated the documentation (if applicable).
@msranjana
msranjana force-pushed the feat/local-playground-ui branch from f1c55ee to 72d167c Compare July 30, 2026 18:22
I have updated the documentation (if applicable).
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The CLI adds a local browser playground for browsing indexes, loading an index, and running configurable searches through a credentialed Moss client.

Changes

Local Playground

Layer / File(s) Summary
CLI command and packaged server startup
packages/moss-cli/src/moss_cli/commands/playground.py, packages/moss-cli/src/moss_cli/main.py, packages/moss-cli/pyproject.toml
The moss playground command resolves credentials, selects a port, serves the UI, optionally opens a browser, and packages the HTML asset.
HTTP API and SDK execution
packages/moss-cli/src/moss_cli/commands/playground.py, packages/moss-cli/tests/test_playground.py
The server authenticates requests, serves index data, loads indexes, validates query parameters, serializes results, suppresses superseded requests, and runs SDK operations through a persistent async worker.
Browser interface and documentation
packages/moss-cli/src/moss_cli/playground/index.html, packages/moss-cli/README.md, packages/moss-cli/tests/test_playground.py
The UI supports index selection, debounced searches, Top K and alpha controls, result metadata, loading states, errors, and stale-request handling. Documentation and tests cover usage and client-side behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant PlaygroundHandler
  participant AsyncWorker
  participant MossClient
  Browser->>PlaygroundHandler: POST /api/query with query options
  PlaygroundHandler->>AsyncWorker: Submit query operation
  AsyncWorker->>MossClient: Execute query through MossClient
  MossClient-->>AsyncWorker: Return matching results
  AsyncWorker-->>PlaygroundHandler: Return serialized query data
  PlaygroundHandler-->>Browser: JSON results and timing
Loading

Suggested reviewers: ashvathsureshkumar

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the new CLI playground command and its WASM-based query UI.
Linked Issues check ✅ Passed The changes provide a local playground that loads indexes, runs queries, displays ranked results and scores, and tunes search parameters as requested in issue #434.
Out of Scope Changes check ✅ Passed The documentation, packaging, server, UI, registration, and tests all directly support the local playground objective.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (8)
packages/moss-cli/src/moss_cli/commands/playground.py (4)

132-135: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Compare the token with secrets.compare_digest.

The != comparison on str short-circuits on the first differing byte. Use a constant-time comparison for the shared secret. The change is small and removes a timing side channel.

🔒 Proposed fix
-        if self.headers.get("X-Moss-Token") != self._token:
+        provided = self.headers.get("X-Moss-Token") or ""
+        if not self._token or not secrets.compare_digest(provided, self._token):
             self._send_json(403, {"error": "Forbidden: invalid or missing token"})
             return False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/moss-cli/src/moss_cli/commands/playground.py` around lines 132 -
135, Update _check_api_request to compare the X-Moss-Token header with
self._token using secrets.compare_digest instead of !=, preserving the existing
403 response and False return for invalid or missing tokens.

387-392: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unreachable credential check.

resolve_credentials never returns empty values. It raises typer.BadParameter when credentials are missing, as shown in packages/moss-cli/src/moss_cli/config.py Lines 168-173. The block below therefore never runs, and its message duplicates the error text that resolve_credentials already produces.

♻️ Proposed refactor
-    if not pid or not pkey:
-        console.print(
-            "[red]No credentials found.[/red] Run [bold]moss init[/bold] first "
-            "or set MOSS_PROJECT_ID and MOSS_PROJECT_KEY."
-        )
-        raise typer.Exit(1)
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/moss-cli/src/moss_cli/commands/playground.py` around lines 387 -
392, Remove the unreachable `if not pid or not pkey` credential check and its
associated console message and `typer.Exit` from the command flow. Rely on
`resolve_credentials` to raise `typer.BadParameter` when credentials are
missing, while leaving valid credential handling unchanged.

82-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the return value of stop to state its meaning.

stop returns True when the thread is still alive, so the caller at Line 432 reads if worker.stop(): as success. The inverted meaning is easy to misread in later changes. Return a named result or invert the value.

♻️ Proposed refactor
-    def stop(self, timeout: float | None = 5.0) -> bool:
+    def stop(self, timeout: float | None = 5.0) -> bool:
+        """Stop the loop and join the thread. Return True if the thread stopped."""
         self._loop.call_soon_threadsafe(self._loop.stop)
         self._thread.join(timeout)
-        return self._thread.is_alive()
+        return not self._thread.is_alive()

Then update the caller:

if not worker.stop():
    console.print("[yellow]Worker thread did not shut down gracefully within timeout[/yellow]")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/moss-cli/src/moss_cli/commands/playground.py` around lines 82 - 85,
Update the stop method’s return contract and its caller so the result clearly
represents whether shutdown completed successfully: invert the current
self._thread.is_alive() result in stop, then change the worker.stop() check to
handle failure with the existing warning message when shutdown does not complete
within the timeout.

285-289: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Both POST handlers accept non-string values for request-body fields. topK, alpha, and requestId receive strict isinstance checks, but name and query use data.get(field, "") and only a truthiness test. A JSON number, list, or object is truthy, reaches the PyO3 boundary, and surfaces as an opaque 500 rather than a 400. This replaces the two per-site comments.

  • packages/moss-cli/src/moss_cli/commands/playground.py#L285-L289: in _handle_post_query, require isinstance(name, str) and isinstance(query, str), each non-empty after strip().
  • packages/moss-cli/src/moss_cli/commands/playground.py#L269-L272: in _handle_post_load_index, require isinstance(name, str) and non-empty after strip().
🛡️ Proposed fix: shared field extractor
def _require_str(self, data: dict, field: str) -> str | None:
    """Return the field value, or send a 400 response and return None."""
    value = data.get(field)
    if not isinstance(value, str) or not value.strip():
        self._send_json(400, {"error": f"Missing or invalid '{field}' in request body"})
        return None
    return value
-        name = data.get("name", "")
-        query = data.get("query", "")
-        if not name or not query:
-            self._send_json(400, {"error": "Missing 'name' or 'query' in request body"})
-            return
+        name = self._require_str(data, "name")
+        if name is None:
+            return
+        query = self._require_str(data, "query")
+        if query is None:
+            return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/moss-cli/src/moss_cli/commands/playground.py` around lines 285 -
289, Update _handle_post_query at
packages/moss-cli/src/moss_cli/commands/playground.py#L285-L289 to require name
and query to be strings containing non-whitespace characters, returning HTTP 400
for missing or invalid values. Update _handle_post_load_index at
packages/moss-cli/src/moss_cli/commands/playground.py#L269-L272 with the same
validation for name. Reuse a shared string-field validation helper if
appropriate, while preserving the existing error-response behavior.
packages/moss-cli/src/moss_cli/playground/index.html (3)

249-252: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the searchTimeout declaration next to the other state variables.

loadIndex reads searchTimeout at Line 250, but let searchTimeout = null; appears later at Line 368. The code works because loadIndex runs only from the click listener after module evaluation completes. Any future call during module evaluation would raise a temporal dead zone ReferenceError. Declare searchTimeout with indexes, currentIndex, requestId, and searchAbort at Lines 148-151.

♻️ Proposed refactor
 let requestId = 0;
 let searchAbort = null;
+let searchTimeout = null;
-let searchTimeout = null;
-
 function handleSearch(e) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/moss-cli/src/moss_cli/playground/index.html` around lines 249 - 252,
Move the existing searchTimeout declaration from its later location to the
state-variable declarations alongside indexes, currentIndex, requestId, and
searchAbort; remove the original declaration and preserve its initialization and
all existing timeout behavior.

89-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add accessible names to the form controls.

Three controls have no accessible name:

  • #index-select at Line 89 relies on the .panel-title div, which is not a label.
  • #search-input at Line 111 has only a placeholder. A placeholder is not an accessible name.
  • #alpha-slider at Line 121 sits between two <span> elements, which do not label it.

A screen reader announces these as unlabeled controls. Add aria-label attributes.

♿ Proposed fix
-            <select id="index-select">
+            <select id="index-select" aria-label="Select an index">
-                <input type="text" id="search-input" placeholder="Type to search..." disabled />
+                <input type="text" id="search-input" placeholder="Type to search..." aria-label="Search query" disabled />
-                <input type="range" id="alpha-slider" min="0" max="1" step="0.05" value="0.5" disabled />
+                <input type="range" id="alpha-slider" min="0" max="1" step="0.05" value="0.5"
+                       aria-label="Alpha: keyword to semantic balance" disabled />

Also consider aria-live="polite" on #load-status and #results so status changes are announced.

Also applies to: 111-111, 121-121

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/moss-cli/src/moss_cli/playground/index.html` around lines 89 - 91,
Add accessible names to the form controls in the playground markup: add
descriptive aria-label attributes to `#index-select`, `#search-input`, and
`#alpha-slider`. Do not rely on the surrounding .panel-title, placeholder text, or
adjacent spans as labels; leave the optional aria-live enhancement for
`#load-status` and `#results` out of scope.

418-422: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Build the error box with DOM APIs instead of innerHTML.

This is the only place where dynamic data reaches innerHTML. escapeHtml at Lines 362-366 escapes <, >, and &, but it does not escape quotes, so the helper is unsafe if it is ever reused for an attribute value. The rest of the file already uses textContent. Construct the node directly and delete escapeHtml.

Note that the catch block also swallows AbortError. The thisAbort.signal.aborted check at Line 419 covers it, so no extra guard is needed.

♻️ Proposed refactor
     } catch (e) {
       if (thisRequestId !== requestId || thisAbort.signal.aborted) return;
       console.error('Query failed:', e);
-      results.innerHTML = `<div class="status-box status-error">Query failed: ${escapeHtml(e.message || e)}</div>`;
+      results.innerHTML = '';
+      const box = document.createElement('div');
+      box.className = 'status-box status-error';
+      box.textContent = `Query failed: ${e.message || e}`;
+      results.appendChild(box);
     }

Then remove the now-unused helper:

-function escapeHtml(text) {
-  const div = document.createElement('div');
-  div.textContent = text;
-  return div.innerHTML;
-}
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/moss-cli/src/moss_cli/playground/index.html` around lines 418 - 422,
Update the catch block in the request handling flow to build the error status
box with DOM APIs and assign the dynamic error text via textContent, while
preserving the existing status-box/status-error structure and abort/request
guards. Remove the now-unused escapeHtml helper; do not add an extra AbortError
guard.

Source: Linters/SAST tools

packages/moss-cli/tests/test_playground.py (1)

4-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for the request validation and token check.

This smoke test covers only asset presence. The PR checklist records that feature tests were not completed. The highest-value untested logic is deterministic and needs no network:

  • _check_api_request rejects a missing token, a wrong token, a foreign Host, and a foreign Origin.
  • _handle_post_query rejects a non-integer topK, a non-finite alpha, an out-of-range topK, an out-of-range alpha, and a missing requestId.
  • The _latest_request_id comparison marks an older request as superseded.

You can drive these through http.server with a stub client assigned to PlaygroundHandler.client, or by starting DaemonThreadingHTTPServer on an ephemeral port inside a fixture.

Do you want me to generate these tests?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/moss-cli/tests/test_playground.py` around lines 4 - 8, Expand the
playground tests beyond test_playground_html_asset_exists by exercising
PlaygroundHandler request validation and token checks with a stub client and
local HTTP server or direct handler setup. Cover _check_api_request for
missing/wrong tokens and foreign Host/Origin, and _handle_post_query for invalid
topK/alpha values and missing requestId. Also verify _latest_request_id marks an
older request as superseded, keeping all tests network-independent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/moss-cli/README.md`:
- Around line 153-170: Update the playground documentation examples and
description: state that port 8765 is used when available and the next free port
otherwise, add the --profile staging example, instruct users with --no-open to
use the exact URL printed by the command including its #<token> fragment, and
move the API-proxy statement outside the capabilities list.

In `@packages/moss-cli/src/moss_cli/commands/playground.py`:
- Around line 243-244: Update the request-body handling around Content-Length in
the authenticated handler to safely parse the header, return a 400 response for
malformed or negative values, and reject values above the permitted body-size
limit before calling self.rfile.read. Preserve the existing empty-body fallback
for a zero length.
- Around line 324-328: Update PlaygroundHandler request staleness tracking so
_latest_request_id is scoped per browser session instead of shared server-wide,
using the session identifier supplied by the client; apply the same per-session
lookup in the in-worker recheck around the query execution path, and include
sessionId in /api/query requests from index.html so page reloads start a fresh
session.
- Line 350: Update packages/moss-cli/src/moss_cli/commands/playground.py at
lines 350-350, 208-208, 228-228, and 277-277 in _handle_post_query,
_handle_list_indexes, _handle_get_index, and _handle_post_load_index so
responses use a fixed safe error message instead of str(e), while logging the
exception detail locally; centralize this behavior in a shared helper if
appropriate.
- Around line 230-236: Update log_message to escape each client-controlled value
with rich.markup.escape before interpolating it into any console.print message,
preserving the existing argument-count formatting while preventing request data
from being interpreted as Rich markup.

In `@packages/moss-cli/src/moss_cli/playground/index.html`:
- Around line 237-242: Update the `.index-info` styling to use `white-space:
pre-wrap` so the newline separators appended by `showIndexInfo` render as line
breaks; leave the existing row-building logic unchanged.
- Around line 388-390: Update the topK parsing near rawTopK, parsedTopK, and
topK to round the numeric value and clamp it to the inclusive range 1–50 before
sending the request. Preserve the existing default of 5 for blank or non-numeric
input so the server always receives a valid integer.
- Around line 140-146: Update the token initialization in the module script to
read the hash once, then remove it from the address bar using
history.replaceState so it is not retained in browser history. Add startup
handling around the existing initialization flow to detect an empty mossToken
and display a direct missing-token message instead of attempting API calls and
showing a generic list failure.
- Around line 411-414: Replace direct res.json() parsing for non-OK responses
with one shared errorFromResponse helper that reads the body as text, attempts
JSON.parse to reuse a server-provided error, and falls back to an error
containing the HTTP status. Apply the helper in the /api/query handler at
packages/moss-cli/src/moss_cli/playground/index.html#L411-L414, refreshIndexList
at `#L170-L173`, and loadIndex at `#L267-L270`.

---

Nitpick comments:
In `@packages/moss-cli/src/moss_cli/commands/playground.py`:
- Around line 132-135: Update _check_api_request to compare the X-Moss-Token
header with self._token using secrets.compare_digest instead of !=, preserving
the existing 403 response and False return for invalid or missing tokens.
- Around line 387-392: Remove the unreachable `if not pid or not pkey`
credential check and its associated console message and `typer.Exit` from the
command flow. Rely on `resolve_credentials` to raise `typer.BadParameter` when
credentials are missing, while leaving valid credential handling unchanged.
- Around line 82-85: Update the stop method’s return contract and its caller so
the result clearly represents whether shutdown completed successfully: invert
the current self._thread.is_alive() result in stop, then change the
worker.stop() check to handle failure with the existing warning message when
shutdown does not complete within the timeout.
- Around line 285-289: Update _handle_post_query at
packages/moss-cli/src/moss_cli/commands/playground.py#L285-L289 to require name
and query to be strings containing non-whitespace characters, returning HTTP 400
for missing or invalid values. Update _handle_post_load_index at
packages/moss-cli/src/moss_cli/commands/playground.py#L269-L272 with the same
validation for name. Reuse a shared string-field validation helper if
appropriate, while preserving the existing error-response behavior.

In `@packages/moss-cli/src/moss_cli/playground/index.html`:
- Around line 249-252: Move the existing searchTimeout declaration from its
later location to the state-variable declarations alongside indexes,
currentIndex, requestId, and searchAbort; remove the original declaration and
preserve its initialization and all existing timeout behavior.
- Around line 89-91: Add accessible names to the form controls in the playground
markup: add descriptive aria-label attributes to `#index-select`, `#search-input`,
and `#alpha-slider`. Do not rely on the surrounding .panel-title, placeholder
text, or adjacent spans as labels; leave the optional aria-live enhancement for
`#load-status` and `#results` out of scope.
- Around line 418-422: Update the catch block in the request handling flow to
build the error status box with DOM APIs and assign the dynamic error text via
textContent, while preserving the existing status-box/status-error structure and
abort/request guards. Remove the now-unused escapeHtml helper; do not add an
extra AbortError guard.

In `@packages/moss-cli/tests/test_playground.py`:
- Around line 4-8: Expand the playground tests beyond
test_playground_html_asset_exists by exercising PlaygroundHandler request
validation and token checks with a stub client and local HTTP server or direct
handler setup. Cover _check_api_request for missing/wrong tokens and foreign
Host/Origin, and _handle_post_query for invalid topK/alpha values and missing
requestId. Also verify _latest_request_id marks an older request as superseded,
keeping all tests network-independent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c2fbf5d6-7a44-41a9-9f10-efd55bd23ff0

📥 Commits

Reviewing files that changed from the base of the PR and between de26a4b and 6edb5e4.

📒 Files selected for processing (6)
  • packages/moss-cli/README.md
  • packages/moss-cli/pyproject.toml
  • packages/moss-cli/src/moss_cli/commands/playground.py
  • packages/moss-cli/src/moss_cli/main.py
  • packages/moss-cli/src/moss_cli/playground/index.html
  • packages/moss-cli/tests/test_playground.py

Comment thread packages/moss-cli/README.md
Comment thread packages/moss-cli/src/moss_cli/commands/playground.py Outdated
Comment thread packages/moss-cli/src/moss_cli/commands/playground.py Outdated
Comment thread packages/moss-cli/src/moss_cli/commands/playground.py Outdated
Comment thread packages/moss-cli/src/moss_cli/commands/playground.py Outdated
Comment thread packages/moss-cli/src/moss_cli/playground/index.html
Comment thread packages/moss-cli/src/moss_cli/playground/index.html
Comment thread packages/moss-cli/src/moss_cli/playground/index.html Outdated
Comment thread packages/moss-cli/src/moss_cli/playground/index.html Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/moss-cli/tests/test_playground.py`:
- Around line 181-201: Scope stale-query tracking in PlaygroundHandler to
individual browser sessions instead of the shared _latest_request_id class
value. Add or reuse a browser session identifier and maintain the latest request
ID per session, ensuring reloads and concurrent tabs can submit lower IDs
without being marked superseded. Update the stale-request tests to cover both
scenarios and verify valid queries are processed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 810982fd-d14d-415a-ab53-0fbb2edcbd2f

📥 Commits

Reviewing files that changed from the base of the PR and between 6edb5e4 and 7ae2a7b.

📒 Files selected for processing (1)
  • packages/moss-cli/tests/test_playground.py

Comment thread packages/moss-cli/tests/test_playground.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/moss-cli/src/moss_cli/commands/playground.py">

<violation number="1" location="packages/moss-cli/src/moss_cli/commands/playground.py:86">
P2: Ctrl+C can still block process exit during an in-flight index load or query because stopping the event loop does not stop the `asyncio.to_thread` executor work. Track/cancel or drain outstanding operations and shut down the executor before returning from the command.</violation>

<violation number="2" location="packages/moss-cli/src/moss_cli/commands/playground.py:303">
P2: Loading several indexes during one playground session retains all of them in server memory, so exploring large indexes can grow the process until it is killed. Track the active index and unload the previous one before loading a replacement, or provide an explicit unload/eviction policy.</violation>

<violation number="3" location="packages/moss-cli/src/moss_cli/commands/playground.py:345">
P2: An oversized integer `alpha` crashes the request handler instead of returning a validation error; catch conversion overflow before the existing finite/range checks.</violation>

<violation number="4" location="packages/moss-cli/src/moss_cli/commands/playground.py:394">
P2: Automatic port selection has a time-of-check/time-of-use race: a port reported free can be occupied before `ThreadingHTTPServer` binds it, making `moss playground` fail even though later ports may be available. Binding the actual HTTP server while probing, or retrying server construction across the candidate range, would make the next-free-port guarantee reliable.</violation>

<violation number="5" location="packages/moss-cli/src/moss_cli/commands/playground.py:419">
P1: Running `moss playground` without CLI, environment, or profile credentials exits before the browser can open, so the advertised manual connection fallback is unreachable. The command would need to start a credential-free UI/server path and provide a manual connection form (with corresponding credential handling) instead of unconditionally calling `resolve_credentials` first.</violation>
</file>

<file name="packages/moss-cli/src/moss_cli/playground/index.html">

<violation number="1" location="packages/moss-cli/src/moss_cli/playground/index.html:419">
P0: Searches are executed by the credentialed Python server rather than by the browser WASM UI. As a result, this command does not deliver the advertised WASM-based/local-browser query behavior and adds a server-side query endpoint that the feature contract explicitly excludes; wiring the page to `@moss-dev/moss-web` and keeping the server limited to index retrieval/loading would preserve the intended architecture.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/moss-cli/src/moss_cli/playground/index.html Outdated
Comment thread packages/moss-cli/src/moss_cli/commands/playground.py Outdated
Comment thread packages/moss-cli/src/moss_cli/commands/playground.py Outdated
Comment thread packages/moss-cli/src/moss_cli/commands/playground.py Outdated
Comment thread packages/moss-cli/src/moss_cli/commands/playground.py Outdated
Comment thread packages/moss-cli/src/moss_cli/commands/playground.py Outdated
Copilot AI review requested due to automatic review settings August 1, 2026 02:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/moss-cli/src/moss_cli/commands/playground.py`:
- Around line 303-313: Update the nested _call function to set
PlaygroundHandler._loaded_index to None immediately after a successful
unload_index(previous) completes, before calling load_index(name). Add a
regression test covering successful unload followed by load_index raising, and
verify the tracked index remains None.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c90bf2d-e167-4d02-b7d3-c7aa96b29f16

📥 Commits

Reviewing files that changed from the base of the PR and between af177cb and 0182ecf.

📒 Files selected for processing (3)
  • packages/moss-cli/src/moss_cli/commands/playground.py
  • packages/moss-cli/src/moss_cli/playground/index.html
  • packages/moss-cli/tests/test_playground.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/moss-cli/src/moss_cli/playground/index.html

Comment thread packages/moss-cli/src/moss_cli/commands/playground.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new moss playground subcommand to the Moss CLI that starts a local HTTP server and serves a browser UI for browsing indexes, loading an index, and running queries interactively.

Changes:

  • Added moss playground Typer command backed by a local ThreadingHTTPServer with token-gated API endpoints.
  • Added a static single-page playground UI (index.html) plus packaging config to ship the HTML asset.
  • Added unit tests covering request validation, token/host/origin checks, and stale-query supersession behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/moss-cli/src/moss_cli/commands/playground.py Implements the playground server, API endpoints, token checks, and CLI command wiring.
packages/moss-cli/src/moss_cli/playground/index.html Adds the browser-based UI that calls the local playground API.
packages/moss-cli/src/moss_cli/main.py Registers the new playground command in the CLI.
packages/moss-cli/tests/test_playground.py Adds tests for API validation, auth checks, and concurrency/supersession logic.
packages/moss-cli/README.md Documents usage for moss playground and how the tokenized URL works.
packages/moss-cli/pyproject.toml Includes the playground HTML file in package data for distribution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


def playground_command(
ctx: typer.Context,
port: int = typer.Option(0, "--port", "-p", help="Port for the HTTP server (0 = auto)"),
if not isinstance(value, str) or not value.strip():
self._send_json(400, {"error": message})
return None
return value
Comment on lines +451 to +454
worker = AsyncWorker()
client = worker.submit(lambda: MossClient(pid, pkey))

# Start server
Comment on lines +419 to +423
const res = await apiFetch('/api/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: thisAbort.signal,
body: JSON.stringify({ name: idx, query, topK, alpha, requestId: thisRequestId, sessionId }),

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/moss-cli/src/moss_cli/commands/playground.py">

<violation number="1" location="packages/moss-cli/src/moss_cli/commands/playground.py:306">
P2: When loading an index fails after the previous index was already unloaded, `_loaded_index` still names the unloaded previous index. The next switch to a different index will try to `unload_index` an index that is no longer loaded, which can raise in the SDK and turn a user typo (bad index name) into a working-index teardown plus a confusing 500 on the next load. Consider resetting `_loaded_index` (or restoring the previous value) when `load_index` fails inside `_call`, so the unload bookkeeping stays consistent even after a failed switch.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

async def _call():
previous = PlaygroundHandler._loaded_index
if previous is not None and previous != name:
unload = client.unload_index(previous)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When loading an index fails after the previous index was already unloaded, _loaded_index still names the unloaded previous index. The next switch to a different index will try to unload_index an index that is no longer loaded, which can raise in the SDK and turn a user typo (bad index name) into a working-index teardown plus a confusing 500 on the next load. Consider resetting _loaded_index (or restoring the previous value) when load_index fails inside _call, so the unload bookkeeping stays consistent even after a failed switch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/moss-cli/src/moss_cli/commands/playground.py, line 306:

<comment>When loading an index fails after the previous index was already unloaded, `_loaded_index` still names the unloaded previous index. The next switch to a different index will try to `unload_index` an index that is no longer loaded, which can raise in the SDK and turn a user typo (bad index name) into a working-index teardown plus a confusing 500 on the next load. Consider resetting `_loaded_index` (or restoring the previous value) when `load_index` fails inside `_call`, so the unload bookkeeping stays consistent even after a failed switch.</comment>

<file context>
@@ -300,7 +300,18 @@ def _handle_post_load_index(self, data: dict) -> None:
+            async def _call():
+                previous = PlaygroundHandler._loaded_index
+                if previous is not None and previous != name:
+                    unload = client.unload_index(previous)
+                    if inspect.isawaitable(unload):
+                        await unload
</file context>

msranjana and others added 2 commits August 1, 2026 08:11
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
<script type="importmap">
{
"imports": {
"@moss-dev/moss-web": "https://unpkg.com/@moss-dev/moss-web@1.0.0/dist/index.js",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKING ```html
"@moss-dev/moss-web": "https://unpkg.com/@moss-dev/moss-web@1.0.0/dist/index.js",

The page executes remote CDN modules and then receives the configured `projectKey` from `/api/config`, so every auto-connected playground session trusts those CDN responses with the user's credentials. Fix by bundling/serving the JS/WASM assets from the CLI package with `script-src 'self'`, or keep the key on the local server and proxy the Moss calls instead of exposing it to remotely loaded code.

connect(pid, pkey);
});

changeConnBtn.addEventListener('click', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONSIDER ```js
changeConnBtn.addEventListener('click', () => {
mainPanel.classList.add('hidden');

Switching connections hides the main panel but leaves `currentIndex`, pending searches, old results, and `searchTimeout` intact; a previous query can still render after reconnect, and an index with the same name in the new project can be treated as already loaded. Add a `resetSessionState()` that clears/increments `requestId`, cancels the timeout, sets `currentIndex = null`, clears results/search fields/status, and call it before showing the connection form or before assigning the new client.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/moss-cli/src/moss_cli/playground/index.html">

<violation number="1" location="packages/moss-cli/src/moss_cli/playground/index.html:277">
P1: Reconnecting can falsely reuse the previous client's loaded-index state: selecting the same index after Change connection enables search without loading it into the new client. Reset `currentIndex` and invalidate pending searches when changing connections.</violation>

<violation number="2" location="packages/moss-cli/src/moss_cli/playground/index.html:283">
P0: Every connection attempt fails because `@moss-dev/moss-web` exposes a constructor, not `MossClient.create`; instantiate it with `new MossClient(pid, pkey)` so the playground can reach the index UI.</violation>

<violation number="3" location="packages/moss-cli/src/moss_cli/playground/index.html:521">
P2: Query latency is always missing with the WASM SDK because its result field is `timeTakenInMs` while the renderer still reads `timeTakenMs`; update the renderer to use the SDK field.</violation>
</file>

<file name="packages/moss-cli/src/moss_cli/commands/playground.py">

<violation number="1" location="packages/moss-cli/src/moss_cli/commands/playground.py:119">
P1: The automatic connection exposes the CLI's project key to an untrusted CDN-loaded JavaScript module, turning a CDN/package compromise into project credential theft. Keeping cloud calls behind the local server, or issuing a scoped short-lived browser token and self-hosting/integrity-pinning the client bundle, would preserve the playground flow without exposing the long-lived key.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

connectBtn.disabled = true;
try {
await loadSdk();
client = await MossClient.create(pid, pkey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: Every connection attempt fails because @moss-dev/moss-web exposes a constructor, not MossClient.create; instantiate it with new MossClient(pid, pkey) so the playground can reach the index UI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/moss-cli/src/moss_cli/playground/index.html, line 283:

<comment>Every connection attempt fails because `@moss-dev/moss-web` exposes a constructor, not `MossClient.create`; instantiate it with `new MossClient(pid, pkey)` so the playground can reach the index UI.</comment>

<file context>
@@ -166,36 +230,92 @@ <h1>Moss <span>Playground</span></h1>
+  connectBtn.disabled = true;
+  try {
+    await loadSdk();
+    client = await MossClient.create(pid, pkey);
+    connectPanel.classList.add('hidden');
+    mainPanel.classList.remove('hidden');
</file context>
Suggested change
client = await MossClient.create(pid, pkey);
client = new MossClient(pid, pkey);


async function connect(pid, pkey) {
connectPanel.classList.remove('hidden');
mainPanel.classList.add('hidden');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Reconnecting can falsely reuse the previous client's loaded-index state: selecting the same index after Change connection enables search without loading it into the new client. Reset currentIndex and invalidate pending searches when changing connections.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/moss-cli/src/moss_cli/playground/index.html, line 277:

<comment>Reconnecting can falsely reuse the previous client's loaded-index state: selecting the same index after Change connection enables search without loading it into the new client. Reset `currentIndex` and invalidate pending searches when changing connections.</comment>

<file context>
@@ -166,36 +230,92 @@ <h1>Moss <span>Playground</span></h1>
+
+async function connect(pid, pkey) {
+  connectPanel.classList.remove('hidden');
+  mainPanel.classList.add('hidden');
+  showStatus(connectStatus, '',
+    'Loading the Moss WASM engine (first load downloads the embedding model)...');
</file context>
Suggested change
mainPanel.classList.add('hidden');
++requestId;
if (searchTimeout) clearTimeout(searchTimeout);
currentIndex = null;
mainPanel.classList.add('hidden');

200,
{
"projectId": self._project_id,
"projectKey": self._project_key,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The automatic connection exposes the CLI's project key to an untrusted CDN-loaded JavaScript module, turning a CDN/package compromise into project credential theft. Keeping cloud calls behind the local server, or issuing a scoped short-lived browser token and self-hosting/integrity-pinning the client bundle, would preserve the playground flow without exposing the long-lived key.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/moss-cli/src/moss_cli/commands/playground.py, line 119:

<comment>The automatic connection exposes the CLI's project key to an untrusted CDN-loaded JavaScript module, turning a CDN/package compromise into project credential theft. Keeping cloud calls behind the local server, or issuing a scoped short-lived browser token and self-hosting/integrity-pinning the client bundle, would preserve the playground flow without exposing the long-lived key.</comment>

<file context>
@@ -205,42 +111,14 @@ def _serve_index(self) -> None:
+            200,
+            {
+                "projectId": self._project_id,
+                "projectKey": self._project_key,
+            },
+        )
</file context>

if (placeholder) placeholder.innerHTML = '<p>Searching...</p>';

try {
const searchResults = await client.query(idx, query, { topK, alpha });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Query latency is always missing with the WASM SDK because its result field is timeTakenInMs while the renderer still reads timeTakenMs; update the renderer to use the SDK field.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/moss-cli/src/moss_cli/playground/index.html, line 521:

<comment>Query latency is always missing with the WASM SDK because its result field is `timeTakenInMs` while the renderer still reads `timeTakenMs`; update the renderer to use the SDK field.</comment>

<file context>
@@ -407,34 +512,22 @@ <h1>Moss <span>Playground</span></h1>
-      }
-      const searchResults = await res.json();
-      if (thisRequestId !== requestId || thisAbort.signal.aborted) return;
+      const searchResults = await client.query(idx, query, { topK, alpha });
+      if (thisRequestId !== requestId) return;
       renderResults(searchResults);
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tooling: local playground UI for exploring an index

3 participants