moontide-mcp 2.0.2 → 2.2.0
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/README.md +94 -6
- package/dist/ask.js +76 -15
- package/dist/mcp.js +198 -26
- package/dist/protocol.js +130 -2
- package/dist/reference.js +22 -0
- package/dist/request-status.js +221 -0
- package/dist/session.js +256 -7
- package/package.json +1 -1
- package/page/ask-view.js +64 -57
- package/page/ask.js +363 -188
- package/page/bar-model.js +43 -2
- package/page/change-timing.js +60 -4
- package/page/harmony.js +13 -0
- package/page/index.html +7 -4
- package/page/jam-analysis.js +68 -23
- package/page/jam-model.js +196 -31
- package/page/jam-resolve.js +271 -0
- package/page/jam-view.js +182 -10
- package/page/lights.js +300 -0
- package/page/main.js +2171 -188
- package/page/midi-clock.js +23 -4
- package/page/midi-out.js +17 -2
- package/page/mixer-groups.js +114 -0
- package/page/mixer-model.js +172 -29
- package/page/mixer-rewrite.js +30 -1
- package/page/mixer-view.js +289 -14
- package/page/note-edit.js +267 -0
- package/page/note-locate.js +346 -0
- package/page/note-panel.js +402 -0
- package/page/note-tag.js +141 -0
- package/page/page-rewrite.js +184 -0
- package/page/query-check.js +21 -2
- package/page/request-status-view.js +419 -0
- package/page/request-status.js +686 -0
- package/page/song-edit.js +230 -0
- package/page/song-fit.js +250 -0
- package/page/song-grid-model.js +506 -0
- package/page/song-grid-view.js +710 -0
- package/page/song-prefs.js +100 -0
- package/page/structure-check.js +216 -0
- package/page/structure-view.js +38 -0
- package/page/structure.js +761 -0
- package/page/style.css +1218 -65
- package/page/takeover.js +20 -5
- package/page/tempo-glide.js +330 -0
- package/page/tempo-guard.js +62 -10
- package/page/timeline-edit-view.js +1956 -0
- package/page/timeline-gesture.js +279 -0
- package/page/timeline.js +197 -3
- package/page/top-bar-view.js +18 -13
- package/page/tracks.js +37 -8
- package/page/ui-state.js +10 -5
- package/page/views.js +39 -9
- package/reference/functions.md +1 -0
- package/reference/midi.md +2 -0
- package/reference/structure.md +131 -0
- package/reference/transitions.md +13 -1
package/page/ask-view.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// The Ask Claude box's DOM: the small box a right-click opens near the pointer, its
|
|
2
|
-
// chips, its field, its message line
|
|
2
|
+
// chips, its field, its message line and the top-bar page-requests
|
|
3
3
|
// status (whether page requests reach Claude live) with its (i) button and the
|
|
4
4
|
// short popover the (i) opens, and a refused page token on the top-bar error line as soon as the
|
|
5
5
|
// server says so. Every text reaches the page through
|
|
6
6
|
// textContent, title, placeholder or setAttribute, never as markup: the chips carry names and code
|
|
7
7
|
// written by a person or by Claude. No DOM access at import time: main.js calls createAskView() with
|
|
8
|
-
// its document and a send function. The pure rules (copy, limits, placement
|
|
9
|
-
// page/
|
|
8
|
+
// its document and a send function. The pure rules (copy, limits, placement) live in page/ask.js. What became of
|
|
9
|
+
// each request is the Requests list's business (page/request-status-view.js): this module hands every request and
|
|
10
|
+
// every answer to the request tracker, and shows only the line under the Parts button.
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
13
|
ASK_COPY,
|
|
@@ -16,9 +17,9 @@ import {
|
|
|
16
17
|
pageRequestsText,
|
|
17
18
|
askMessage,
|
|
18
19
|
askTextProblem,
|
|
19
|
-
createAskStatus,
|
|
20
20
|
cutText,
|
|
21
21
|
describeContext,
|
|
22
|
+
fitGestureToLine,
|
|
22
23
|
fitMomentToLine,
|
|
23
24
|
newAskId,
|
|
24
25
|
partsAskState,
|
|
@@ -31,21 +32,17 @@ import {
|
|
|
31
32
|
* Creates the box controller. `document` is the page's document (named so on purpose: every element
|
|
32
33
|
* lookup below reads `document.getElementById`, the shape test/page-hidden.test.js scans); `send`
|
|
33
34
|
* sends one JSON-able message to the server; `reportError` and `clearError` write and clear the
|
|
34
|
-
* top-bar error line; `setTimer`, `clearTimer` and `now` are the page's clock
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* top-bar error line; `setTimer`, `clearTimer` and `now` are the page's clock. `requests` is the per-request tracker
|
|
36
|
+
* of page/request-status.js: every request sent is handed to it, and so is every server answer to one. The tracker
|
|
37
|
+
* tells the page (main.js) when a request was never confirmed; the page then calls `notConfirmed(id)` here.
|
|
37
38
|
*/
|
|
38
|
-
export function createAskView({ document, send, reportError, clearError, setTimer, clearTimer, now,
|
|
39
|
+
export function createAskView({ document, send, reportError, clearError, setTimer, clearTimer, now, requests }) {
|
|
39
40
|
const askBox = document.getElementById('ask-box');
|
|
40
41
|
const askContextChip = document.getElementById('ask-context-chip');
|
|
41
42
|
const askCurrentChip = document.getElementById('ask-current-chip');
|
|
42
43
|
const askField = document.getElementById('ask-field');
|
|
43
44
|
const askMessageEl = document.getElementById('ask-message');
|
|
44
45
|
const askHint = document.getElementById('ask-hint');
|
|
45
|
-
const askStatus = document.getElementById('ask-status');
|
|
46
|
-
// The status holds its long sentence and a short form (shown at fit step 3, aria-hidden).
|
|
47
|
-
const askLong = askStatus === null ? null : askStatus.querySelector('.ask-long');
|
|
48
|
-
const askShort = askStatus === null ? null : askStatus.querySelector('.ask-short');
|
|
49
46
|
const partsAsk = document.getElementById('parts-ask');
|
|
50
47
|
const partsAskStatus = document.getElementById('parts-ask-status');
|
|
51
48
|
const pageRequests = document.getElementById('page-requests');
|
|
@@ -59,9 +56,6 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
59
56
|
!askField ||
|
|
60
57
|
!askMessageEl ||
|
|
61
58
|
!askHint ||
|
|
62
|
-
!askStatus ||
|
|
63
|
-
!askLong ||
|
|
64
|
-
!askShort ||
|
|
65
59
|
!partsAsk ||
|
|
66
60
|
!partsAskStatus ||
|
|
67
61
|
!pageRequests ||
|
|
@@ -71,8 +65,8 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
71
65
|
) {
|
|
72
66
|
throw new Error('Moontide: the Ask Claude markup is missing from the page.');
|
|
73
67
|
}
|
|
74
|
-
if (typeof
|
|
75
|
-
throw new Error('Moontide: createAskView needs the
|
|
68
|
+
if (requests === undefined || typeof requests.sent !== 'function' || typeof requests.serverMessage !== 'function') {
|
|
69
|
+
throw new Error('Moontide: createAskView needs the request tracker.');
|
|
76
70
|
}
|
|
77
71
|
|
|
78
72
|
// The field's description lists the message line only while it shows, so it is never empty.
|
|
@@ -119,7 +113,8 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
119
113
|
// The version id the top bar shows, or null before any version.
|
|
120
114
|
let currentVersion = null;
|
|
121
115
|
// What the right-click pointed at, snapshotted when the box opened: { context, chip } and, when the
|
|
122
|
-
// Timeline showed the music there, the moment captured at the click
|
|
116
|
+
// Timeline showed the music there, the moment captured at the click, and, when a gesture on a note opened the box,
|
|
117
|
+
// that gesture; for a range, the cycles it was cut to when the pointer left the page (`rangeCut`).
|
|
123
118
|
let snapshot = null;
|
|
124
119
|
// The last request sent: { id, text }, so an accepted request clears its own draft.
|
|
125
120
|
let lastSent = null;
|
|
@@ -137,36 +132,10 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
137
132
|
// The side the box opened on (above the pointer or below), kept while its height changes; null
|
|
138
133
|
// means choose again.
|
|
139
134
|
let placedAbove = null;
|
|
140
|
-
// The Parts view's button: whether the code has no sections (so the button shows)
|
|
141
|
-
// request while it waits for Claude to take it
|
|
135
|
+
// The Parts view's button: whether the code has no sections (so the button shows) and the id of its
|
|
136
|
+
// request while it waits for Claude to take it.
|
|
142
137
|
let partsAvailable = false;
|
|
143
138
|
let partsPendingId = null;
|
|
144
|
-
let statusText = null;
|
|
145
|
-
|
|
146
|
-
const status = createAskStatus({
|
|
147
|
-
render: (text, short) => {
|
|
148
|
-
statusText = text;
|
|
149
|
-
askLong.textContent = text ?? '';
|
|
150
|
-
askShort.textContent = short ?? '';
|
|
151
|
-
askStatus.hidden = text === null;
|
|
152
|
-
fit();
|
|
153
|
-
renderPartsAsk();
|
|
154
|
-
},
|
|
155
|
-
onTimeout: (id) => {
|
|
156
|
-
reportError(ASK_COPY.didNotConfirm);
|
|
157
|
-
if (id === partsPendingId) {
|
|
158
|
-
// The canned request needs no draft: the button just comes back.
|
|
159
|
-
partsPendingId = null;
|
|
160
|
-
renderPartsAsk();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
notSent = true;
|
|
164
|
-
renderMessage();
|
|
165
|
-
},
|
|
166
|
-
setTimer,
|
|
167
|
-
clearTimer,
|
|
168
|
-
now,
|
|
169
|
-
});
|
|
170
139
|
|
|
171
140
|
/** The fields of the request the box would send for `text` (trimmed), the id measured with the next one's shape. */
|
|
172
141
|
function requestFields(text) {
|
|
@@ -177,6 +146,7 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
177
146
|
chip: snapshot.chip,
|
|
178
147
|
current: currentVersion,
|
|
179
148
|
moment: snapshot.moment,
|
|
149
|
+
gesture: snapshot.gesture,
|
|
180
150
|
};
|
|
181
151
|
}
|
|
182
152
|
|
|
@@ -189,7 +159,7 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
189
159
|
function renderMessage() {
|
|
190
160
|
const length = askField.value.trim().length;
|
|
191
161
|
const tooLongLine = lineTooLong();
|
|
192
|
-
const text = askMessage({ connected, length, notSent, tokenRefused, lineTooLong: tooLongLine });
|
|
162
|
+
const text = askMessage({ connected, length, notSent, tokenRefused, lineTooLong: tooLongLine, rangeCut: snapshot === null ? null : (snapshot.rangeCut ?? null) });
|
|
193
163
|
askMessageEl.textContent = text ?? '';
|
|
194
164
|
askMessageEl.hidden = text === null;
|
|
195
165
|
askField.setAttribute('aria-describedby', askMessageEl.hidden ? DESCRIBED_WITHOUT_MESSAGE : DESCRIBED_WITH_MESSAGE);
|
|
@@ -199,6 +169,14 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
199
169
|
refit();
|
|
200
170
|
}
|
|
201
171
|
|
|
172
|
+
/** The state phrase of the Parts request's row in the Requests list, or null when it has no row. */
|
|
173
|
+
function partsRowPhrase() {
|
|
174
|
+
if (partsPendingId === null) return null;
|
|
175
|
+
const model = requests.model();
|
|
176
|
+
const row = [...model.rows, ...model.recent].find((candidate) => candidate.id === partsPendingId);
|
|
177
|
+
return row === undefined ? null : row.phrase;
|
|
178
|
+
}
|
|
179
|
+
|
|
202
180
|
/** The Parts button and the line under it, from partsAskState. */
|
|
203
181
|
function renderPartsAsk() {
|
|
204
182
|
partsAsk.hidden = !partsAvailable;
|
|
@@ -207,7 +185,7 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
207
185
|
partsAskStatus.hidden = true;
|
|
208
186
|
return;
|
|
209
187
|
}
|
|
210
|
-
const state = partsAskState({ connected, pending: partsPendingId !== null, statusText });
|
|
188
|
+
const state = partsAskState({ connected, pending: partsPendingId !== null, statusText: partsRowPhrase() });
|
|
211
189
|
partsAsk.disabled = state.disabled;
|
|
212
190
|
partsAskStatus.textContent = state.line ?? '';
|
|
213
191
|
partsAskStatus.hidden = state.line === null;
|
|
@@ -264,13 +242,17 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
264
242
|
/**
|
|
265
243
|
* Puts a prefill in the field (a failed change sent to Claude). An empty field is simply filled. A field that
|
|
266
244
|
* already holds a draft keeps it: the prefill follows after a blank line, cut so the whole field stays within
|
|
267
|
-
* MAX_ASK_TEXT_LENGTH
|
|
268
|
-
* room at all, the draft stays as it is (the chip still
|
|
245
|
+
* MAX_ASK_TEXT_LENGTH, and is not added again when the draft already ends with it. The caret goes to the end so the
|
|
246
|
+
* user types the question after it. When the draft leaves no room at all, the draft stays as it is (the chip still
|
|
247
|
+
* names the error).
|
|
269
248
|
*/
|
|
270
249
|
function applyPrefill(prefill) {
|
|
271
250
|
const draft = askField.value;
|
|
272
251
|
if (draft === '') {
|
|
273
252
|
askField.value = prefill;
|
|
253
|
+
} else if (draft.endsWith(prefill)) {
|
|
254
|
+
// The request is already there (the same target was asked about twice, or the browser raised a right-click after
|
|
255
|
+
// a key press that had already opened the box): it is not added a second time.
|
|
274
256
|
} else {
|
|
275
257
|
const room = MAX_ASK_TEXT_LENGTH - draft.length - 2;
|
|
276
258
|
if (room < 1) return;
|
|
@@ -284,8 +266,8 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
284
266
|
* `{ prefill }` the field also receives that text (see applyPrefill); without it nothing about the field changes.
|
|
285
267
|
*/
|
|
286
268
|
function open(descriptor, point, { prefill } = {}) {
|
|
287
|
-
const { context, chip, moment } = describeContext(descriptor);
|
|
288
|
-
snapshot = moment === undefined ? {
|
|
269
|
+
const { context, chip, moment, gesture, rangeCut } = describeContext(descriptor);
|
|
270
|
+
snapshot = { context, chip, ...(moment === undefined ? {} : { moment }), ...(gesture === undefined ? {} : { gesture }), ...(rangeCut === undefined ? {} : { rangeCut }) };
|
|
289
271
|
askContextChip.textContent = chip;
|
|
290
272
|
askContextChip.setAttribute('title', chip);
|
|
291
273
|
const wasHidden = askBox.hidden;
|
|
@@ -327,12 +309,17 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
327
309
|
const frame = { type: 'ask', id, text, context: describedContext.context, chip: describedContext.chip, current: currentVersion };
|
|
328
310
|
// The moment rides after `current`, and only when there is one: a frame without it is as it always was.
|
|
329
311
|
// Notes come off its end until the whole line fits what the server takes (the rest counts in `more`).
|
|
330
|
-
const
|
|
312
|
+
const fields = { ...frame, moment: describedContext.moment, gesture: describedContext.gesture };
|
|
313
|
+
const moment = fitMomentToLine(fields);
|
|
331
314
|
if (moment !== undefined) frame.moment = moment;
|
|
315
|
+
// The gesture follows the moment, and only when the box was opened by one. The notes of a range gesture come off its end the
|
|
316
|
+
// same way; any other gesture is sent as it is.
|
|
317
|
+
const gesture = fitGestureToLine({ ...fields, moment });
|
|
318
|
+
if (gesture !== undefined) frame.gesture = gesture;
|
|
332
319
|
send(frame);
|
|
333
320
|
// While the token is refused the line stays: the server's 'token' refusal writes it again, so it never blinks off.
|
|
334
321
|
if (!authRefused) clearError();
|
|
335
|
-
|
|
322
|
+
requests.sent(id, { text, chip: describedContext.chip });
|
|
336
323
|
return id;
|
|
337
324
|
}
|
|
338
325
|
|
|
@@ -358,6 +345,8 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
358
345
|
// The History view must never see this Enter, even after focus moves back to a row.
|
|
359
346
|
event.preventDefault();
|
|
360
347
|
event.stopPropagation();
|
|
348
|
+
// A held key that opened the box (the auto-repeat lands here once focus has moved) must not also send what is in it.
|
|
349
|
+
if (event.repeat) return;
|
|
361
350
|
submit();
|
|
362
351
|
} else if (event.key === 'Escape') {
|
|
363
352
|
// Esc closes the box only, never an open view.
|
|
@@ -464,13 +453,13 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
464
453
|
return;
|
|
465
454
|
}
|
|
466
455
|
if (msg.type === 'ask-received') {
|
|
467
|
-
|
|
456
|
+
requests.serverMessage(msg);
|
|
468
457
|
if (partsPendingId !== null && msg.ids.includes(partsPendingId)) partsPendingId = null;
|
|
469
458
|
renderPartsAsk();
|
|
470
459
|
return;
|
|
471
460
|
}
|
|
472
461
|
if (msg.type !== 'ask-result') return;
|
|
473
|
-
|
|
462
|
+
requests.serverMessage(msg);
|
|
474
463
|
const fromParts = msg.id === partsPendingId;
|
|
475
464
|
if (msg.status === 'refused') {
|
|
476
465
|
reportError(refusalText(msg.reason, msg.detail));
|
|
@@ -491,10 +480,25 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
491
480
|
renderPartsAsk();
|
|
492
481
|
}
|
|
493
482
|
|
|
483
|
+
/**
|
|
484
|
+
* The tracker gave up on request `id`: the server never confirmed it, or the connection closed first. The error line
|
|
485
|
+
* says so, and the draft comes back (or, for the Parts button's canned request, the button is released).
|
|
486
|
+
*/
|
|
487
|
+
function notConfirmed(id) {
|
|
488
|
+
reportError(ASK_COPY.didNotConfirm);
|
|
489
|
+
if (id === partsPendingId) {
|
|
490
|
+
// The canned request needs no draft: the button just comes back.
|
|
491
|
+
partsPendingId = null;
|
|
492
|
+
renderPartsAsk();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
notSent = true;
|
|
496
|
+
renderMessage();
|
|
497
|
+
}
|
|
498
|
+
|
|
494
499
|
function setConnected(state) {
|
|
495
500
|
connected = state === 'connected';
|
|
496
501
|
if (state === 'disconnected') {
|
|
497
|
-
status.dropAll();
|
|
498
502
|
partsPendingId = null;
|
|
499
503
|
// The state is unknown once the socket is gone (the page does not reconnect by itself).
|
|
500
504
|
pageRequests.hidden = true;
|
|
@@ -529,5 +533,8 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
529
533
|
setCurrentVersion,
|
|
530
534
|
setPartsAvailable,
|
|
531
535
|
onServerMessage,
|
|
536
|
+
notConfirmed,
|
|
537
|
+
/** Draws the line under the Parts button again from its request's row; the page calls it after every tracker change. */
|
|
538
|
+
refreshPartsLine: () => renderPartsAsk(),
|
|
532
539
|
};
|
|
533
540
|
}
|