Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ or miss one that does. A test fails the build when it drifts.
| `moshcode start` | engines | launch an engine with its native defaults |
| `moshcode herd` | runtime | run agent sessions that outlive this terminal |
| `moshcode swarm` | runtime | one task, a herd of agents, one answer — plan, fan out, verify, synthesise (PRD 0015) |
| `moshcode omarchy` | runtime | the herd on the Omarchy bar: the snapshot a QML widget polls, and the plugin around it (PRD 0017) |
| `moshcode fleet` | runtime | the OpenFleet sysop tool: open a fleet, cap it, see the tree, stop a swarm, read the ledger (PRD 0016) |
| `moshcode ps` | runtime | list herd sessions and what each one is doing |
| `moshcode cost` <br>`usage` | runtime | what each session is spending, read from the engines' own logs |
Expand Down
8 changes: 8 additions & 0 deletions bin/moshcode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,14 @@ async function main() {
process.exitCode = (await fleetCommand(rest)) || 0;
return;
}
// The Omarchy bar (PRD 0017). `status` is the snapshot a QML widget polls;
// the rest is what makes the plugin shippable from a box that is not Omarchy.
// Lazy like swarm and fleet: a plain launch never reads a manifest.
if (cmd === "omarchy") {
const { omarchyCommand } = await import("../src/omarchy.mjs");
process.exitCode = (await omarchyCommand(rest)) || 0;
return;
}
if (["ps", "attach", "kill", "wait", "restore", "cost", "usage"].includes(cmd)) {
process.exitCode = (await herdCommand([cmd === "usage" ? "cost" : cmd, ...rest])) || 0;
return;
Expand Down
185 changes: 185 additions & 0 deletions omarchy/BarWidget.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// The bar item: one line that is always on screen (PRD 0017).
//
// It polls exactly one command, one at a time, and renders whatever comes back.
// It does not compute fleet state, it does not write anything, and it does not
// start a second process while the first is running. That restraint is the
// whole design: this runs unsandboxed inside Omarchy's shared, long-running
// Quickshell process, so a leaked process or an unguarded parse here is not a
// moshcode bug, it is everyone's bar falling over.
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Ui
import qs.Commons

import "Model.js" as Model

Item {
id: root

// What the shell expects of a bar widget that owns a panel.
property bool opened: false
property bool popoutSwitchClosing: false

// Tunables, all of them conservative.
//
// A poll is a `moshcode` process: node's own start-up plus the CLI's, which
// measured 0.66s to 4.2s on a developer box that was busy running the very
// agents this widget reports on. That is the floor, it is not work this
// plugin can avoid, and it is why the closed-panel poll is ten seconds and
// not one. The fast poll only runs while the panel is open, because that is
// the only time anyone is reading numbers that move.
property int idleIntervalMs: 10000
property int openIntervalMs: 3000
property int backoffIntervalMs: 60000
property int staleAfterMs: 30000
property real moneyFloor: 1.0
property int schema: 1

property var snapshot: null
property int failures: 0
property bool everRan: false

readonly property string widgetState: {
if (!everRan) return "unknown";
if (!snapshot) return Model.UNAVAILABLE;
return Model.state(snapshot);
}
readonly property bool isStale: snapshot ? Model.stale(snapshot, Date.now(), staleAfterMs) : false

implicitWidth: row.implicitWidth
implicitHeight: row.implicitHeight

// The theme decides the colours. A hard-coded colour is a widget that looks
// wrong on somebody else's theme, which is most people.
readonly property color fg: root.barForeground !== undefined ? root.barForeground : "white"
readonly property color accent: (typeof Colors !== "undefined" && Colors.accent !== undefined) ? Colors.accent : fg

Process {
id: poll
command: ["moshcode", "omarchy", "status", "--json"]
running: false

stdout: StdioCollector {
onStreamFinished: {
var parsed = Model.parse(this.text);
root.everRan = true;
if (parsed && Model.supported(parsed, root.schema)) {
root.snapshot = parsed;
root.failures = 0;
} else if (parsed) {
// A newer snapshot than this plugin knows. Say so; do not
// guess at fields that may have moved.
root.snapshot = { unavailable: true, reason: "snapshot schema " + parsed.schema + " is newer than this plugin" };
root.failures = 0;
} else {
root.failures += 1;
if (root.failures >= 3) root.snapshot = { unavailable: true, reason: "moshcode omarchy status returned nothing readable" };
}
timer.interval = root.pollInterval();
}
}

onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
root.everRan = true;
root.failures += 1;
if (root.failures >= 3) {
root.snapshot = { unavailable: true, reason: "moshcode is not installed, or is older than 0.104" };
}
timer.interval = root.pollInterval();
}
}
}

function pollInterval() {
if (failures >= 3) return backoffIntervalMs;
return opened ? openIntervalMs : idleIntervalMs;
}

// One process at a time. `poll.running` is the guard, not a mutex we keep
// ourselves, so a slow snapshot delays the next tick instead of stacking
// processes behind it.
Timer {
id: timer
interval: root.idleIntervalMs
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
if (!poll.running) poll.running = true;
}
}

onOpenedChanged: timer.interval = root.pollInterval()

