@starlove/orbit 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +13 -0
- package/Dockerfile +34 -0
- package/README.md +216 -0
- package/SKILL-ORBIT.md +94 -0
- package/build.mjs +204 -0
- package/dist/full/apple-touch-icon.png +0 -0
- package/dist/full/cursors/live_arrow.cur +0 -0
- package/dist/full/cursors/live_link.cur +0 -0
- package/dist/full/cursors/live_move.cur +0 -0
- package/dist/full/cursors/live_select.cur +0 -0
- package/dist/full/cursors/live_unavail.cur +0 -0
- package/dist/full/favicon-32x32.png +0 -0
- package/dist/full/index.html +314 -0
- package/dist/full/js/app.70be7d06.js +554 -0
- package/dist/full/js/app.70be7d06.js.map +7 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/styles.c2e3d20a.css +4892 -0
- package/dist/full/version.json +6 -0
- package/docs/AGENT_PROTOCOL.md +379 -0
- package/docs/MCP_SETUP.md +49 -0
- package/docs/mcp-client.example.json +13 -0
- package/package.json +62 -0
- package/pnpm-workspace.yaml +2 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/index.html +314 -0
- package/public/js/api.js +35 -0
- package/public/js/app.js +35 -0
- package/public/js/board-menu.js +145 -0
- package/public/js/config.js +19 -0
- package/public/js/create-card.js +153 -0
- package/public/js/dom.js +19 -0
- package/public/js/drawer.js +137 -0
- package/public/js/format.js +108 -0
- package/public/js/kanban.js +475 -0
- package/public/js/local-backend.js +934 -0
- package/public/js/main.js +142 -0
- package/public/js/modal.js +31 -0
- package/public/js/router.js +114 -0
- package/public/js/search.js +53 -0
- package/public/js/settings-tabs.js +7 -0
- package/public/js/settings.js +931 -0
- package/public/js/sse.js +156 -0
- package/public/js/starscape.js +315 -0
- package/public/js/state.js +54 -0
- package/public/js/ticket-detail.js +766 -0
- package/public/js/toast.js +74 -0
- package/public/js/transport.js +22 -0
- package/public/js/unread.js +121 -0
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +4892 -0
- package/src/cli/orbit.js +438 -0
- package/src/core/agent.js +262 -0
- package/src/core/auth.js +49 -0
- package/src/core/backups.js +136 -0
- package/src/core/board.js +112 -0
- package/src/core/boards.js +310 -0
- package/src/core/bootstrap.js +162 -0
- package/src/core/db.js +237 -0
- package/src/core/delete-board.js +53 -0
- package/src/core/events.js +314 -0
- package/src/core/http.js +81 -0
- package/src/core/paths.js +25 -0
- package/src/core/provision-repo-board.js +113 -0
- package/src/core/queries.js +246 -0
- package/src/core/registry.js +207 -0
- package/src/core/relations.js +163 -0
- package/src/core/router.js +414 -0
- package/src/core/search.js +70 -0
- package/src/core/seed.js +226 -0
- package/src/core/snapshots.js +255 -0
- package/src/core/states.js +167 -0
- package/src/core/system-picker.js +93 -0
- package/src/core/tickets.js +344 -0
- package/src/core/util.js +105 -0
- package/src/mcp-server.js +773 -0
- package/src/server.js +48 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Application entry point. Imports `starscape.js` for its side effects
|
|
2
|
+
// (canvas + theme toggle init), wires the topbar buttons and global
|
|
3
|
+
// keyboard/click listeners, and kicks off the bootstrap → router activation
|
|
4
|
+
// sequence. The reusable `load` / `render` helpers live in app.js so feature
|
|
5
|
+
// modules can import them without creating cycles back through this entry.
|
|
6
|
+
|
|
7
|
+
import "./starscape.js";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
$,
|
|
11
|
+
boardMenuBtn,
|
|
12
|
+
boardFlyout,
|
|
13
|
+
drawer,
|
|
14
|
+
drawerBackdrop,
|
|
15
|
+
createFlyoutBackdrop,
|
|
16
|
+
searchInput
|
|
17
|
+
} from "./dom.js";
|
|
18
|
+
import { state } from "./state.js";
|
|
19
|
+
import { api, withBoardQuery } from "./api.js";
|
|
20
|
+
import { toast, debounce } from "./toast.js";
|
|
21
|
+
import { navigate, applyRoute, syncUrlFromState, setRouterReady } from "./router.js";
|
|
22
|
+
import {
|
|
23
|
+
openBoardFlyout,
|
|
24
|
+
closeBoardFlyout,
|
|
25
|
+
closeMenuFlyouts
|
|
26
|
+
} from "./board-menu.js";
|
|
27
|
+
import {
|
|
28
|
+
openCreateFlyout,
|
|
29
|
+
closeCreateFlyout,
|
|
30
|
+
isCreateFlyoutOpen
|
|
31
|
+
} from "./create-card.js";
|
|
32
|
+
import { enableKanbanDragScroll } from "./kanban.js";
|
|
33
|
+
import { runSearch, hideSearch } from "./search.js";
|
|
34
|
+
import { load } from "./app.js";
|
|
35
|
+
import { edition } from "./config.js";
|
|
36
|
+
import { openModal, wireModal } from "./modal.js";
|
|
37
|
+
|
|
38
|
+
async function init() {
|
|
39
|
+
const welcomeDialog = document.getElementById("welcomeDialog");
|
|
40
|
+
wireModal(welcomeDialog);
|
|
41
|
+
if (edition === "preview") {
|
|
42
|
+
openModal(welcomeDialog);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
$("#settingsBtn").addEventListener("click", async () => {
|
|
46
|
+
closeCreateFlyout();
|
|
47
|
+
closeMenuFlyouts();
|
|
48
|
+
navigate({
|
|
49
|
+
boardId: state.boardId,
|
|
50
|
+
view: "settings",
|
|
51
|
+
tab: state.settingsTab || "lanes"
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
boardMenuBtn?.addEventListener("click", (event) => {
|
|
56
|
+
event.stopPropagation();
|
|
57
|
+
if (!boardFlyout.hidden) {
|
|
58
|
+
closeBoardFlyout();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
openBoardFlyout();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
$("#newTicketBtn").addEventListener("click", () => {
|
|
65
|
+
if (isCreateFlyoutOpen()) {
|
|
66
|
+
closeCreateFlyout();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
openCreateFlyout("");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
createFlyoutBackdrop.addEventListener("click", () => {
|
|
73
|
+
closeCreateFlyout();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
$("#drawerCloseBtn").addEventListener("click", () => {
|
|
77
|
+
navigate({
|
|
78
|
+
boardId: state.boardId,
|
|
79
|
+
view: "board"
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
$("#drawerArchiveBtn").addEventListener("click", async () => {
|
|
84
|
+
const ticketId = state.selectedTicketId;
|
|
85
|
+
if (!ticketId || state.detailMode !== "ticket") return;
|
|
86
|
+
const ticket = state.data?.tickets?.find((t) => t.id === ticketId);
|
|
87
|
+
const titleForToast = ticket?.title || "card";
|
|
88
|
+
await api(withBoardQuery(`/api/tickets/${encodeURIComponent(ticketId)}/archive`), { method: "POST" });
|
|
89
|
+
toast.error(`Archived: ${titleForToast}`);
|
|
90
|
+
await load();
|
|
91
|
+
navigate({ boardId: state.boardId, view: "board" });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
drawerBackdrop.addEventListener("click", () => {
|
|
95
|
+
navigate({
|
|
96
|
+
boardId: state.boardId,
|
|
97
|
+
view: "board"
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
document.addEventListener("keydown", (event) => {
|
|
102
|
+
if (event.key !== "Escape") return;
|
|
103
|
+
if (!boardFlyout.hidden) {
|
|
104
|
+
closeMenuFlyouts();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (isCreateFlyoutOpen()) {
|
|
108
|
+
closeCreateFlyout();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!drawer.classList.contains("is-open")) return;
|
|
112
|
+
const el = document.activeElement;
|
|
113
|
+
if (el?.classList?.contains("inline-title-editor") || el?.classList?.contains("inline-desc-editor")) return;
|
|
114
|
+
navigate({
|
|
115
|
+
boardId: state.boardId,
|
|
116
|
+
view: "board"
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
searchInput.addEventListener("input", debounce(runSearch, 220));
|
|
121
|
+
document.addEventListener("click", (event) => {
|
|
122
|
+
if (!event.target.closest("#topbarSearch")) hideSearch();
|
|
123
|
+
if (!event.target.closest(".topbar-menu")) closeMenuFlyouts();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
enableKanbanDragScroll();
|
|
127
|
+
|
|
128
|
+
await load();
|
|
129
|
+
|
|
130
|
+
// Activate the router. If the page was loaded with a hash, restore that
|
|
131
|
+
// view; otherwise canonicalize the URL to reflect the current state so the
|
|
132
|
+
// back button has a starting point.
|
|
133
|
+
setRouterReady(true);
|
|
134
|
+
const initialHash = location.hash;
|
|
135
|
+
if (initialHash && initialHash !== "#" && initialHash !== "#/") {
|
|
136
|
+
await applyRoute();
|
|
137
|
+
} else {
|
|
138
|
+
syncUrlFromState({ replace: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
init();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Generic <dialog> modal utility.
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// wireModal(el) — wire backdrop-click + [data-modal-close] buttons.
|
|
5
|
+
// openModal(el) — show the dialog as a modal (focus-trapped overlay).
|
|
6
|
+
// closeModal(el) — close it programmatically.
|
|
7
|
+
//
|
|
8
|
+
// Escape key is handled natively by the browser; no extra wiring needed.
|
|
9
|
+
|
|
10
|
+
export function openModal(dialogEl) {
|
|
11
|
+
dialogEl?.showModal();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function closeModal(dialogEl) {
|
|
15
|
+
if (dialogEl?.open) dialogEl.close();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function wireModal(dialogEl) {
|
|
19
|
+
if (!dialogEl) return;
|
|
20
|
+
|
|
21
|
+
// Close when the user clicks the backdrop (the dialog element itself,
|
|
22
|
+
// outside its inner content box).
|
|
23
|
+
dialogEl.addEventListener("click", (event) => {
|
|
24
|
+
if (event.target === dialogEl) closeModal(dialogEl);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Close buttons: any element inside the dialog with [data-modal-close].
|
|
28
|
+
dialogEl.querySelectorAll("[data-modal-close]").forEach((btn) => {
|
|
29
|
+
btn.addEventListener("click", () => closeModal(dialogEl));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Hash-based router. The URL hash is the source of truth for which view is
|
|
2
|
+
// shown. `navigate()` writes to it; the popstate listener replays it. The
|
|
3
|
+
// in-memory `state` object is still used for rendering — applyRoute() just
|
|
4
|
+
// mirrors the parsed hash into `state` before calling render().
|
|
5
|
+
|
|
6
|
+
import { state } from "./state.js";
|
|
7
|
+
import { SETTINGS_TAB_IDS, normalizeSettingsTab } from "./settings-tabs.js";
|
|
8
|
+
import { closeMenuFlyouts } from "./board-menu.js";
|
|
9
|
+
import { closeCreateFlyout } from "./create-card.js";
|
|
10
|
+
|
|
11
|
+
const Route = {
|
|
12
|
+
parse(hash) {
|
|
13
|
+
const cleaned = String(hash || "").replace(/^#/, "").replace(/^\/+/, "");
|
|
14
|
+
const parts = cleaned ? cleaned.split("/") : [];
|
|
15
|
+
const out = { boardId: "", view: "board", ticketId: "", tab: "" };
|
|
16
|
+
let i = 0;
|
|
17
|
+
if (parts[i] === "b" && parts[i + 1]) { out.boardId = decodeURIComponent(parts[i + 1]); i += 2; }
|
|
18
|
+
if (parts[i] === "t" && parts[i + 1]) {
|
|
19
|
+
out.view = "ticket";
|
|
20
|
+
out.ticketId = decodeURIComponent(parts[i + 1]);
|
|
21
|
+
} else if (parts[i] === "settings") {
|
|
22
|
+
out.view = "settings";
|
|
23
|
+
const t = parts[i + 1];
|
|
24
|
+
if (t && SETTINGS_TAB_IDS.has(t)) out.tab = t;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
},
|
|
28
|
+
build({ boardId = "", view = "board", ticketId = "", tab = "" } = {}) {
|
|
29
|
+
const segs = [];
|
|
30
|
+
if (boardId) segs.push("b", encodeURIComponent(boardId));
|
|
31
|
+
if (view === "ticket" && ticketId) segs.push("t", encodeURIComponent(ticketId));
|
|
32
|
+
else if (view === "settings") {
|
|
33
|
+
segs.push("settings");
|
|
34
|
+
if (tab && SETTINGS_TAB_IDS.has(tab)) segs.push(tab);
|
|
35
|
+
}
|
|
36
|
+
return "#/" + segs.join("/");
|
|
37
|
+
},
|
|
38
|
+
fromState() {
|
|
39
|
+
let view = "board";
|
|
40
|
+
if (state.detailMode === "ticket" && state.selectedTicketId) view = "ticket";
|
|
41
|
+
else if (state.detailMode === "settings") view = "settings";
|
|
42
|
+
return {
|
|
43
|
+
boardId: state.boardId,
|
|
44
|
+
view,
|
|
45
|
+
ticketId: state.selectedTicketId,
|
|
46
|
+
tab: view === "settings" ? normalizeSettingsTab(state.settingsTab) : ""
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let routerReady = false;
|
|
52
|
+
|
|
53
|
+
export function setRouterReady(value) {
|
|
54
|
+
routerReady = value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function navigate(routeObj, { replace = false } = {}) {
|
|
58
|
+
const hash = Route.build(routeObj);
|
|
59
|
+
if (location.hash === hash) {
|
|
60
|
+
if (routerReady) await applyRoute();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (replace) history.replaceState(null, "", hash);
|
|
64
|
+
else history.pushState(null, "", hash);
|
|
65
|
+
if (routerReady) await applyRoute();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Push or replace the URL to match the current `state` without re-rendering.
|
|
69
|
+
// Used after flows that already mutated state and called load()/render()
|
|
70
|
+
// (e.g. ticket/board creation).
|
|
71
|
+
export function syncUrlFromState({ replace = false } = {}) {
|
|
72
|
+
const hash = Route.build(Route.fromState());
|
|
73
|
+
if (location.hash === hash) return;
|
|
74
|
+
if (replace) history.replaceState(null, "", hash);
|
|
75
|
+
else history.pushState(null, "", hash);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function applyRoute() {
|
|
79
|
+
const { load, render } = await import("./app.js");
|
|
80
|
+
const r = Route.parse(location.hash);
|
|
81
|
+
const prevBoardId = state.boardId;
|
|
82
|
+
if (r.boardId) state.boardId = r.boardId;
|
|
83
|
+
|
|
84
|
+
if (r.view === "ticket") {
|
|
85
|
+
state.selectedTicketId = r.ticketId || "";
|
|
86
|
+
state.detailMode = r.ticketId ? "ticket" : "empty";
|
|
87
|
+
} else if (r.view === "settings") {
|
|
88
|
+
state.selectedTicketId = "";
|
|
89
|
+
state.detailMode = "settings";
|
|
90
|
+
if (r.tab) {
|
|
91
|
+
state.settingsTab = r.tab;
|
|
92
|
+
try {
|
|
93
|
+
localStorage.setItem("mab_settings_tab", r.tab);
|
|
94
|
+
} catch {
|
|
95
|
+
/* ignore */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
state.selectedTicketId = "";
|
|
100
|
+
state.detailMode = "empty";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
closeMenuFlyouts();
|
|
104
|
+
closeCreateFlyout();
|
|
105
|
+
|
|
106
|
+
if (r.boardId && r.boardId !== prevBoardId) {
|
|
107
|
+
await load();
|
|
108
|
+
}
|
|
109
|
+
await render();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
window.addEventListener("popstate", () => {
|
|
113
|
+
if (routerReady) void applyRoute();
|
|
114
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Topbar search. Debounced via the input handler in main.js; queries the
|
|
2
|
+
// /api/search endpoint and renders a small results dropdown that navigates
|
|
3
|
+
// to the chosen ticket on click.
|
|
4
|
+
|
|
5
|
+
import { searchInput, searchResults } from "./dom.js";
|
|
6
|
+
import { state, currentBoard } from "./state.js";
|
|
7
|
+
import { api } from "./api.js";
|
|
8
|
+
import { navigate } from "./router.js";
|
|
9
|
+
import { escapeHtml, ticketLabel } from "./format.js";
|
|
10
|
+
import { closeCreateFlyout } from "./create-card.js";
|
|
11
|
+
|
|
12
|
+
export async function runSearch() {
|
|
13
|
+
const query = searchInput.value.trim();
|
|
14
|
+
if (query.length < 2) {
|
|
15
|
+
hideSearch();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const board = currentBoard();
|
|
19
|
+
if (!board) return;
|
|
20
|
+
const result = await api(`/api/search?q=${encodeURIComponent(query)}&board=${encodeURIComponent(board.slug)}`);
|
|
21
|
+
if (result.results.length === 0) {
|
|
22
|
+
searchResults.hidden = false;
|
|
23
|
+
searchResults.innerHTML = `<div class="search-hit">No results</div>`;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
searchResults.hidden = false;
|
|
27
|
+
searchResults.innerHTML = result.results
|
|
28
|
+
.map(
|
|
29
|
+
(ticket) => `
|
|
30
|
+
<div class="search-hit" data-ticket-id="${ticket.id}">
|
|
31
|
+
<strong>${escapeHtml(ticketLabel(ticket))}</strong>
|
|
32
|
+
${escapeHtml(ticket.title)}
|
|
33
|
+
</div>
|
|
34
|
+
`
|
|
35
|
+
)
|
|
36
|
+
.join("");
|
|
37
|
+
searchResults.querySelectorAll("[data-ticket-id]").forEach((hit) => {
|
|
38
|
+
hit.addEventListener("click", async () => {
|
|
39
|
+
closeCreateFlyout();
|
|
40
|
+
hideSearch();
|
|
41
|
+
navigate({
|
|
42
|
+
boardId: state.boardId,
|
|
43
|
+
view: "ticket",
|
|
44
|
+
ticketId: hit.dataset.ticketId
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function hideSearch() {
|
|
51
|
+
searchResults.hidden = true;
|
|
52
|
+
searchResults.innerHTML = "";
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Settings drawer tab ids — must match URL segments `#/b/…/settings/:tab` and `settings.js` tab config. */
|
|
2
|
+
export const SETTINGS_TAB_IDS = new Set(["lanes", "ai", "notes", "journal", "repository", "archive"]);
|
|
3
|
+
|
|
4
|
+
export function normalizeSettingsTab(raw) {
|
|
5
|
+
if (!raw || !SETTINGS_TAB_IDS.has(raw)) return "lanes";
|
|
6
|
+
return raw;
|
|
7
|
+
}
|