moontide-mcp 2.1.0 → 2.4.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 +54 -331
- package/dist/ask.js +59 -12
- package/dist/catalog.js +118 -9
- package/dist/instruments.js +83 -9
- package/dist/mcp.js +146 -20
- package/dist/midi-state.js +43 -1
- package/dist/protocol.js +123 -5
- package/dist/reference.js +12 -0
- package/dist/request-status.js +221 -0
- package/dist/session.js +203 -7
- package/dist/vocabulary.js +420 -0
- package/package.json +3 -2
- package/page/ask-view.js +118 -50
- package/page/ask.js +139 -169
- package/page/bar-model.js +44 -2
- package/page/change-timing.js +35 -0
- package/page/index.html +6 -2
- package/page/instruments-model.js +50 -3
- package/page/instruments-view.js +14 -2
- package/page/jam-analysis.js +68 -23
- package/page/jam-model.js +185 -29
- package/page/jam-resolve.js +271 -0
- package/page/jam-view.js +165 -10
- package/page/lights.js +29 -7
- package/page/main.js +807 -66
- package/page/midi-clock.js +22 -3
- package/page/midi-in.js +407 -0
- package/page/midi-out.js +557 -48
- package/page/midi-plan.js +42 -6
- package/page/midi-rig.js +286 -13
- package/page/midi-settings.js +507 -0
- package/page/mixer-view.js +7 -1
- package/page/note-panel.js +29 -8
- package/page/note-tag.js +19 -4
- package/page/page-rewrite.js +1 -1
- package/page/played-buffer.js +208 -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/scratch-gesture.js +239 -0
- package/page/scratch-listen.js +260 -0
- package/page/scratch-model.js +1696 -0
- package/page/scratch-view.js +2176 -0
- package/page/song-grid-model.js +15 -0
- package/page/song-grid-view.js +1 -1
- package/page/structure.js +2 -1
- package/page/style.css +1243 -43
- package/page/tempo-glide.js +330 -0
- package/page/tempo-guard.js +62 -10
- package/page/timeline-edit-view.js +1 -1
- package/page/timeline.js +2 -1
- package/page/top-bar-view.js +18 -13
- package/page/views.js +20 -7
- package/reference/midi.md +2 -0
- package/reference/motif.md +83 -0
- package/reference/settings.md +161 -0
- package/reference/transitions.md +13 -1
package/page/ask-view.js
CHANGED
|
@@ -1,52 +1,54 @@
|
|
|
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,
|
|
13
14
|
BOX_GAP_REM,
|
|
14
15
|
MAX_ASK_TEXT_LENGTH,
|
|
16
|
+
MAX_MOTIF_PHRASE_LENGTH,
|
|
17
|
+
MAX_REQUEST_LINE_BYTES,
|
|
15
18
|
PAGE_REQUESTS_COPY,
|
|
16
19
|
pageRequestsText,
|
|
17
20
|
askMessage,
|
|
18
21
|
askTextProblem,
|
|
19
|
-
createAskStatus,
|
|
20
22
|
cutText,
|
|
21
23
|
describeContext,
|
|
22
24
|
fitGestureToLine,
|
|
23
25
|
fitMomentToLine,
|
|
26
|
+
motifChip,
|
|
24
27
|
newAskId,
|
|
25
28
|
partsAskState,
|
|
26
29
|
placeBox,
|
|
27
30
|
refusalText,
|
|
28
31
|
requestFitsLine,
|
|
32
|
+
requestLineBytes,
|
|
29
33
|
} from './ask.js';
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* Creates the box controller. `document` is the page's document (named so on purpose: every element
|
|
33
37
|
* lookup below reads `document.getElementById`, the shape test/page-hidden.test.js scans); `send`
|
|
34
38
|
* sends one JSON-able message to the server; `reportError` and `clearError` write and clear the
|
|
35
|
-
* top-bar error line; `setTimer`, `clearTimer` and `now` are the page's clock
|
|
36
|
-
*
|
|
37
|
-
*
|
|
39
|
+
* top-bar error line; `setTimer`, `clearTimer` and `now` are the page's clock. `requests` is the per-request tracker
|
|
40
|
+
* of page/request-status.js: every request sent is handed to it, and so is every server answer to one. The tracker
|
|
41
|
+
* tells the page (main.js) when a request was never confirmed; the page then calls `notConfirmed(id)` here. `onMotifResult` is
|
|
42
|
+
* called with what became of the Scratchpad's motif request (`{ status: 'received' }` once Claude has it, `{ status: 'waiting' }` once the
|
|
43
|
+
* server holds it for Claude's next call); the motif request has its own pending id, so nothing it meets ever reaches the Ask box or its draft.
|
|
38
44
|
*/
|
|
39
|
-
export function createAskView({ document, send, reportError, clearError, setTimer, clearTimer, now,
|
|
45
|
+
export function createAskView({ document, send, reportError, clearError, setTimer, clearTimer, now, requests, onMotifResult = () => {} }) {
|
|
40
46
|
const askBox = document.getElementById('ask-box');
|
|
41
47
|
const askContextChip = document.getElementById('ask-context-chip');
|
|
42
48
|
const askCurrentChip = document.getElementById('ask-current-chip');
|
|
43
49
|
const askField = document.getElementById('ask-field');
|
|
44
50
|
const askMessageEl = document.getElementById('ask-message');
|
|
45
51
|
const askHint = document.getElementById('ask-hint');
|
|
46
|
-
const askStatus = document.getElementById('ask-status');
|
|
47
|
-
// The status holds its long sentence and a short form (shown at fit step 3, aria-hidden).
|
|
48
|
-
const askLong = askStatus === null ? null : askStatus.querySelector('.ask-long');
|
|
49
|
-
const askShort = askStatus === null ? null : askStatus.querySelector('.ask-short');
|
|
50
52
|
const partsAsk = document.getElementById('parts-ask');
|
|
51
53
|
const partsAskStatus = document.getElementById('parts-ask-status');
|
|
52
54
|
const pageRequests = document.getElementById('page-requests');
|
|
@@ -60,9 +62,6 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
60
62
|
!askField ||
|
|
61
63
|
!askMessageEl ||
|
|
62
64
|
!askHint ||
|
|
63
|
-
!askStatus ||
|
|
64
|
-
!askLong ||
|
|
65
|
-
!askShort ||
|
|
66
65
|
!partsAsk ||
|
|
67
66
|
!partsAskStatus ||
|
|
68
67
|
!pageRequests ||
|
|
@@ -72,8 +71,8 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
72
71
|
) {
|
|
73
72
|
throw new Error('Moontide: the Ask Claude markup is missing from the page.');
|
|
74
73
|
}
|
|
75
|
-
if (typeof
|
|
76
|
-
throw new Error('Moontide: createAskView needs the
|
|
74
|
+
if (requests === undefined || typeof requests.sent !== 'function' || typeof requests.serverMessage !== 'function') {
|
|
75
|
+
throw new Error('Moontide: createAskView needs the request tracker.');
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
// The field's description lists the message line only while it shows, so it is never empty.
|
|
@@ -139,36 +138,12 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
139
138
|
// The side the box opened on (above the pointer or below), kept while its height changes; null
|
|
140
139
|
// means choose again.
|
|
141
140
|
let placedAbove = null;
|
|
142
|
-
// The Parts view's button: whether the code has no sections (so the button shows)
|
|
143
|
-
// request while it waits for Claude to take it
|
|
141
|
+
// The Parts view's button: whether the code has no sections (so the button shows) and the id of its
|
|
142
|
+
// request while it waits for Claude to take it.
|
|
144
143
|
let partsAvailable = false;
|
|
145
144
|
let partsPendingId = null;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const status = createAskStatus({
|
|
149
|
-
render: (text, short) => {
|
|
150
|
-
statusText = text;
|
|
151
|
-
askLong.textContent = text ?? '';
|
|
152
|
-
askShort.textContent = short ?? '';
|
|
153
|
-
askStatus.hidden = text === null;
|
|
154
|
-
fit();
|
|
155
|
-
renderPartsAsk();
|
|
156
|
-
},
|
|
157
|
-
onTimeout: (id) => {
|
|
158
|
-
reportError(ASK_COPY.didNotConfirm);
|
|
159
|
-
if (id === partsPendingId) {
|
|
160
|
-
// The canned request needs no draft: the button just comes back.
|
|
161
|
-
partsPendingId = null;
|
|
162
|
-
renderPartsAsk();
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
notSent = true;
|
|
166
|
-
renderMessage();
|
|
167
|
-
},
|
|
168
|
-
setTimer,
|
|
169
|
-
clearTimer,
|
|
170
|
-
now,
|
|
171
|
-
});
|
|
145
|
+
// The Scratchpad's motif request, the same way: the id of the one on its way, so its refusal never arms the Ask box.
|
|
146
|
+
let motifPendingId = null;
|
|
172
147
|
|
|
173
148
|
/** The fields of the request the box would send for `text` (trimmed), the id measured with the next one's shape. */
|
|
174
149
|
function requestFields(text) {
|
|
@@ -202,6 +177,14 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
202
177
|
refit();
|
|
203
178
|
}
|
|
204
179
|
|
|
180
|
+
/** The state phrase of the Parts request's row in the Requests list, or null when it has no row. */
|
|
181
|
+
function partsRowPhrase() {
|
|
182
|
+
if (partsPendingId === null) return null;
|
|
183
|
+
const model = requests.model();
|
|
184
|
+
const row = [...model.rows, ...model.recent].find((candidate) => candidate.id === partsPendingId);
|
|
185
|
+
return row === undefined ? null : row.phrase;
|
|
186
|
+
}
|
|
187
|
+
|
|
205
188
|
/** The Parts button and the line under it, from partsAskState. */
|
|
206
189
|
function renderPartsAsk() {
|
|
207
190
|
partsAsk.hidden = !partsAvailable;
|
|
@@ -210,7 +193,7 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
210
193
|
partsAskStatus.hidden = true;
|
|
211
194
|
return;
|
|
212
195
|
}
|
|
213
|
-
const state = partsAskState({ connected, pending: partsPendingId !== null, statusText });
|
|
196
|
+
const state = partsAskState({ connected, pending: partsPendingId !== null, statusText: partsRowPhrase() });
|
|
214
197
|
partsAsk.disabled = state.disabled;
|
|
215
198
|
partsAskStatus.textContent = state.line ?? '';
|
|
216
199
|
partsAskStatus.hidden = state.line === null;
|
|
@@ -344,7 +327,7 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
344
327
|
send(frame);
|
|
345
328
|
// While the token is refused the line stays: the server's 'token' refusal writes it again, so it never blinks off.
|
|
346
329
|
if (!authRefused) clearError();
|
|
347
|
-
|
|
330
|
+
requests.sent(id, { text, chip: describedContext.chip });
|
|
348
331
|
return id;
|
|
349
332
|
}
|
|
350
333
|
|
|
@@ -455,6 +438,46 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
455
438
|
renderPartsAsk();
|
|
456
439
|
});
|
|
457
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Sends the Scratchpad's motif as a page request: `words` are the user's own (trimmed) and `motif` is the motif field of
|
|
443
|
+
* page/scratch-model.js's motifRequest. The request has its own pending id, goes to the tracker like any other and never
|
|
444
|
+
* touches the Ask box. Returns `{ id }` once it is sent, or `{ refused: word, ...numbers }` and sends nothing: 'disconnected',
|
|
445
|
+
* 'pending' (the last motif request has not reached the server: the server has not answered it yet), 'empty-words', 'words-too-long' (`length`, `max`) or 'too-big'
|
|
446
|
+
* (`bytes` and `max`, measured on the exact frame, id included; a phrase over MAX_MOTIF_PHRASE_LENGTH characters is refused the same way). The
|
|
447
|
+
* phrase is never cut: a motif that does not fit is refused whole.
|
|
448
|
+
*/
|
|
449
|
+
function sendMotif({ words, motif }) {
|
|
450
|
+
if (!connected) return { refused: 'disconnected' };
|
|
451
|
+
if (motifPendingId !== null) return { refused: 'pending' };
|
|
452
|
+
const text = words.trim();
|
|
453
|
+
if (text === '') return { refused: 'empty-words' };
|
|
454
|
+
if (text.length > MAX_ASK_TEXT_LENGTH) return { refused: 'words-too-long', length: text.length, max: MAX_ASK_TEXT_LENGTH };
|
|
455
|
+
const id = newAskId(counter + 1, now());
|
|
456
|
+
// A phrase longer than the server takes is refused like any motif too big for the line, before describeContext (which throws on it)
|
|
457
|
+
// checks the motif. The bytes are those of the line it would make; the rest of the request always puts that line over its bound.
|
|
458
|
+
if (motif !== null && typeof motif === 'object' && typeof motif.phrase === 'string' && motif.phrase.length > MAX_MOTIF_PHRASE_LENGTH) {
|
|
459
|
+
const bytes = requestLineBytes({ id, text, context: { kind: 'motif' }, chip: motifChip(motif), current: currentVersion, motif });
|
|
460
|
+
return { refused: 'too-big', bytes, max: MAX_REQUEST_LINE_BYTES };
|
|
461
|
+
}
|
|
462
|
+
const described = describeContext({ kind: 'motif', motif });
|
|
463
|
+
const frame = { type: 'ask', id, text, context: described.context, chip: described.chip, current: currentVersion, motif: described.motif };
|
|
464
|
+
const bytes = requestLineBytes(frame);
|
|
465
|
+
if (bytes > MAX_REQUEST_LINE_BYTES) return { refused: 'too-big', bytes, max: MAX_REQUEST_LINE_BYTES };
|
|
466
|
+
counter += 1;
|
|
467
|
+
send(frame);
|
|
468
|
+
// While the token is refused the line stays: the server's 'token' refusal writes it again, so it never blinks off.
|
|
469
|
+
if (!authRefused) clearError();
|
|
470
|
+
requests.sent(frame.id, { text, chip: described.chip });
|
|
471
|
+
motifPendingId = frame.id;
|
|
472
|
+
return { id: frame.id };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** The server has the motif request: it is no longer on its way, and the page is told. */
|
|
476
|
+
function arrivedMotif() {
|
|
477
|
+
motifPendingId = null;
|
|
478
|
+
onMotifResult({ status: 'received' });
|
|
479
|
+
}
|
|
480
|
+
|
|
458
481
|
/** The server's answers: token result, one result per request, and hand-overs of waiting requests. */
|
|
459
482
|
function onServerMessage(msg) {
|
|
460
483
|
if (msg.type === 'ask-auth') {
|
|
@@ -478,13 +501,28 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
478
501
|
return;
|
|
479
502
|
}
|
|
480
503
|
if (msg.type === 'ask-received') {
|
|
481
|
-
|
|
504
|
+
requests.serverMessage(msg);
|
|
482
505
|
if (partsPendingId !== null && msg.ids.includes(partsPendingId)) partsPendingId = null;
|
|
506
|
+
if (motifPendingId !== null && msg.ids.includes(motifPendingId)) arrivedMotif();
|
|
483
507
|
renderPartsAsk();
|
|
484
508
|
return;
|
|
485
509
|
}
|
|
486
510
|
if (msg.type !== 'ask-result') return;
|
|
487
|
-
|
|
511
|
+
requests.serverMessage(msg);
|
|
512
|
+
if (msg.id === motifPendingId) {
|
|
513
|
+
// The Scratchpad's request is answered in its own strip: nothing of it reaches the Ask box, its draft or its not-sent message.
|
|
514
|
+
if (msg.status === 'received') {
|
|
515
|
+
arrivedMotif();
|
|
516
|
+
} else if (msg.status === 'waiting') {
|
|
517
|
+
// The server holds it until Claude's next Moontide call: it is no longer on its way, so the next motif may go, and the strip says it waits.
|
|
518
|
+
motifPendingId = null;
|
|
519
|
+
onMotifResult({ status: 'waiting' });
|
|
520
|
+
} else if (msg.status === 'refused') {
|
|
521
|
+
motifPendingId = null;
|
|
522
|
+
onMotifResult({ status: 'refused', text: refusalText(msg.reason, msg.detail) });
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
488
526
|
const fromParts = msg.id === partsPendingId;
|
|
489
527
|
if (msg.status === 'refused') {
|
|
490
528
|
reportError(refusalText(msg.reason, msg.detail));
|
|
@@ -505,11 +543,33 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
505
543
|
renderPartsAsk();
|
|
506
544
|
}
|
|
507
545
|
|
|
546
|
+
/**
|
|
547
|
+
* The tracker gave up on request `id`: the server never confirmed it, or the connection closed first. The error line
|
|
548
|
+
* says so, and the draft comes back (or, for the Parts button's canned request, the button is released).
|
|
549
|
+
*/
|
|
550
|
+
function notConfirmed(id) {
|
|
551
|
+
if (id === motifPendingId) {
|
|
552
|
+
// The Scratchpad's strip says it; the Ask box and its draft are not involved.
|
|
553
|
+
motifPendingId = null;
|
|
554
|
+
onMotifResult({ status: 'not-confirmed' });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
reportError(ASK_COPY.didNotConfirm);
|
|
558
|
+
if (id === partsPendingId) {
|
|
559
|
+
// The canned request needs no draft: the button just comes back.
|
|
560
|
+
partsPendingId = null;
|
|
561
|
+
renderPartsAsk();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
notSent = true;
|
|
565
|
+
renderMessage();
|
|
566
|
+
}
|
|
567
|
+
|
|
508
568
|
function setConnected(state) {
|
|
509
569
|
connected = state === 'connected';
|
|
510
570
|
if (state === 'disconnected') {
|
|
511
|
-
status.dropAll();
|
|
512
571
|
partsPendingId = null;
|
|
572
|
+
motifPendingId = null;
|
|
513
573
|
// The state is unknown once the socket is gone (the page does not reconnect by itself).
|
|
514
574
|
pageRequests.hidden = true;
|
|
515
575
|
closeHelp();
|
|
@@ -541,7 +601,15 @@ export function createAskView({ document, send, reportError, clearError, setTime
|
|
|
541
601
|
contains: (node) => askBox.contains(node),
|
|
542
602
|
setConnected,
|
|
543
603
|
setCurrentVersion,
|
|
604
|
+
sendMotif,
|
|
605
|
+
/** True while a motif request is on its way to the server. */
|
|
606
|
+
motifPending: () => motifPendingId !== null,
|
|
607
|
+
/** True while the page is connected to the server. */
|
|
608
|
+
isConnected: () => connected,
|
|
544
609
|
setPartsAvailable,
|
|
545
610
|
onServerMessage,
|
|
611
|
+
notConfirmed,
|
|
612
|
+
/** Draws the line under the Parts button again from its request's row; the page calls it after every tracker change. */
|
|
613
|
+
refreshPartsLine: () => renderPartsAsk(),
|
|
546
614
|
};
|
|
547
615
|
}
|