6 Commits
Author SHA1 Message Date
Edouard Durand 5373c85b86 RigShare 1.2.2 2026-09-26 08:27:05 +00:00
Edouard Durand 1f38a22535 Queue: filled square stop icon 2026-09-26 08:22:57 +00:00
Edouard Durand 531fec2ec9 Queue tab: running and pending prompts with who queued them
The server records who queued each prompt (and the tab's name, sent by the
client) and publishes queue items with the stats. Cancelling someone else's
prompt or clearing the queue needs Admin. Tabs: Live, Chat, People, Queue,
Server, Admin.
2026-09-26 08:20:44 +00:00
Edouard Durand 91f3383e86 Registry: send the changelog, fix the logo, fuller description
The publish workflow passes the version's CHANGELOG.md section to comfy-cli
(COMFY_NODE_CHANGELOG), icon.png is re-rendered from icon.svg at 512x512
with all four corners rounded, and the description covers the features.
2026-09-26 07:25:07 +00:00
Edouard Durand 2b1b3b11ce Admin: clear the chat history
DELETE /rigshare/api/chat (admin) empties chat.jsonl, tells every client to
clear its history, then posts a notice saying who cleared it. Admin tab gets
a Chat section with a confirmed Clear chat history button.
2026-09-26 07:21:58 +00:00
Edouard Durand 7e10db82df App mode: keep an open RigShare or Files panel when entering the App view
ComfyUI closes the side panel when a tab enters the App view; reopen the
RigShare tab that was open, so it behaves the same in both directions.
2026-09-26 07:17:49 +00:00
32 changed files with 351 additions and 15 deletions
+23
View File
@@ -14,7 +14,30 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v7
- name: Changelog for this version
# comfy-cli sends COMFY_NODE_CHANGELOG as the version's changelog
# (the registry's Updates section): the matching CHANGELOG.md section.
run: |
version=$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml | head -n1)
notes=$(awk -v v="$version" '
$0 ~ "^## " v "( |$)" { on = 1; next }
on && /^## / { exit }
on { print }
' CHANGELOG.md | sed -e '/./,$!d')
if [ -z "$notes" ]; then
echo "::warning::No CHANGELOG.md section for $version"
exit 0
fi
delimiter="EOF_$(date +%s%N)"
{
echo "COMFY_NODE_CHANGELOG<<$delimiter"
printf '%s\n' "$notes"
echo "$delimiter"
} >> "$GITHUB_ENV"
echo "Changelog for $version:"
printf '%s\n' "$notes"
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here.
skip_checkout: "true"
+20
View File
@@ -1,5 +1,25 @@
# Changelog
## 1.2.2 (2026-09-26)
A Queue tab showing who launched what, and chat clean-up for admins.
### Added
- **Queue** tab: the run in progress and the ones waiting, each with the workflow's name, who queued it and when. Stop or remove your own runs from there; admins can do it for anyone's. The server enforces it too: stopping or removing someone else's prompt, or clearing the whole queue, needs *Admin*
- Admins can clear the whole chat history (*Admin → Chat*, or `DELETE /rigshare/api/chat`). Everyone's chat empties at once, followed by a notice saying who cleared it
### Changed
- Panel tabs in a new order: Live, Chat, People, Queue, Server, Admin
- A fuller description on the Comfy Registry
### Fixed
- Entering the App view closed an open RigShare or Files panel (going back to the graph kept it open). It now stays open both ways
- The Comfy Registry showed no changelog: the publish workflow now sends the matching `CHANGELOG.md` section
- The registry logo was a clipped render with only one rounded corner
## 1.2.1 (2026-09-26)
### Changed
+8 -2
View File
@@ -25,13 +25,14 @@ A custom node that turns a single ComfyUI server into a shared workspace. Everyo
- lets you switch tabs freely, because only the tab on screen syncs, and keeps open tabs pointed at a workflow when someone renames or moves it
- shows who is online and which workflow they are on, plus live cursors and selections
- lets you follow someone, jumping to their tab and view
- keeps the whole chat history, loading older messages as you scroll up, with an unread badge, a blinking icon and a sound
- keeps the whole chat history, loading older messages as you scroll up, with an unread badge, a blinking icon and a sound; admins can clear it
- shows the queue: what is running and waiting, and who launched each run, with a button to stop or remove your own
- keeps per-workflow history: a snapshot on every save and every few minutes, named snapshots, one-click restore
- works in ComfyUI's App mode: everyone picks their own graph or App view, App inputs sync live, and you can see who is in which view
- gives each account permissions to edit, queue, use ComfyUI Manager or administer, enforced by the server and not just hidden in the UI
- keeps simultaneous edits apart: two people adding or connecting nodes at the same moment both keep their work
- issues personal API keys, so scripts and other apps can call ComfyUI with their owner's permissions
- shows CPU, RAM, and for each GPU the load, VRAM, temperature and power, plus the queue, in a panel tab or a floating widget
- shows CPU, RAM, and for each GPU the load, VRAM, temperature and power, in a panel tab or a floating widget
- sends execution progress and previews to everyone, not only whoever queued
What this fork adds over Nexus:
@@ -55,9 +56,14 @@ What this fork adds over Nexus:
<td align="center"><img src="docs/images/access.png" width="260" alt="Access list of a shared folder"><br><sub>Who can open a folder</sub></td>
</tr>
<tr>
<td align="center"><img src="docs/images/queue.png" width="260" alt="Queue tab"><br><sub>Queue: who launched what</sub></td>
<td align="center"><img src="docs/images/server.png" width="260" alt="Server tab"><br><sub>Server monitor</sub></td>
<td align="center"><img src="docs/images/account.png" width="260" alt="Account and API keys"><br><sub>Account and API keys</sub></td>
</tr>
<tr>
<td align="center"><img src="docs/images/admin.png" width="260" alt="Admin tab"><br><sub>Accounts and server settings</sub></td>
<td></td>
<td></td>
</tr>
</table>
+1 -1
View File
@@ -6,7 +6,7 @@
|---|---|
| *(none)*, a viewer | Watch shared workflows, chat, follow people, open snapshots as private copies |
| **Edit** | Change shared workflows, save or rename workflow files, take and restore snapshots, upload files |
| **Queue** | Queue prompts, interrupt, free memory |
| **Queue** | Queue prompts, stop or remove your own runs, free memory. Stopping or removing someone else's, or clearing the queue, needs *Admin* |
| **Manager** | Open ComfyUI Manager: install, update or remove custom nodes and models, snapshots, restart |
| **Admin** | Everything, plus deleting workflow files, accounts and server settings |
+1
View File
@@ -47,6 +47,7 @@ The panel uses these; they take the same session cookie or API key.
| `GET /rigshare/api/me` | The current account |
| `GET/POST /rigshare/api/me/keys`, `DELETE /rigshare/api/me/keys/{id}` | Your API keys |
| `GET /rigshare/api/chat?before=<seq>&limit=50` | Older chat messages, oldest first, and whether more exist |
| `DELETE /rigshare/api/chat` | Clear the whole chat history (admin) |
| `GET /rigshare/api/rooms` | Live workflows |
| `GET /rigshare/api/room?key=…` | One room, with its workflow JSON |
| `GET /rigshare/api/snapshots?room=…` | History of a room |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 73 KiB

+1 -1
View File
@@ -34,7 +34,7 @@ Admins change these under **Admin → Server settings**. They are stored in `rig
| `room_expiry_days` | `14` | Forget the live state of workflows nobody opened for this long |
| `snapshot_interval_sec` | `300` | Automatic snapshot interval while editing; `0` turns it off |
| `snapshot_keep` | `30` | Automatic snapshots kept per workflow, and as many on-save ones; named ones are always kept |
| `chat_keep` | `0` | Chat messages kept: `0` keeps the whole history, a number trims it to the latest ones at startup |
| `chat_keep` | `0` | Chat messages kept: `0` keeps the whole history, a number trims it to the latest ones at startup. Admins can also clear it under *Admin → Chat* |
## Environment variables
+2
View File
@@ -19,6 +19,8 @@ Only the tab on screen is live. Switch to another tab and you leave the room; sw
**Live** shows the tab on screen: whether it is live, who else is in it, and buttons for **Snapshot** and **History**. For a private or unsaved tab it says why it is not live. Below that is the list of **live workflows**: those someone has open right now, with their folder and the people in each one. Open any of them with the arrow. The Files tab marks live files with a green dot.
**Queue** shows the run in progress and the ones waiting, each with the workflow's name, who queued it and when. The button on a row stops or removes it: your own runs, or anyone's for an admin.
**People** lists who is online and which workflow they are on. The eye button *follows* someone: it jumps to their tab and keeps your view on theirs until you click or scroll the canvas.
## What syncs, and how
+2 -2
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-rigshare"
description = "Multi-user collaboration for ComfyUI: accounts and login, live-shared workflows, cursors, chat, snapshots and a server monitor."
version = "1.2.1"
description = "Turn one ComfyUI server into a shared team workspace. Sign-in with accounts, permissions (edit, queue, Manager, admin) and personal API keys, enforced by the server. A private folder for each person plus shared folders with access lists, with Save asking where. Live co-editing of every shared workflow, with cursors, follow mode and conflict-free merges, in the graph or App view. Per-workflow history with a snapshot on every save, team chat with its whole history, and a CPU, RAM, GPU and queue monitor."
version = "1.2.2"
license = { file = "LICENSE" }
dependencies = []
+7
View File
@@ -57,6 +57,13 @@ class ChatLog:
os.replace(tmp, self.path)
self._index()
def clear(self):
"""Delete the whole history; numbering starts again at 0."""
with self._lock:
with open(self.path, "w", encoding="utf-8"):
pass
self.offsets = []
def __len__(self):
return len(self.offsets)
+37
View File
@@ -15,6 +15,7 @@ import json
import logging
import secrets
import time
from collections import OrderedDict
from aiohttp import web, WSMsgType
@@ -106,6 +107,7 @@ class Hub:
self.stats = None
self._task = None
self.workspace = None # set by __init__ (folders, private spaces)
self.queue_owners = OrderedDict() # prompt_id -> {user, name, workflow, at}
# ----- lifecycle ----------------------------------------------------
@@ -142,10 +144,39 @@ class Hub:
continue
try:
self.stats = await loop.run_in_executor(None, collect_stats, self.server)
self.stats["queue_items"] = self.with_owners(self.stats.get("queue_items"))
await self.broadcast({"type": "stats", "stats": self.stats})
except Exception as e:
log.debug(f"[RigShare] stats error: {e}")
# ----- queue ----------------------------------------------------------
def record_prompt(self, prompt_id, user, workflow):
"""Remember who queued a prompt (ComfyUI itself doesn't)."""
if not prompt_id:
return
self.queue_owners[prompt_id] = {
"user": user["username"] if user else None,
"name": (user.get("display_name") or user["username"]) if user else None,
"workflow": clean_text(workflow, 120) or None, "at": int(time.time())}
while len(self.queue_owners) > 2000:
self.queue_owners.popitem(last=False)
def owner_of(self, prompt_id):
return (self.queue_owners.get(prompt_id) or {}).get("user")
def with_owners(self, items):
if items is None:
return None
return [{**it, **(self.queue_owners.get(it["id"]) or {})} for it in items]
def running_prompt_ids(self):
try:
running, _ = self.server.prompt_queue.get_current_queue_volatile()
return [entry[1] for entry in running]
except Exception:
return []
def snapshot(self, room, author, label=None):
if room.doc is None:
return None
@@ -381,6 +412,12 @@ class Hub:
async def broadcast_server(self):
await self.broadcast({"type": "server", "server": self.server_info()})
async def clear_chat(self, by):
"""An admin wiped the chat: everyone's history empties, then a notice says who."""
self.chat.clear()
await self.broadcast({"type": "chat_cleared"})
await self.system_chat(f"{by} cleared the chat history")
async def system_chat(self, text):
msg = {"id": secrets.token_hex(6), "system": True, "text": text, "ts": int(time.time() * 1000)}
msg = self.chat.append(msg)
+46
View File
@@ -8,6 +8,7 @@ import json
import logging
import os
import time
from urllib.parse import unquote
from collections import defaultdict
from aiohttp import web
@@ -356,6 +357,13 @@ def setup(server, store, hub):
messages, more = hub.chat.page(before, limit)
return web.json_response({"messages": messages, "more": more})
@routes.delete("/rigshare/api/chat")
async def clear_chat(request):
admin = require_admin(request)
await hub.clear_chat(admin.get("display_name") or admin["username"])
log.info(f"[RigShare] {admin['username']} cleared the chat history")
return web.json_response({"ok": True})
@routes.get("/rigshare/api/rooms")
async def list_rooms(request):
return web.json_response([r for r in hub.room_list() if room_access(request, hub.rooms[r["key"]])])
@@ -609,6 +617,34 @@ def setup(server, store, hub):
action = {"GET": "get", "HEAD": "get", "POST": "save", "DELETE": "delete"}.get(request.method)
return (action, src, None) if action else None
def api_path(request):
return request.path[4:] if request.path.startswith("/api/") else request.path
async def check_queue_ownership(request):
"""Cancelling a queued prompt or stopping the running one: your own, unless admin."""
if request.method != "POST" or api_path(request) not in ("/queue", "/interrupt"):
return None
if trusted(request.remote or "") or perms_for(request).get("admin"):
return None
user = current_user(request)
me = user["username"] if user else None
try:
body = await request.json() if request.body_exists else {}
except ValueError:
body = {}
body = body if isinstance(body, dict) else {}
if api_path(request) == "/queue":
if body.get("clear"):
return "only admins can clear the whole queue"
ids = body.get("delete") or []
else:
ids = [body["prompt_id"]] if body.get("prompt_id") else hub.running_prompt_ids()
for prompt_id in ids:
owner = hub.owner_of(prompt_id)
if owner and owner != me:
return "you can only cancel your own prompts"
return None
def snapshot_saved_file(request, path, body):
if not path.endswith(".json") or Workspace.workflows_rel(path) is None:
return
@@ -719,9 +755,19 @@ def setup(server, store, hub):
"details": "", "extra_info": {}}, "node_errors": {}},
status=403)
break
refusal = await check_queue_ownership(request)
if refusal:
return web.json_response({"error": f"RigShare: {refusal}"}, status=403)
if userdata and userdata[0] == "save":
await request.read() # keep the body for the snapshot below (aiohttp caches it)
response = await handler(request)
# Remember who queued each prompt, for the Queue tab.
if request.method == "POST" and api_path(request) == "/prompt" and getattr(response, "status", 0) == 200:
try:
prompt_id = json.loads(response.body).get("prompt_id")
except (ValueError, TypeError, AttributeError):
prompt_id = None
hub.record_prompt(prompt_id, current_user(request), unquote(request.headers.get("X-RigShare-Workflow", "")))
# Every save of a workflow keeps a snapshot, named after when it was saved.
if userdata and userdata[0] == "save" and getattr(response, "status", 0) == 200:
snapshot_saved_file(request, userdata[1], await request.read())
+18
View File
@@ -51,6 +51,23 @@ def _gpus():
return out
def queue_items(server):
"""Running and pending prompts: [{id, number, status, nodes}], running first."""
try:
running, pending = server.prompt_queue.get_current_queue_volatile()
except Exception:
return None
items = []
for status, entries in (("running", running), ("pending", sorted(pending, key=lambda e: e[0]))):
for entry in entries:
try:
prompt = entry[2] if isinstance(entry[2], dict) else {}
items.append({"id": entry[1], "number": entry[0], "status": status, "nodes": len(prompt)})
except (IndexError, TypeError):
continue
return items
def _queue(server):
try:
running, pending = server.prompt_queue.get_current_queue_volatile()
@@ -76,4 +93,5 @@ def collect_stats(server=None):
stats["gpus"] = []
if server is not None:
stats["queue"] = _queue(server)
stats["queue_items"] = queue_items(server)
return stats
+35 -4
View File
@@ -33,17 +33,28 @@ def enc(path):
return quote(path, safe="")
class FakeQueue:
"""ComfyUI's PromptQueue, as far as RigShare reads it."""
def __init__(self):
self.running, self.pending, self.number = [], [], 0
def get_current_queue_volatile(self):
return list(self.running), list(self.pending)
class FakePromptServer:
def __init__(self):
self.app = web.Application()
self.sent = []
self.prompt_queue = FakeQueue()
def send_sync(self, event, data, sid=None):
self.sent.append((event, sid))
def comfy_routes(user_root):
"""Minimal ComfyUI /userdata routes for the single default user."""
def comfy_routes(user_root, queue=None):
"""Minimal ComfyUI /userdata routes for the single default user, plus the queue."""
def resolve(name):
if "%" in name:
@@ -83,6 +94,24 @@ def comfy_routes(user_root):
async def ok(request):
return web.json_response({"ok": True})
async def prompt(request):
if request.method != "POST" or queue is None:
return web.json_response({"ok": True})
body = await request.json() if request.body_exists else {}
queue.number += 1
prompt_id = f"p{queue.number}"
queue.pending.append((queue.number, prompt_id, (body or {}).get("prompt") or {"1": {}}, {}, []))
return web.json_response({"prompt_id": prompt_id, "number": queue.number, "node_errors": {}})
async def queue_route(request):
body = await request.json() if request.body_exists else {}
if queue is not None:
gone = set((body or {}).get("delete") or [])
queue.pending = [e for e in queue.pending if e[1] not in gone]
if (body or {}).get("clear"):
queue.pending = []
return web.Response(status=200)
table = web.RouteTableDef()
for prefix in ("", "/api"):
table.get(prefix + "/userdata")(listing)
@@ -90,7 +119,9 @@ def comfy_routes(user_root):
table.post(prefix + "/userdata/{file}")(post)
table.delete(prefix + "/userdata/{file}")(delete)
table.post(prefix + "/userdata/{file}/move/{dest}")(move)
table.route("*", prefix + "/prompt")(ok)
table.route("*", prefix + "/prompt")(prompt)
table.post(prefix + "/queue")(queue_route)
table.post(prefix + "/interrupt")(ok)
table.route("*", prefix + "/manager/{tail:.*}")(ok)
table.route("*", prefix + "/customnode/{tail:.*}")(ok)
return table
@@ -184,7 +215,7 @@ async def rig_server(config=None, workspace=True, setup=None):
if workspace:
hub.workspace = Workspace(store, os.path.join(user_root, "workflows"))
routes.setup(ps, store, hub)
ps.app.add_routes(comfy_routes(user_root))
ps.app.add_routes(comfy_routes(user_root, ps.prompt_queue))
if setup:
setup(ps.app)
server = TestServer(ps.app, host="127.0.0.1")
+55
View File
@@ -440,3 +440,58 @@ def test_every_save_keeps_a_snapshot(run):
await h.post(rig.url(f"/api/userdata/{enc(path)}"), data=b'{"nodes": []}', headers=viewer)
assert len(rig.store.list_snapshots("file:" + path)) == 2
run(scenario())
def test_admin_clears_chat_history(run):
async def scenario():
async with rig_server() as rig:
admin = await rig.setup_admin()
alice = await rig.add_user("alice", edit=True)
sock = await rig.ws("alice")
for i in range(5):
await sock.send({"type": "chat", "text": f"hi {i}"})
await asyncio.sleep(0.2)
await sock.drain()
assert (await rig.http.delete(rig.url("/rigshare/api/chat"), headers=alice)).status == 403, "admins only"
assert (await rig.http.delete(rig.url("/rigshare/api/chat"), headers=admin)).status == 200
msgs = await sock.drain()
kinds = [m["type"] for m in msgs]
assert "chat_cleared" in kinds and kinds.index("chat_cleared") < kinds.index("chat")
notice = next(m for m in msgs if m["type"] == "chat")["message"]
assert notice["system"] and "cleared the chat history" in notice["text"] and notice["seq"] == 0
history = await (await rig.http.get(rig.url("/rigshare/api/chat"), headers=alice)).json()
assert [m["text"] for m in history["messages"]] == [notice["text"]] and not history["more"]
await sock.close()
run(scenario())
def test_queue_shows_who_launched_and_guards_cancelling(run):
async def scenario():
async with rig_server() as rig:
admin = await rig.setup_admin()
alice = await rig.add_user("alice", queue=True)
bob = await rig.add_user("bob", queue=True)
h = rig.http
async def queue(user, name):
r = await h.post(rig.url("/api/prompt"), json={"prompt": {"1": {}, "2": {}}},
headers={**user, "X-RigShare-Workflow": quote(name)})
return (await r.json())["prompt_id"]
a = await queue(alice, "Face swap é")
b = await queue(bob, "Upscale")
rig.ps.prompt_queue.running.append(rig.ps.prompt_queue.pending.pop(0)) # alice's starts
from rigshare_core.stats import queue_items
items = rig.hub.with_owners(queue_items(rig.ps))
assert [(i["status"], i["user"], i["workflow"], i["nodes"]) for i in items] == [
("running", "alice", "Face swap é", 2), ("pending", "bob", "Upscale", 2)]
# Your own runs only, unless admin; clearing everyone's is admin-only.
assert (await h.post(rig.url("/api/queue"), json={"delete": [b]}, headers=alice)).status == 403
assert (await h.post(rig.url("/api/interrupt"), json={}, headers=bob)).status == 403, "alice's is running"
assert (await h.post(rig.url("/api/interrupt"), json={"prompt_id": a}, headers=alice)).status == 200
assert (await h.post(rig.url("/api/queue"), json={"clear": True}, headers=bob)).status == 403
assert (await h.post(rig.url("/api/queue"), json={"delete": [b]}, headers=bob)).status == 200
assert not rig.ps.prompt_queue.pending
assert (await h.post(rig.url("/api/queue"), json={"clear": True}, headers=admin)).status == 200
run(scenario())
+26
View File
@@ -33,6 +33,9 @@ export class AppDock {
}));
this.dock.style.display = "none";
document.body.append(this.dock);
this.wasApp = false;
this.graphTab = null; // our tab open in the graph view, if any
this.restore = null; // { tabId, until }: reopen it after entering the App view
setInterval(() => this.update(), 300);
}
@@ -48,6 +51,7 @@ export class AppDock {
update() {
const active = this.sync.inAppMode;
this.keepOpenIntoApp(active);
const rail = active ? document.querySelector(RAIL) : null;
if (rail) this.addToRail(rail);
else this.removeFromRail();
@@ -55,6 +59,28 @@ export class AppDock {
this.refreshButtons();
}
/**
* ComfyUI closes the side panel when a tab enters the App view (not when it
* goes back to the graph). If a RigShare tab was open, open it again.
*/
keepOpenIntoApp(inApp) {
const open = this.activeTabId;
const ours = Object.values(this.views).map((v) => v.tabId);
if (!inApp) {
this.graphTab = ours.includes(open) ? open : null;
this.restore = null;
} else if (!this.wasApp && this.graphTab) {
this.restore = { tabId: this.graphTab, until: Date.now() + 1500 };
}
this.wasApp = inApp;
if (!this.restore) return;
if (open === this.restore.tabId || Date.now() > this.restore.until) {
this.restore = null;
} else if (open === null) {
this.app.extensionManager?.command?.execute(`Workspace.ToggleSidebarTab.${this.restore.tabId}`);
}
}
/** Copy one of ComfyUI's sidebar buttons for each view, placed after its own tabs. */
addToRail(rail) {
const ours = Object.values(this.railButtons);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 118 KiB

+5
View File
@@ -146,6 +146,11 @@ export class RigClient extends EventTarget {
this.stats = msg.stats;
this.emit("stats", msg.stats);
break;
case "chat_cleared":
this.chat = [];
this.chatMore = false;
this.emit("chat-history", this.chat);
break;
case "chat":
this.chat.push(msg.message);
this.emit("chat", msg.message);
+9 -3
View File
@@ -51,12 +51,18 @@ function setting(id, fallback) {
}
}
function installAuthHeaders(client) {
function installAuthHeaders(client, sync) {
const original = api.fetchApi.bind(api);
api.fetchApi = (route, options = {}) => {
let headers = options.headers || {};
if (headers instanceof Headers) headers = Object.fromEntries(headers.entries());
return original(route, { ...options, headers: { ...headers, ...client.authHeaders() } });
headers = { ...headers, ...client.authHeaders() };
// Queueing: name the workflow, so the Queue tab can show what each run is.
if (route === "/prompt" && (options.method || "GET").toUpperCase() === "POST") {
const wf = sync.store?.activeWorkflow;
if (wf) headers["X-RigShare-Workflow"] = encodeURIComponent(sync.tabName(wf));
}
return original(route, { ...options, headers });
};
}
@@ -297,7 +303,7 @@ app.registerExtension({
panel.chatToasts = setting("RigShare.ChatToasts", false);
client.on("toast", (t) => app.extensionManager?.toast?.add({ life: 4000, ...t }));
installAuthHeaders(client);
installAuthHeaders(client, sync);
installQueueGuard(client);
installManagerGuard(client);
installComfyAccountGuard(client);
+54 -2
View File
@@ -10,8 +10,9 @@ const ICON_URL = new URL("./assets/icon.svg", import.meta.url).href;
const VIEWS = [
{ id: "workflows", icon: "pi-share-alt", label: "Live" },
{ id: "people", icon: "pi-users", label: "People" },
{ id: "chat", icon: "pi-comments", label: "Chat" },
{ id: "people", icon: "pi-users", label: "People" },
{ id: "queue", icon: "pi-list", label: "Queue" },
{ id: "server", icon: "pi-server", label: "Server" },
{ id: "admin", icon: "pi-shield", label: "Admin", admin: true },
];
@@ -60,7 +61,10 @@ export class RigSharePanel {
client.on("status", () => this.render());
client.on("self", () => this.render());
client.on("presence", () => this.renderSoft());
client.on("stats", () => { if (this.view === "server") this.renderBody(); });
client.on("stats", () => {
if (this.view === "server" || this.view === "queue") this.renderBody();
this.renderNav(); // queue count
});
client.on("chat-history", () => this.renderChatLog());
client.on("chat", (m) => this.onChat(m));
sync.addEventListener("state", () => { if (this.view === "workflows") this.renderBody(); });
@@ -171,6 +175,7 @@ export class RigSharePanel {
people: c.users.length || null,
chat: this.unread || null,
workflows: c.rooms?.length || null,
queue: c.stats?.queue_items?.length || null,
};
this.nav.replaceChildren(...VIEWS.filter((v) => !v.admin || c.perms.admin).map((v) =>
h("button", {
@@ -199,6 +204,7 @@ export class RigSharePanel {
people: () => this.peopleView(),
chat: () => [this.chatView],
server: () => this.serverView(),
queue: () => this.queueView(),
admin: () => this.adminView(),
account: () => this.accountView(),
};
@@ -566,6 +572,44 @@ export class RigSharePanel {
// ----- server ---------------------------------------------------------
// ----- queue ----------------------------------------------------------
queueView() {
const c = this.client;
const items = c.stats?.queue_items;
if (items == null) return [this.section("pi-list", "Queue", h("p", { class: "rs-muted" }, "Waiting for the server…"))];
const running = items.filter((it) => it.status === "running");
const pending = items.filter((it) => it.status === "pending");
return [
this.section("pi-play", "Running", ...(running.length ? running.map((it) => this.queueRow(it)) : [h("p", { class: "rs-muted" }, "Nothing running.")])),
this.section("pi-list", `Pending${pending.length ? ` (${pending.length})` : ""}`,
...(pending.length ? pending.map((it, i) => this.queueRow(it, i + 1)) : [h("p", { class: "rs-muted" }, "Nothing waiting.")])),
];
}
queueRow(it, position) {
const c = this.client;
const online = it.user ? c.users.find((u) => u.key === it.user) : null;
const who = it.name || (it.user ? `@${it.user}` : "Unknown");
const mine = it.user && it.user === c.self?.key;
const canCancel = c.perms.queue && (mine || c.perms.admin);
const running = it.status === "running";
const meta = [position ? `#${position}` : null, `${it.nodes} nodes`, it.at ? `queued ${timeAgo(it.at * 1000)}` : null].filter(Boolean).join(" · ");
return h("div", { class: `rs-row-item rs-queue-row ${running ? "running" : ""}` },
h("span", { class: "rs-avatar", style: `background:${online?.color ?? "var(--rs-border)"}`, title: who }, initials(who)),
h("div", { class: "rs-grow rs-min0" },
h("div", { class: "rs-name rs-ellipsis" }, it.workflow || "Workflow",
running ? h("i", { class: "pi pi-spin pi-spinner rs-inline-icon rs-muted", title: "Running" }) : null),
h("div", { class: "rs-muted rs-small rs-ellipsis" }, `${who}${mine ? " (you)" : ""}`),
h("div", { class: "rs-muted rs-small" }, meta)),
canCancel ? h("button", {
class: "rs-btn rs-btn-icon rs-danger", title: running ? "Stop this run" : "Remove from the queue",
onclick: () => this.run(() => running
? c.request("POST", "/api/interrupt", { prompt_id: it.id })
: c.request("POST", "/api/queue", { delete: [it.id] }), running ? "Stopping…" : "Removed from the queue"),
}, running ? h("span", { class: "rs-stop-icon" }) : h("i", { class: "pi pi-times" })) : null);
}
serverView() {
const st = this.client.stats;
const floating = this.widget.enabled;
@@ -687,6 +731,14 @@ export class RigSharePanel {
return [
this.section("pi-users", "Accounts", this.adminAccounts, this.createAccountForm()),
this.section("pi-cog", "Server settings", this.adminSettings),
this.section("pi-comments", "Chat",
h("p", { class: "rs-muted rs-small" }, "The whole chat history is kept. Clearing it deletes every message for everyone; it cannot be undone."),
h("div", { class: "rs-actions" }, h("button", {
class: "rs-btn rs-danger", onclick: async () => {
if (!(await this.confirm("Clear chat history", "Delete every chat message for everyone? This cannot be undone."))) return;
await this.run(() => this.client.request("DELETE", "/rigshare/api/chat"), "Chat history cleared");
},
}, h("i", { class: "pi pi-trash" }), "Clear chat history"))),
];
}
+1
View File
@@ -263,6 +263,7 @@ body.rs-no-manager [aria-label="ComfyUI Manager"] { display: none !important; }
.rs-view-chip { display: inline-flex; align-items: center; gap: 3px; margin-left: 6px; font-size: 10.5px; padding: 0 6px; }
.rs-view-chip .rs-comfy-icon { font-size: 11px; }
.rs-view-chip.rs-view-app { color: var(--rs-accent); border-color: var(--rs-accent); }
.rs-stop-icon { display: block; width: 10px; height: 10px; border-radius: 2px; background: currentColor; }
.rs-cursor-layer { position: fixed; inset: 0; pointer-events: none; z-index: 900; overflow: hidden; }
.rs-cursor { position: absolute; left: 0; top: 0; will-change: transform; transition: transform 70ms linear; }
.rs-cursor svg { display: block; filter: drop-shadow(0 1px 1px rgb(0 0 0 / .4)); }