Row {
id: row
anchors.verticalCenter: parent.verticalCenter
spacing: 6

Rectangle {
width: 8
height: 8
radius: 4
anchors.verticalCenter: parent.verticalCenter
color: root.widgetState === Model.BLOCKED ? root.accent : root.fg
opacity: {
if (root.widgetState === Model.UNAVAILABLE) return 0.35;
if (root.widgetState === Model.IDLE) return 0.5;
if (root.isStale) return 0.5;
return 1.0;
}

// The blocked state is the reason this plugin exists, so it is the
// one thing on the bar that moves.
SequentialAnimation on opacity {
running: root.widgetState === Model.BLOCKED
loops: Animation.Infinite
NumberAnimation { to: 0.35; duration: 900 }
NumberAnimation { to: 1.0; duration: 900 }
}
}

Text {
anchors.verticalCenter: parent.verticalCenter
text: root.everRan ? Model.label(root.snapshot, root.moneyFloor) : "moshcode …"
color: root.widgetState === Model.BLOCKED ? root.accent : root.fg
opacity: root.isStale ? 0.5 : 1.0
font.family: root.bar !== undefined && root.bar.fontFamily !== undefined ? root.bar.fontFamily : undefined
font.pixelSize: root.bar !== undefined && root.bar.fontPixelSize !== undefined ? root.bar.fontPixelSize : 13
}
}

MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: root.toggle()
}

Loader {
id: panelLoader
active: false
source: "Panel.qml"
onLoaded: {
item.widget = root;
item.open();
}
}

function open() {
if (!panelLoader.active) { panelLoader.active = true; return; }
if (panelLoader.item) panelLoader.item.open();
opened = true;
}

function close() {
if (panelLoader.item) panelLoader.item.close();
opened = false;
}

function toggle() {
if (opened) close();
else open();
}
}
21 changes: 21 additions & 0 deletions omarchy/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Profullstack, Inc. (dba moshcoding)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
118 changes: 118 additions & 0 deletions omarchy/Model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Shaping and formatting for the widget and the panel (PRD 0017).
//
// .pragma library: one copy, no QML context, no access to anything. Everything
// here is a pure function of the snapshot `moshcode omarchy status --json`
// printed, so the two surfaces cannot drift on what "blocked" or "$/h" means.
.pragma library

// The four states the widget has. Blocked outranks everything: it is the only
// one a human can clear, and the only one where the machine is spending a pane
// and a context window on nothing at all.
var UNAVAILABLE = "unavailable";
var BLOCKED = "blocked";
var BUSY = "busy";
var IDLE = "idle";

function parse(text) {
// A bad parse is a state, not an exception. Anything thrown from here ends
// up in Omarchy's shared shell process, which is everyone's bar.
try {
var snap = JSON.parse(text);
if (!snap || typeof snap !== "object") return null;
return snap;
} catch (e) {
return null;
}
}

// The plugin renders a schema it knows and says so about anything newer,
// rather than reading fields that may have moved underneath it.
function supported(snap, schema) {
return !!snap && snap.schema === schema;
}

function counts(snap) {
var c = (snap && snap.counts) || {};
return {
live: c.live || 0,
working: c.working || 0,
blocked: c.blocked || 0,
idle: c.idle || 0,
done: c.done || 0
};
}

function window(snap, key) {
var rows = (snap && snap.burn) || [];
for (var i = 0; i < rows.length; i++) {
if (rows[i] && rows[i].key === key) return rows[i];
}
return null;
}

function perHour(snap) {
var row = window(snap, "1h");
return row && row.perHour !== null && row.perHour !== undefined ? row.perHour : null;
}

function state(snap, staleMs) {
if (!snap) return UNAVAILABLE;
if (snap.unavailable) return UNAVAILABLE;
var c = counts(snap);
if (c.blocked > 0) return BLOCKED;
if (c.working > 0 || c.live > 0) return BUSY;
return IDLE;
}

function stale(snap, now, staleMs) {
if (!snap || !snap.generatedAt) return false;
var at = Date.parse(snap.generatedAt);
if (isNaN(at)) return false;
return (now - at) > staleMs;
}

function money(value) {
if (value === null || value === undefined) return "";
if (value >= 100) return "$" + Math.round(value);
if (value >= 10) return "$" + value.toFixed(1);
return "$" + value.toFixed(2);
}

// One line, read at a glance, in the order you care about it: what needs you,
// then what is running, then what it costs.
function label(snap, floor) {
if (!snap) return "moshcode ?";
if (snap.unavailable) return "moshcode —";
var c = counts(snap);
var parts = [];
if (c.blocked > 0) parts.push(c.blocked + " blocked");
parts.push(c.live + " agent" + (c.live === 1 ? "" : "s"));
var rate = perHour(snap);
if (rate !== null && rate >= floor) parts.push(money(rate) + "/h");
return parts.join(" · ");
}

function age(ms) {
if (ms === null || ms === undefined) return "";
var s = Math.round(ms / 1000);
if (s < 60) return s + "s";
var m = Math.round(s / 60);
if (m < 60) return m + "m";
var h = Math.floor(m / 60);
return h + "h" + (m % 60) + "m";
}

// The cwd matters for telling two `claude` sessions apart, and the head of it
// never does. Keep the tail.
function tail(cwd, keep) {
if (!cwd) return "";
var parts = String(cwd).split("/").filter(function (p) { return p.length > 0; });
if (parts.length <= keep) return cwd;
return "…/" + parts.slice(parts.length - keep).join("/");
}

function agentLine(agent) {
var bits = [agent.name, agent.engine, agent.state];
if (agent.blockedOn) bits[2] = agent.state + ":" + agent.blockedOn;
return bits.join(" ");
}
Loading
Loading