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.
Files changed (57) hide show
  1. package/README.md +94 -6
  2. package/dist/ask.js +76 -15
  3. package/dist/mcp.js +198 -26
  4. package/dist/protocol.js +130 -2
  5. package/dist/reference.js +22 -0
  6. package/dist/request-status.js +221 -0
  7. package/dist/session.js +256 -7
  8. package/package.json +1 -1
  9. package/page/ask-view.js +64 -57
  10. package/page/ask.js +363 -188
  11. package/page/bar-model.js +43 -2
  12. package/page/change-timing.js +60 -4
  13. package/page/harmony.js +13 -0
  14. package/page/index.html +7 -4
  15. package/page/jam-analysis.js +68 -23
  16. package/page/jam-model.js +196 -31
  17. package/page/jam-resolve.js +271 -0
  18. package/page/jam-view.js +182 -10
  19. package/page/lights.js +300 -0
  20. package/page/main.js +2171 -188
  21. package/page/midi-clock.js +23 -4
  22. package/page/midi-out.js +17 -2
  23. package/page/mixer-groups.js +114 -0
  24. package/page/mixer-model.js +172 -29
  25. package/page/mixer-rewrite.js +30 -1
  26. package/page/mixer-view.js +289 -14
  27. package/page/note-edit.js +267 -0
  28. package/page/note-locate.js +346 -0
  29. package/page/note-panel.js +402 -0
  30. package/page/note-tag.js +141 -0
  31. package/page/page-rewrite.js +184 -0
  32. package/page/query-check.js +21 -2
  33. package/page/request-status-view.js +419 -0
  34. package/page/request-status.js +686 -0
  35. package/page/song-edit.js +230 -0
  36. package/page/song-fit.js +250 -0
  37. package/page/song-grid-model.js +506 -0
  38. package/page/song-grid-view.js +710 -0
  39. package/page/song-prefs.js +100 -0
  40. package/page/structure-check.js +216 -0
  41. package/page/structure-view.js +38 -0
  42. package/page/structure.js +761 -0
  43. package/page/style.css +1218 -65
  44. package/page/takeover.js +20 -5
  45. package/page/tempo-glide.js +330 -0
  46. package/page/tempo-guard.js +62 -10
  47. package/page/timeline-edit-view.js +1956 -0
  48. package/page/timeline-gesture.js +279 -0
  49. package/page/timeline.js +197 -3
  50. package/page/top-bar-view.js +18 -13
  51. package/page/tracks.js +37 -8
  52. package/page/ui-state.js +10 -5
  53. package/page/views.js +39 -9
  54. package/reference/functions.md +1 -0
  55. package/reference/midi.md +2 -0
  56. package/reference/structure.md +131 -0
  57. 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, the top-bar request status and the top-bar page-requests
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, status timing) live in
9
- // page/ask.js.
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; `fit` is the top bar's fit step, run
35
- * after the status changes because a new text changes the bar's width (a ResizeObserver does not fire for content
36
- * growing inside a full bar).
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, fit }) {
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 fit !== 'function') {
75
- throw new Error('Moontide: createAskView needs the top bar\'s fit function.');
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), the id of its
141
- // request while it waits for Claude to take it, and the top-bar status text now on screen.
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. The caret goes to the end so the user types the question after it. When the draft leaves no
268
- * room at all, the draft stays as it is (the chip still names the error).
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 ? { context, chip } : { context, chip, moment };
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 moment = fitMomentToLine({ ...frame, moment: describedContext.moment });
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
- status.sent(id);
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
- status.handedOver(msg.ids);
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
- status.answered(msg);
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
  }