@starlove/orbit 1.1.2 → 1.1.11
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 +0 -0
- package/Dockerfile +0 -0
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/SKILL-ORBIT.md +2 -0
- package/build.mjs +0 -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 +160 -15
- package/dist/full/js/app.55b38e12.js +698 -0
- package/dist/full/js/app.55b38e12.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.836872e7.css → styles.273fc37a.css} +79 -39
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +0 -0
- package/docs/MCP_SETUP.md +0 -0
- package/docs/ORBIT_DISPATCH.md +68 -8
- package/docs/SS-Orbit-Dark.png +0 -0
- package/docs/SS-Orbit-Light.png +0 -0
- package/docs/mcp-client.example.json +0 -0
- package/docs/setup/orbit-init-wizard.md +0 -0
- package/package.json +1 -1
- 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 +158 -13
- package/public/js/actor-labels.js +0 -0
- package/public/js/api.js +0 -0
- package/public/js/app.js +0 -0
- package/public/js/board-menu.js +0 -0
- package/public/js/card-actions.js +19 -0
- package/public/js/config.js +0 -0
- package/public/js/create-card.js +0 -0
- package/public/js/dom.js +0 -0
- package/public/js/drawer.js +17 -43
- package/public/js/format.js +5 -5
- package/public/js/kanban.js +157 -92
- package/public/js/local-backend.js +0 -0
- package/public/js/main.js +2 -1
- package/public/js/modal.js +0 -0
- package/public/js/motion-preference.js +14 -3
- package/public/js/router.js +0 -0
- package/public/js/search.js +0 -0
- package/public/js/settings-tabs.js +0 -0
- package/public/js/settings.js +131 -27
- package/public/js/sse.js +0 -0
- package/public/js/starscape.js +0 -0
- package/public/js/state.js +0 -0
- package/public/js/theme-preference.js +0 -0
- package/public/js/ticket-detail.js +88 -20
- package/public/js/toast.js +0 -0
- package/public/js/transport.js +0 -0
- package/public/js/unread.js +0 -0
- package/public/js/url-routes.js +0 -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 +79 -39
- package/src/cli/orbit.js +22 -0
- package/src/core/agent.js +0 -0
- package/src/core/agents-md.js +0 -0
- package/src/core/attachments.js +0 -0
- package/src/core/auth.js +0 -0
- package/src/core/backups.js +0 -0
- package/src/core/board.js +1 -0
- package/src/core/boards.js +0 -0
- package/src/core/bootstrap.js +0 -0
- package/src/core/db.js +19 -0
- package/src/core/delete-board.js +0 -0
- package/src/core/dispatch.js +176 -11
- package/src/core/events.js +0 -0
- package/src/core/http.js +0 -0
- package/src/core/migrate.js +0 -0
- package/src/core/paths.js +0 -0
- package/src/core/provision-repo-board.js +0 -0
- package/src/core/queries.js +0 -0
- package/src/core/registry.js +2 -2
- package/src/core/relations.js +0 -0
- package/src/core/review-verdicts.js +138 -0
- package/src/core/router.js +32 -3
- package/src/core/search.js +0 -0
- package/src/core/seed.js +0 -0
- package/src/core/snapshots.js +429 -9
- package/src/core/states.js +0 -0
- package/src/core/system-picker.js +0 -0
- package/src/core/tickets.js +0 -0
- package/src/core/util.js +0 -0
- package/src/mcp/orbit-client.js +45 -19
- package/src/mcp-server.js +102 -27
- package/src/server.js +0 -0
- package/dist/full/js/app.a4f045fc.js +0 -676
- package/dist/full/js/app.a4f045fc.js.map +0 -7
package/public/js/starscape.js
CHANGED
|
File without changes
|
package/public/js/state.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -39,15 +39,15 @@ import { formatActorLabel, formatCommentAuthor } from "./actor-labels.js";
|
|
|
39
39
|
* cache; otherwise leave the board DOM alone.
|
|
40
40
|
*/
|
|
41
41
|
async function refreshTicketDetail(ticketId, { renderBoardAfter = false } = {}) {
|
|
42
|
-
state.selectedTicketId = ticketId;
|
|
43
|
-
state.detailMode = "ticket";
|
|
44
42
|
const [context, statusHistory, commentsResult] = await Promise.all([
|
|
45
43
|
api(withBoardQuery(`/api/tickets/${ticketId}/context?depth=1`)),
|
|
46
44
|
api(withBoardQuery(`/api/tickets/${ticketId}/history`)),
|
|
47
45
|
api(withBoardQuery(`/api/tickets/${ticketId}/comments`))
|
|
48
46
|
]);
|
|
47
|
+
if (context.ticket?.id !== ticketId) return;
|
|
49
48
|
upsertTicket(context.ticket);
|
|
50
49
|
if (renderBoardAfter) renderBoard();
|
|
50
|
+
if (state.detailMode !== "ticket" || state.selectedTicketId !== ticketId) return;
|
|
51
51
|
await renderDetail({ context, statusHistory, comments: commentsResult.comments || [] });
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -71,11 +71,62 @@ async function patchTicket(ticketId, body) {
|
|
|
71
71
|
* - initialValue: text shown in the editor on open.
|
|
72
72
|
* - ariaLabel: full label for screen readers.
|
|
73
73
|
* - emptyMessage: if set, an empty trimmed value triggers a warning toast and cancels.
|
|
74
|
+
* - sourceEvent: optional pointer/click event used to seed the textarea caret near the clicked text.
|
|
74
75
|
* - rerender: async fn called on cancel / no-change to put the static node back.
|
|
75
76
|
* - commit: async fn called with the new value when the user accepts a real change.
|
|
76
77
|
* Receives the *raw* editor value; the caller is responsible for any
|
|
77
78
|
* trimming, cleanText, or PATCH wiring.
|
|
78
79
|
*/
|
|
80
|
+
function inlineTextOffsetFromPoint(node, sourceEvent, maxLength) {
|
|
81
|
+
if (!node || !sourceEvent || typeof sourceEvent.clientX !== "number" || typeof sourceEvent.clientY !== "number") return null;
|
|
82
|
+
|
|
83
|
+
let range = null;
|
|
84
|
+
if (document.caretPositionFromPoint) {
|
|
85
|
+
const position = document.caretPositionFromPoint(sourceEvent.clientX, sourceEvent.clientY);
|
|
86
|
+
if (position?.offsetNode && (position.offsetNode === node || node.contains(position.offsetNode))) {
|
|
87
|
+
range = document.createRange();
|
|
88
|
+
range.setStart(node, 0);
|
|
89
|
+
range.setEnd(position.offsetNode, position.offset);
|
|
90
|
+
}
|
|
91
|
+
} else if (document.caretRangeFromPoint) {
|
|
92
|
+
const caretRange = document.caretRangeFromPoint(sourceEvent.clientX, sourceEvent.clientY);
|
|
93
|
+
if (caretRange?.startContainer && (caretRange.startContainer === node || node.contains(caretRange.startContainer))) {
|
|
94
|
+
range = document.createRange();
|
|
95
|
+
range.setStart(node, 0);
|
|
96
|
+
range.setEnd(caretRange.startContainer, caretRange.startOffset);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!range) return null;
|
|
101
|
+
return Math.max(0, Math.min(maxLength, range.toString().length));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function captureInlineEditScrollState(node) {
|
|
105
|
+
const scrollContainer = node?.closest?.(".drawer-inner");
|
|
106
|
+
if (!scrollContainer) return null;
|
|
107
|
+
return {
|
|
108
|
+
scrollContainer,
|
|
109
|
+
nodeRect: node.getBoundingClientRect(),
|
|
110
|
+
scrollTop: scrollContainer.scrollTop
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function restoreInlineEditScroll(scrollState, editor) {
|
|
115
|
+
if (!scrollState?.scrollContainer || !editor?.isConnected) return;
|
|
116
|
+
const editorRect = editor.getBoundingClientRect();
|
|
117
|
+
const nextScrollTop = Math.max(0, scrollState.scrollContainer.scrollTop + (editorRect.top - scrollState.nodeRect.top));
|
|
118
|
+
if (Number.isFinite(nextScrollTop)) scrollState.scrollContainer.scrollTop = nextScrollTop;
|
|
119
|
+
else scrollState.scrollContainer.scrollTop = scrollState.scrollTop;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function focusWithoutScrolling(editor) {
|
|
123
|
+
try {
|
|
124
|
+
editor.focus({ preventScroll: true });
|
|
125
|
+
} catch {
|
|
126
|
+
editor.focus();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
79
130
|
export function startInlineEdit(node, opts) {
|
|
80
131
|
if (!node || node.dataset.editing === "true") return;
|
|
81
132
|
const {
|
|
@@ -84,15 +135,20 @@ export function startInlineEdit(node, opts) {
|
|
|
84
135
|
initialValue = "",
|
|
85
136
|
ariaLabel,
|
|
86
137
|
emptyMessage,
|
|
138
|
+
sourceEvent,
|
|
87
139
|
rerender,
|
|
88
140
|
commit: commitHandler
|
|
89
141
|
} = opts;
|
|
90
142
|
|
|
143
|
+
const scrollState = multiline ? captureInlineEditScrollState(node) : null;
|
|
144
|
+
const initialString = String(initialValue);
|
|
145
|
+
const clickedCaretOffset = multiline ? inlineTextOffsetFromPoint(node, sourceEvent, initialString.length) : null;
|
|
146
|
+
|
|
91
147
|
node.dataset.editing = "true";
|
|
92
148
|
|
|
93
149
|
const editor = document.createElement(multiline ? "textarea" : "input");
|
|
94
150
|
editor.className = multiline ? "inline-desc-editor" : "inline-title-editor";
|
|
95
|
-
editor.value =
|
|
151
|
+
editor.value = initialString;
|
|
96
152
|
if (fieldName) editor.setAttribute("data-editing-field", fieldName);
|
|
97
153
|
if (ariaLabel) editor.setAttribute("aria-label", ariaLabel);
|
|
98
154
|
if (!multiline) {
|
|
@@ -101,13 +157,22 @@ export function startInlineEdit(node, opts) {
|
|
|
101
157
|
editor.autocomplete = "off";
|
|
102
158
|
editor.spellcheck = true;
|
|
103
159
|
} else {
|
|
104
|
-
editor.rows = Math.max(3,
|
|
160
|
+
editor.rows = Math.max(3, initialString.split("\n").length + 1);
|
|
161
|
+
const sourceHeight = node.getBoundingClientRect().height;
|
|
162
|
+
if (Number.isFinite(sourceHeight) && sourceHeight > 0) {
|
|
163
|
+
editor.style.minHeight = `${Math.ceil(sourceHeight)}px`;
|
|
164
|
+
}
|
|
105
165
|
}
|
|
106
166
|
|
|
107
167
|
node.replaceWith(editor);
|
|
108
|
-
editor
|
|
109
|
-
if (!multiline)
|
|
110
|
-
|
|
168
|
+
focusWithoutScrolling(editor);
|
|
169
|
+
if (!multiline) {
|
|
170
|
+
editor.select();
|
|
171
|
+
} else {
|
|
172
|
+
const selectionStart = clickedCaretOffset ?? 0;
|
|
173
|
+
editor.setSelectionRange(selectionStart, selectionStart);
|
|
174
|
+
restoreInlineEditScroll(scrollState, editor);
|
|
175
|
+
}
|
|
111
176
|
|
|
112
177
|
let done = false;
|
|
113
178
|
|
|
@@ -115,11 +180,9 @@ export function startInlineEdit(node, opts) {
|
|
|
115
180
|
document.removeEventListener("pointerdown", handleOutsidePointerDown, true);
|
|
116
181
|
};
|
|
117
182
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// stranded on screen until a full reload. Put the original node back before
|
|
122
|
-
// any downstream render runs.
|
|
183
|
+
// If a downstream render rebuilds the title block while the editor is open,
|
|
184
|
+
// the editor is dropped along with everything else; only restore the saved
|
|
185
|
+
// node when our editor is still connected to the DOM.
|
|
123
186
|
const restoreNode = () => {
|
|
124
187
|
if (editor.isConnected) editor.replaceWith(node);
|
|
125
188
|
delete node.dataset.editing;
|
|
@@ -209,12 +272,13 @@ function wireTicketDetailEditors(ticket) {
|
|
|
209
272
|
}
|
|
210
273
|
|
|
211
274
|
const descEl = drawerInner.querySelector('[data-edit-field="description"]');
|
|
212
|
-
const editDescription = () =>
|
|
275
|
+
const editDescription = (sourceEvent) =>
|
|
213
276
|
startInlineEdit(descEl, {
|
|
214
277
|
fieldName: "description",
|
|
215
278
|
multiline: true,
|
|
216
279
|
initialValue: ticket.description || "",
|
|
217
280
|
ariaLabel: "Edit ticket description",
|
|
281
|
+
sourceEvent,
|
|
218
282
|
rerender: () => renderDetail(),
|
|
219
283
|
commit: (next) => patchTicket(ticketId, { description: cleanText(next) })
|
|
220
284
|
});
|
|
@@ -222,7 +286,7 @@ function wireTicketDetailEditors(ticket) {
|
|
|
222
286
|
if (descEl) {
|
|
223
287
|
descEl.addEventListener("click", (event) => {
|
|
224
288
|
event.preventDefault();
|
|
225
|
-
editDescription();
|
|
289
|
+
editDescription(event);
|
|
226
290
|
});
|
|
227
291
|
descEl.addEventListener("keydown", (event) => {
|
|
228
292
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -245,18 +309,19 @@ function wireTicketDetailEditors(ticket) {
|
|
|
245
309
|
for (const { key, label } of AI_FIELDS) {
|
|
246
310
|
const fieldEl = drawerInner.querySelector(`[data-edit-field="${key}"]`);
|
|
247
311
|
if (!fieldEl) continue;
|
|
248
|
-
const edit = () =>
|
|
312
|
+
const edit = (sourceEvent) =>
|
|
249
313
|
startInlineEdit(fieldEl, {
|
|
250
314
|
fieldName: key,
|
|
251
315
|
multiline: true,
|
|
252
316
|
initialValue: ticket[key] || "",
|
|
253
317
|
ariaLabel: `Edit ${label}`,
|
|
318
|
+
sourceEvent,
|
|
254
319
|
rerender: () => renderDetail(),
|
|
255
320
|
commit: (next) => patchTicket(ticketId, { [key]: cleanText(next) })
|
|
256
321
|
});
|
|
257
322
|
fieldEl.addEventListener("click", (event) => {
|
|
258
323
|
event.preventDefault();
|
|
259
|
-
edit();
|
|
324
|
+
edit(event);
|
|
260
325
|
});
|
|
261
326
|
fieldEl.addEventListener("keydown", (event) => {
|
|
262
327
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -337,23 +402,26 @@ export async function renderDetail(options = {}) {
|
|
|
337
402
|
return;
|
|
338
403
|
}
|
|
339
404
|
|
|
405
|
+
const requestedTicketId = state.selectedTicketId;
|
|
340
406
|
const hasOption = (key) => Object.prototype.hasOwnProperty.call(options, key);
|
|
341
407
|
const [context, statusHistory, comments, attachmentList] = await Promise.all([
|
|
342
408
|
hasOption("context")
|
|
343
409
|
? Promise.resolve(options.context)
|
|
344
|
-
: api(withBoardQuery(`/api/tickets/${
|
|
410
|
+
: api(withBoardQuery(`/api/tickets/${requestedTicketId}/context?depth=1`)),
|
|
345
411
|
hasOption("statusHistory")
|
|
346
412
|
? Promise.resolve(options.statusHistory)
|
|
347
|
-
: api(withBoardQuery(`/api/tickets/${
|
|
413
|
+
: api(withBoardQuery(`/api/tickets/${requestedTicketId}/history`)),
|
|
348
414
|
hasOption("comments")
|
|
349
415
|
? Promise.resolve(options.comments)
|
|
350
|
-
: api(withBoardQuery(`/api/tickets/${
|
|
416
|
+
: api(withBoardQuery(`/api/tickets/${requestedTicketId}/comments`)).then((result) => result.comments || []),
|
|
351
417
|
hasOption("attachmentList")
|
|
352
418
|
? Promise.resolve(options.attachmentList)
|
|
353
419
|
: features.attachments
|
|
354
|
-
? api(withBoardQuery(`/api/tickets/${
|
|
420
|
+
? api(withBoardQuery(`/api/tickets/${requestedTicketId}/attachments`)).catch(() => ({ attachments: [] }))
|
|
355
421
|
: Promise.resolve({ attachments: [] })
|
|
356
422
|
]);
|
|
423
|
+
if (state.detailMode !== "ticket" || state.selectedTicketId !== requestedTicketId) return;
|
|
424
|
+
if (context.ticket?.id !== requestedTicketId) return;
|
|
357
425
|
const ticket = context.ticket;
|
|
358
426
|
// Acknowledge the read receipt: clear the unread dot on this card.
|
|
359
427
|
// Also wipe the dot from the already-rendered card in the board so the
|
package/public/js/toast.js
CHANGED
|
File without changes
|
package/public/js/transport.js
CHANGED
|
File without changes
|
package/public/js/unread.js
CHANGED
|
File without changes
|
package/public/js/url-routes.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/public/orbits-logo.png
CHANGED
|
File without changes
|
package/public/styles.css
CHANGED
|
@@ -321,7 +321,7 @@ textarea:focus {
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
@media (prefers-reduced-motion: reduce) {
|
|
324
|
-
[data-theme="dark"] .topbar {
|
|
324
|
+
:root:not([data-motion="full"])[data-theme="dark"] .topbar {
|
|
325
325
|
transition: none;
|
|
326
326
|
}
|
|
327
327
|
}
|
|
@@ -1037,8 +1037,8 @@ h1 {
|
|
|
1037
1037
|
}
|
|
1038
1038
|
|
|
1039
1039
|
@media (prefers-reduced-motion: reduce) {
|
|
1040
|
-
.create-flyout,
|
|
1041
|
-
.create-flyout-backdrop {
|
|
1040
|
+
:root:not([data-motion="full"]) .create-flyout,
|
|
1041
|
+
:root:not([data-motion="full"]) .create-flyout-backdrop {
|
|
1042
1042
|
transition: none;
|
|
1043
1043
|
}
|
|
1044
1044
|
}
|
|
@@ -1347,7 +1347,7 @@ h1 {
|
|
|
1347
1347
|
}
|
|
1348
1348
|
|
|
1349
1349
|
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: reduce) {
|
|
1350
|
-
.add-card-phantom {
|
|
1350
|
+
:root:not([data-motion="full"]) .add-card-phantom {
|
|
1351
1351
|
transition:
|
|
1352
1352
|
color 160ms ease,
|
|
1353
1353
|
border-color 160ms ease,
|
|
@@ -1402,7 +1402,7 @@ h1 {
|
|
|
1402
1402
|
}
|
|
1403
1403
|
|
|
1404
1404
|
@media (prefers-reduced-motion: reduce) {
|
|
1405
|
-
.agent-dot {
|
|
1405
|
+
:root:not([data-motion="full"]) .agent-dot {
|
|
1406
1406
|
animation: none;
|
|
1407
1407
|
}
|
|
1408
1408
|
}
|
|
@@ -1443,7 +1443,7 @@ h1 {
|
|
|
1443
1443
|
}
|
|
1444
1444
|
|
|
1445
1445
|
@media (prefers-reduced-motion: reduce) {
|
|
1446
|
-
.card-unread-dot {
|
|
1446
|
+
:root:not([data-motion="full"]) .card-unread-dot {
|
|
1447
1447
|
animation: none;
|
|
1448
1448
|
}
|
|
1449
1449
|
}
|
|
@@ -1691,7 +1691,7 @@ h1 {
|
|
|
1691
1691
|
}
|
|
1692
1692
|
|
|
1693
1693
|
@media (prefers-reduced-motion: reduce) {
|
|
1694
|
-
.card {
|
|
1694
|
+
:root:not([data-motion="full"]) .card {
|
|
1695
1695
|
animation: none;
|
|
1696
1696
|
}
|
|
1697
1697
|
}
|
|
@@ -1823,8 +1823,10 @@ h1 {
|
|
|
1823
1823
|
[data-theme="dark"] .card {
|
|
1824
1824
|
border-color: rgba(var(--starlight-rgb),0.12);
|
|
1825
1825
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
|
|
1826
|
-
backdrop-filter: blur
|
|
1827
|
-
|
|
1826
|
+
/* No backdrop-filter: --card is 85% opaque so the blur showed through ~15%
|
|
1827
|
+
(near-invisible) yet forced every card to re-blur the animated starfield
|
|
1828
|
+
behind it on every frame — the page's biggest GPU cost. The opaque fill
|
|
1829
|
+
carries the surface; the starfield still animates in the gaps. */
|
|
1828
1830
|
}
|
|
1829
1831
|
|
|
1830
1832
|
[data-theme="dark"] .card.type-epic {
|
|
@@ -2134,7 +2136,7 @@ h1 {
|
|
|
2134
2136
|
}
|
|
2135
2137
|
|
|
2136
2138
|
@media (prefers-reduced-motion: reduce) {
|
|
2137
|
-
.priority-pill-urgent {
|
|
2139
|
+
:root:not([data-motion="full"]) .priority-pill-urgent {
|
|
2138
2140
|
animation: none;
|
|
2139
2141
|
}
|
|
2140
2142
|
}
|
|
@@ -2165,11 +2167,17 @@ h1 {
|
|
|
2165
2167
|
}
|
|
2166
2168
|
|
|
2167
2169
|
@media (prefers-reduced-motion: no-preference) {
|
|
2168
|
-
[data-theme="dark"] .priority-pill-urgent {
|
|
2170
|
+
:root:not([data-motion="reduce"])[data-theme="dark"] .priority-pill-urgent {
|
|
2169
2171
|
box-shadow: 0 0 0 1px rgba(var(--coral-rgb),0.35), 0 4px 10px rgba(var(--coral-rgb),0.3);
|
|
2170
2172
|
}
|
|
2171
2173
|
}
|
|
2172
2174
|
|
|
2175
|
+
/* Full-motion escape hatch: show the urgent glow even when the OS prefers
|
|
2176
|
+
reduced motion but the user forced full motion. */
|
|
2177
|
+
:root[data-motion="full"][data-theme="dark"] .priority-pill-urgent {
|
|
2178
|
+
box-shadow: 0 0 0 1px rgba(var(--coral-rgb),0.35), 0 4px 10px rgba(var(--coral-rgb),0.3);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2173
2181
|
.card-expand-trigger {
|
|
2174
2182
|
display: flex;
|
|
2175
2183
|
align-items: center;
|
|
@@ -2339,8 +2347,8 @@ h1 {
|
|
|
2339
2347
|
}
|
|
2340
2348
|
|
|
2341
2349
|
@media (prefers-reduced-motion: reduce) {
|
|
2342
|
-
.drawer,
|
|
2343
|
-
.drawer-backdrop {
|
|
2350
|
+
:root:not([data-motion="full"]) .drawer,
|
|
2351
|
+
:root:not([data-motion="full"]) .drawer-backdrop {
|
|
2344
2352
|
transition: none;
|
|
2345
2353
|
}
|
|
2346
2354
|
}
|
|
@@ -3671,16 +3679,13 @@ details.section > summary .mcp-title-row {
|
|
|
3671
3679
|
min-height: 2rem;
|
|
3672
3680
|
}
|
|
3673
3681
|
|
|
3674
|
-
/*
|
|
3675
|
-
|
|
3676
|
-
.settings-notes-body
|
|
3682
|
+
/* Long-form settings fields reserve textarea-sized landing areas so they don't
|
|
3683
|
+
collapse to one-liners when empty. */
|
|
3684
|
+
.settings-notes-body,
|
|
3685
|
+
.settings-agent-instructions-body {
|
|
3677
3686
|
min-height: 11rem;
|
|
3678
3687
|
}
|
|
3679
3688
|
|
|
3680
|
-
.ai-context-setup textarea[name="agent_instructions"] {
|
|
3681
|
-
min-height: 9rem;
|
|
3682
|
-
}
|
|
3683
|
-
|
|
3684
3689
|
.ai-context-setup > .description {
|
|
3685
3690
|
margin-bottom: 0.7rem;
|
|
3686
3691
|
}
|
|
@@ -5170,8 +5175,8 @@ button:disabled,
|
|
|
5170
5175
|
.toast-error .toast-icon { color: #ef4444; }
|
|
5171
5176
|
|
|
5172
5177
|
@media (prefers-reduced-motion: reduce) {
|
|
5173
|
-
.toast-enter { animation: none; opacity: 1; transform: none; }
|
|
5174
|
-
.toast-exit { animation: none; }
|
|
5178
|
+
:root:not([data-motion="full"]) .toast-enter { animation: none; opacity: 1; transform: none; }
|
|
5179
|
+
:root:not([data-motion="full"]) .toast-exit { animation: none; }
|
|
5175
5180
|
}
|
|
5176
5181
|
|
|
5177
5182
|
[data-theme="dark"] .toast {
|
|
@@ -5332,8 +5337,8 @@ button:disabled,
|
|
|
5332
5337
|
}
|
|
5333
5338
|
|
|
5334
5339
|
@media (prefers-reduced-motion: reduce) {
|
|
5335
|
-
.orbit-dialog,
|
|
5336
|
-
.orbit-dialog::backdrop {
|
|
5340
|
+
:root:not([data-motion="full"]) .orbit-dialog,
|
|
5341
|
+
:root:not([data-motion="full"]) .orbit-dialog::backdrop {
|
|
5337
5342
|
animation: none;
|
|
5338
5343
|
}
|
|
5339
5344
|
}
|
|
@@ -5669,40 +5674,75 @@ button:disabled,
|
|
|
5669
5674
|
}
|
|
5670
5675
|
}
|
|
5671
5676
|
|
|
5677
|
+
/* Reduced motion — Starscape v3 `data-motion` contract (migrated from the
|
|
5678
|
+
* legacy `data-reduced-motion="reduce|allow"`):
|
|
5679
|
+
* • OS path: reduce when the OS asks, unless the user forces full motion.
|
|
5680
|
+
* • Explicit path: `data-motion="reduce"` reduces regardless of OS.
|
|
5681
|
+
* • `data-motion="full"` opts out of the OS path (full motion even on OS reduce). */
|
|
5672
5682
|
@media (prefers-reduced-motion: reduce) {
|
|
5673
|
-
[data-
|
|
5674
|
-
[data-
|
|
5675
|
-
[data-
|
|
5676
|
-
|
|
5677
|
-
|
|
5683
|
+
:root:not([data-motion="full"]) *,
|
|
5684
|
+
:root:not([data-motion="full"]) *::before,
|
|
5685
|
+
:root:not([data-motion="full"]) *::after {
|
|
5686
|
+
scroll-behavior: auto !important;
|
|
5687
|
+
transition-duration: 0.01ms !important;
|
|
5688
|
+
animation-duration: 0.01ms !important;
|
|
5689
|
+
animation-iteration-count: 1 !important;
|
|
5690
|
+
}
|
|
5691
|
+
|
|
5692
|
+
:root:not([data-motion="full"])[data-theme="dark"] .starfield,
|
|
5693
|
+
:root:not([data-motion="full"])[data-theme="dark"] .milky-way,
|
|
5694
|
+
:root:not([data-motion="full"])[data-theme="dark"] .grain::before,
|
|
5695
|
+
:root:not([data-motion="full"])[data-theme="dark"] .grain::after,
|
|
5696
|
+
:root:not([data-motion="full"])[data-theme="dark"] .signature-star {
|
|
5678
5697
|
animation: none;
|
|
5679
5698
|
}
|
|
5680
|
-
|
|
5699
|
+
|
|
5700
|
+
:root:not([data-motion="full"])[data-theme="dark"] .meteor {
|
|
5681
5701
|
display: none;
|
|
5682
5702
|
}
|
|
5683
5703
|
}
|
|
5684
5704
|
|
|
5685
|
-
[data-
|
|
5686
|
-
[data-
|
|
5687
|
-
[data-
|
|
5705
|
+
[data-motion="reduce"] *,
|
|
5706
|
+
[data-motion="reduce"] *::before,
|
|
5707
|
+
[data-motion="reduce"] *::after {
|
|
5688
5708
|
scroll-behavior: auto !important;
|
|
5689
5709
|
transition-duration: 0.01ms !important;
|
|
5690
5710
|
animation-duration: 0.01ms !important;
|
|
5691
5711
|
animation-iteration-count: 1 !important;
|
|
5692
5712
|
}
|
|
5693
5713
|
|
|
5694
|
-
[data-
|
|
5695
|
-
[data-
|
|
5696
|
-
[data-
|
|
5697
|
-
[data-
|
|
5698
|
-
[data-
|
|
5714
|
+
[data-motion="reduce"][data-theme="dark"] .starfield,
|
|
5715
|
+
[data-motion="reduce"][data-theme="dark"] .milky-way,
|
|
5716
|
+
[data-motion="reduce"][data-theme="dark"] .grain::before,
|
|
5717
|
+
[data-motion="reduce"][data-theme="dark"] .grain::after,
|
|
5718
|
+
[data-motion="reduce"][data-theme="dark"] .signature-star {
|
|
5699
5719
|
animation: none;
|
|
5700
5720
|
}
|
|
5701
5721
|
|
|
5702
|
-
[data-
|
|
5722
|
+
[data-motion="reduce"][data-theme="dark"] .meteor {
|
|
5703
5723
|
display: none;
|
|
5704
5724
|
}
|
|
5705
5725
|
|
|
5726
|
+
/* Empty-lane icon micro-motion — a low-amplitude exception to the OS reduced-
|
|
5727
|
+
* motion reset above. These are small (64px), slow, localized loops, not the
|
|
5728
|
+
* large-area/parallax motion that triggers vestibular discomfort, so they
|
|
5729
|
+
* survive an *OS-level* reduced-motion preference. They are still fully stopped
|
|
5730
|
+
* by an *explicit* in-app choice (data-motion="reduce") — a deliberate "calm it
|
|
5731
|
+
* down" wins over the ambient OS default. The !important + selector specificity
|
|
5732
|
+
* is what overrides the universal `* { animation-duration: 0.01ms !important }`
|
|
5733
|
+
* reset for just these elements. */
|
|
5734
|
+
@media (prefers-reduced-motion: reduce) {
|
|
5735
|
+
:root:not([data-motion="reduce"]) .empty-state-icon.star {
|
|
5736
|
+
animation: star-pulse 4s ease-in-out infinite !important;
|
|
5737
|
+
}
|
|
5738
|
+
:root:not([data-motion="reduce"]) .empty-state-icon.ai-sparkle {
|
|
5739
|
+
animation: ai-float 6s ease-in-out infinite !important;
|
|
5740
|
+
}
|
|
5741
|
+
:root:not([data-motion="reduce"]) .empty-state-icon.orbit .satellite {
|
|
5742
|
+
animation: orbit-rotate 10s linear infinite !important;
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5706
5746
|
[data-theme="dark"] {
|
|
5707
5747
|
/* Tells the browser to render native widgets (dropdowns, scrollbars,
|
|
5708
5748
|
date pickers, etc.) in their dark variant so option popups match. */
|
package/src/cli/orbit.js
CHANGED
|
@@ -43,6 +43,7 @@ Options (dispatch):
|
|
|
43
43
|
--worktree-path <dir> Worktree path when --worktree is used
|
|
44
44
|
--branch <name> Branch name for the preserved worktree
|
|
45
45
|
--hermes-bin <cmd> Hermes executable (default: hermes)
|
|
46
|
+
--verify-command <cmd> Declare an expected verification command (repeatable)
|
|
46
47
|
--no-spawn Prepare card/worktree/run record but do not start Hermes or move In Progress
|
|
47
48
|
--no-yolo Do not pass --yolo to Hermes (default dispatch passes --yolo)
|
|
48
49
|
--foreground Attach spawned Hermes process to this terminal
|
|
@@ -157,6 +158,13 @@ function resetBusyError(error, projectRoot) {
|
|
|
157
158
|
return wrapped;
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
function invalidArgs(args, message) {
|
|
162
|
+
console.error(message);
|
|
163
|
+
args.command = "help";
|
|
164
|
+
args.invalid = true;
|
|
165
|
+
return args;
|
|
166
|
+
}
|
|
167
|
+
|
|
160
168
|
function parseArgs(argv) {
|
|
161
169
|
const args = {
|
|
162
170
|
command: null,
|
|
@@ -180,6 +188,7 @@ function parseArgs(argv) {
|
|
|
180
188
|
worktreePath: null,
|
|
181
189
|
branch: null,
|
|
182
190
|
hermesBin: "hermes",
|
|
191
|
+
verifyCommands: [],
|
|
183
192
|
noSpawn: false,
|
|
184
193
|
yolo: true,
|
|
185
194
|
keepHandoffFile: false,
|
|
@@ -254,6 +263,18 @@ function parseArgs(argv) {
|
|
|
254
263
|
args.hermesBin = rest[++i];
|
|
255
264
|
} else if (a.startsWith("--hermes-bin=")) {
|
|
256
265
|
args.hermesBin = a.slice("--hermes-bin=".length);
|
|
266
|
+
} else if (a === "--verify-command") {
|
|
267
|
+
const value = rest[i + 1];
|
|
268
|
+
if (!value || value.startsWith("--")) {
|
|
269
|
+
return invalidArgs(args, "--verify-command requires a non-empty value. Use --verify-command=<cmd> for values that start with --.");
|
|
270
|
+
}
|
|
271
|
+
args.verifyCommands.push(rest[++i]);
|
|
272
|
+
} else if (a.startsWith("--verify-command=")) {
|
|
273
|
+
const value = a.slice("--verify-command=".length);
|
|
274
|
+
if (!value) {
|
|
275
|
+
return invalidArgs(args, "--verify-command requires a non-empty value.");
|
|
276
|
+
}
|
|
277
|
+
args.verifyCommands.push(value);
|
|
257
278
|
} else if (a === "--no-spawn") {
|
|
258
279
|
args.noSpawn = true;
|
|
259
280
|
} else if (a === "--no-yolo") {
|
|
@@ -560,6 +581,7 @@ async function runDispatch(options) {
|
|
|
560
581
|
console.log(`Policy: ${result.policy}`);
|
|
561
582
|
console.log(`Worktree: ${result.worktree_path}`);
|
|
562
583
|
if (result.branch) console.log(`Branch: ${result.branch}`);
|
|
584
|
+
if (result.run_record_path) console.log(`Run record: ${result.run_record_path}`);
|
|
563
585
|
if (result.pid) console.log(`PID: ${result.pid}`);
|
|
564
586
|
console.log("AI Written-Plan updated with the generated handoff.");
|
|
565
587
|
console.log("Run record comment added to the ticket.");
|
package/src/core/agent.js
CHANGED
|
File without changes
|
package/src/core/agents-md.js
CHANGED
|
File without changes
|
package/src/core/attachments.js
CHANGED
|
File without changes
|
package/src/core/auth.js
CHANGED
|
File without changes
|
package/src/core/backups.js
CHANGED
|
File without changes
|
package/src/core/board.js
CHANGED
|
@@ -132,6 +132,7 @@ export {
|
|
|
132
132
|
boardManual
|
|
133
133
|
} from "./boards.js";
|
|
134
134
|
export { archiveTicket, createComment, createTicket, deleteTicket, restoreTicket, updateTicket } from "./tickets.js";
|
|
135
|
+
export { createReviewVerdict, getReviewVerdict, listReviewVerdicts } from "./review-verdicts.js";
|
|
135
136
|
export { archivedTicketsForBoard } from "./queries.js";
|
|
136
137
|
export { getTicketBlockers, getTicketRelations } from "./relations.js";
|
|
137
138
|
export { checkpointTicket, claimNext, completeTicket, getAgentDispatchPacket, getContextPack, getContextPackFull, readComments, readTicket } from "./agent.js";
|
package/src/core/boards.js
CHANGED
|
File without changes
|
package/src/core/bootstrap.js
CHANGED
|
File without changes
|
package/src/core/db.js
CHANGED
|
@@ -179,6 +179,22 @@ export function createBoardSchema(db) {
|
|
|
179
179
|
created_at TEXT NOT NULL
|
|
180
180
|
);
|
|
181
181
|
|
|
182
|
+
CREATE TABLE IF NOT EXISTS review_verdicts (
|
|
183
|
+
id TEXT PRIMARY KEY,
|
|
184
|
+
ticket_id TEXT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
|
|
185
|
+
verdict TEXT NOT NULL CHECK (verdict IN ('PASS', 'BLOCK', 'QUESTION')),
|
|
186
|
+
blocking_findings_json TEXT NOT NULL DEFAULT '[]',
|
|
187
|
+
optional_findings_json TEXT NOT NULL DEFAULT '[]',
|
|
188
|
+
evidence_commands_json TEXT NOT NULL DEFAULT '[]',
|
|
189
|
+
reviewer_profile TEXT NOT NULL DEFAULT '',
|
|
190
|
+
reviewer_session_id TEXT NOT NULL DEFAULT '',
|
|
191
|
+
reviewed_commit_sha TEXT NOT NULL DEFAULT '',
|
|
192
|
+
dispatch_run_id TEXT NOT NULL DEFAULT '',
|
|
193
|
+
supersedes_prior_review_id TEXT REFERENCES review_verdicts(id) ON DELETE SET NULL,
|
|
194
|
+
created_by TEXT NOT NULL DEFAULT 'human',
|
|
195
|
+
created_at TEXT NOT NULL
|
|
196
|
+
);
|
|
197
|
+
|
|
182
198
|
CREATE TABLE IF NOT EXISTS ticket_attachments (
|
|
183
199
|
id TEXT PRIMARY KEY,
|
|
184
200
|
ticket_id TEXT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
|
|
@@ -251,6 +267,8 @@ export function createBoardSchema(db) {
|
|
|
251
267
|
CREATE INDEX IF NOT EXISTS idx_tickets_archived ON tickets(board_id, archived_at);
|
|
252
268
|
CREATE INDEX IF NOT EXISTS idx_tickets_parent ON tickets(parent_ticket_id);
|
|
253
269
|
CREATE INDEX IF NOT EXISTS idx_comments_ticket ON comments(ticket_id, created_at);
|
|
270
|
+
CREATE INDEX IF NOT EXISTS idx_review_verdicts_ticket ON review_verdicts(ticket_id, created_at);
|
|
271
|
+
CREATE INDEX IF NOT EXISTS idx_review_verdicts_supersedes ON review_verdicts(supersedes_prior_review_id);
|
|
254
272
|
CREATE INDEX IF NOT EXISTS idx_ticket_attachments_ticket ON ticket_attachments(ticket_id, created_at);
|
|
255
273
|
CREATE INDEX IF NOT EXISTS idx_ticket_attachments_board ON ticket_attachments(board_id, created_at);
|
|
256
274
|
CREATE INDEX IF NOT EXISTS idx_board_entries_board ON board_entries(board_id, type, created_at);
|
|
@@ -266,6 +284,7 @@ export function resetBoard(db) {
|
|
|
266
284
|
DELETE FROM ticket_labels;
|
|
267
285
|
DELETE FROM relations;
|
|
268
286
|
DELETE FROM comments;
|
|
287
|
+
DELETE FROM review_verdicts;
|
|
269
288
|
DELETE FROM ticket_attachments;
|
|
270
289
|
DELETE FROM board_entries;
|
|
271
290
|
DELETE FROM events;
|
package/src/core/delete-board.js
CHANGED
|
File without changes
|