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/dist/session.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { AskChannel, listenerScriptPath } from './ask.js';
|
|
3
|
+
import { NOT_OPEN_REQUEST_MESSAGE, RequestStatusBoard, cleanAnswerText } from './request-status.js';
|
|
3
4
|
import { MIDI_PERMISSION_WAIT_MESSAGE, MidiChannel } from './midi-state.js';
|
|
4
5
|
import { applyEdits } from './edit-code.js';
|
|
5
6
|
import { CatalogSaveError, MAX_CATALOG_DRAFTS, MAX_CATALOG_KITS, MAX_CATALOG_SOUNDS, MAX_PART_SOUNDS, catalogFileExists, countText, flattenCatalogPart, parseCatalog, problemsText, saveCatalog, summarizeCatalog, } from './catalog.js';
|
|
@@ -71,6 +72,14 @@ export function cleanErrorDetail(detail) {
|
|
|
71
72
|
}
|
|
72
73
|
return codePoints.slice(0, MAX_DETAIL_CODE_POINTS).join('') + '…';
|
|
73
74
|
}
|
|
75
|
+
/** The page's refusal text of a tempo ramp it could not make, trimmed and cut like any error detail but kept as a sentence. */
|
|
76
|
+
function cleanRefusal(text) {
|
|
77
|
+
const trimmed = text.trim();
|
|
78
|
+
if (trimmed === '')
|
|
79
|
+
return UNKNOWN_ERROR_DETAIL;
|
|
80
|
+
const codePoints = Array.from(trimmed);
|
|
81
|
+
return codePoints.length <= MAX_DETAIL_CODE_POINTS ? trimmed : codePoints.slice(0, MAX_DETAIL_CODE_POINTS).join('') + '…';
|
|
82
|
+
}
|
|
74
83
|
/**
|
|
75
84
|
* Builds the tool-response text for a failed evaluation: Strudel's message, the line when
|
|
76
85
|
* known, and a truthful note about what is (or isn't) still playing.
|
|
@@ -189,6 +198,26 @@ export function takeoverCanceledLine(cycle, reason) {
|
|
|
189
198
|
}
|
|
190
199
|
return `${head} the page was closed, reloaded or replaced by another tab before then. get_code returns the code from before it.`;
|
|
191
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The note Claude gets, once, when a tempo ramp ended without Claude asking for it: a program stated another tempo, a
|
|
203
|
+
* program stated no tempo, a step failed, or the page that ran it was closed, reloaded or replaced by another tab.
|
|
204
|
+
* Stop, a finished ramp and a replaced ramp say nothing. Only the cause (a fixed word) and a number come in: no
|
|
205
|
+
* page text. `bpm` is the tempo that plays after the end; the wording leaves it out when the page gave none.
|
|
206
|
+
*/
|
|
207
|
+
export function glideEndedLine(reason, bpm) {
|
|
208
|
+
const shown = bpm === undefined ? undefined : Number.isInteger(bpm) ? String(bpm) : bpm.toFixed(1);
|
|
209
|
+
const head = 'Note: the tempo ramp ended early:';
|
|
210
|
+
if (reason === 'other-tempo') {
|
|
211
|
+
return `${head} a program stated another tempo${shown === undefined ? '' : `, and ${shown} BPM plays now`}.`;
|
|
212
|
+
}
|
|
213
|
+
if (reason === 'no-tempo') {
|
|
214
|
+
return `${head} a program stated no tempo${shown === undefined ? '' : `, so the tempo stays at ${shown} BPM, where the ramp had got to`}.`;
|
|
215
|
+
}
|
|
216
|
+
if (reason === 'error') {
|
|
217
|
+
return `${head} a step failed in the page${shown === undefined ? '' : `, so the tempo stays at ${shown} BPM`}.`;
|
|
218
|
+
}
|
|
219
|
+
return `${head} the page was closed, reloaded or replaced by another tab, so the tempo of the code plays.`;
|
|
220
|
+
}
|
|
192
221
|
/**
|
|
193
222
|
* The note when a change that waited for its line took over and replaced code that had been edited by hand
|
|
194
223
|
* in the page. Said at the takeover, never at the request: until the line the hand edit is still what plays, and a
|
|
@@ -261,6 +290,11 @@ export class Session {
|
|
|
261
290
|
pendingChange = null;
|
|
262
291
|
// The notes of waiting changes that did not take over, queued once and drained once, like historyNotes.
|
|
263
292
|
takeoverNotes = [];
|
|
293
|
+
// The tempo ramp the page runs, as far as the server knows: set when a reply says a ramp started (or the takeover of a
|
|
294
|
+
// change whose ramp starts at its line), cleared by Stop, a page end and a page loss. Null when none runs.
|
|
295
|
+
glide = null;
|
|
296
|
+
// The notes of a ramp that ended early, queued once and drained once, like takeoverNotes.
|
|
297
|
+
glideNotes = [];
|
|
264
298
|
// The notes of load failure reports a page sent, one per distinct text (at most three), drained once.
|
|
265
299
|
loadFailureNotes = new Set();
|
|
266
300
|
// BPM: the tempo (rounded to one decimal) the last set_code or restore_version reply stated; null before any.
|
|
@@ -280,6 +314,8 @@ export class Session {
|
|
|
280
314
|
supersededPages = new Set();
|
|
281
315
|
/** The page-to-Claude channel (tokens, trust, limits, queue, listener). */
|
|
282
316
|
ask;
|
|
317
|
+
/** What became of each page request: started, waiting for its line, applied and the other states. */
|
|
318
|
+
requests;
|
|
283
319
|
/** The MIDI facts of the active page, worded once per change into notice lines. */
|
|
284
320
|
midi;
|
|
285
321
|
// The setup file's store (null without one), the JSON of the last midi-rig each page was
|
|
@@ -311,12 +347,36 @@ export class Session {
|
|
|
311
347
|
this.rig = options?.rig ?? null;
|
|
312
348
|
this.build = options?.build ?? null;
|
|
313
349
|
this.recordIntentTtlMs = options?.recordIntentTtlMs ?? RECORD_INTENT_TTL_MS;
|
|
350
|
+
const send = (conn, msg, what) => this.sendToPage(conn, msg, what);
|
|
351
|
+
this.requests = new RequestStatusBoard({ send });
|
|
314
352
|
this.ask = new AskChannel({
|
|
315
|
-
send
|
|
353
|
+
send,
|
|
354
|
+
board: this.requests,
|
|
316
355
|
ackTimeoutMs: options?.askAckTimeoutMs,
|
|
317
356
|
});
|
|
318
357
|
this.midi = new MidiChannel();
|
|
319
358
|
}
|
|
359
|
+
/** A Moontide tool call began: the requests already handed to Claude are being worked on. */
|
|
360
|
+
noteToolCall() {
|
|
361
|
+
this.requests.noteToolCall();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Claude closes an open page request in words. The sentence is cleaned first; one that is empty after
|
|
365
|
+
* cleaning, and an id that is not an open request, are refused with a plain error and change nothing.
|
|
366
|
+
*/
|
|
367
|
+
answerRequest(id, outcome, text) {
|
|
368
|
+
const sentence = cleanAnswerText(text);
|
|
369
|
+
if (sentence === '') {
|
|
370
|
+
throw new Error('The sentence is empty after removing spaces and control characters; give one sentence of words.');
|
|
371
|
+
}
|
|
372
|
+
if (!this.requests.closed(id, outcome, sentence)) {
|
|
373
|
+
throw new Error(NOT_OPEN_REQUEST_MESSAGE);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** Whether a page request with this id is on the board and not finished. */
|
|
377
|
+
requestIsOpen(id) {
|
|
378
|
+
return this.requests.isOpen(id);
|
|
379
|
+
}
|
|
320
380
|
setPageUrl(url) {
|
|
321
381
|
this.pageUrl = url;
|
|
322
382
|
}
|
|
@@ -362,6 +422,42 @@ export class Session {
|
|
|
362
422
|
return;
|
|
363
423
|
this.pendingChange = null;
|
|
364
424
|
this.takeoverNotes.push(takeoverCanceledLine(waiting.cycle, reason));
|
|
425
|
+
const answering = waiting.intent?.answering;
|
|
426
|
+
if (answering === undefined)
|
|
427
|
+
return;
|
|
428
|
+
if (reason !== 'replaced') {
|
|
429
|
+
this.requests.canceled(answering);
|
|
430
|
+
}
|
|
431
|
+
else if (!this.answeringStillCarried(answering, waiting.id)) {
|
|
432
|
+
this.requests.replaced(answering);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* The ramp ended. Clears the record and queues its one note for the next tool reply, except for a finished ramp and
|
|
437
|
+
* for Stop, which are not told. A no-op when no ramp is recorded, so a second cause (a cancel, then a closing page)
|
|
438
|
+
* never adds a second line.
|
|
439
|
+
*/
|
|
440
|
+
dropGlide(reason, bpm) {
|
|
441
|
+
if (this.glide === null)
|
|
442
|
+
return;
|
|
443
|
+
this.glide = null;
|
|
444
|
+
if (reason === 'finished' || reason === 'stop')
|
|
445
|
+
return;
|
|
446
|
+
this.glideNotes.push(glideEndedLine(reason, bpm));
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Whether a change other than this one still in flight answers the same request. A newer change that
|
|
450
|
+
* names the request carries its answer on, so the request must not end as replaced while that answer is
|
|
451
|
+
* still coming: the newer change settles it (applied, failed or replaced in its turn).
|
|
452
|
+
*/
|
|
453
|
+
answeringStillCarried(answering, exceptEvaluateId) {
|
|
454
|
+
for (const [evaluateId, entry] of this.pending) {
|
|
455
|
+
if (entry.kind === 'evaluate' && evaluateId !== exceptEvaluateId) {
|
|
456
|
+
if (this.recordIntents.get(evaluateId)?.answering === answering)
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
365
461
|
}
|
|
366
462
|
/**
|
|
367
463
|
* Keeps the change the page accepted for its line. One at most (last wins): a change that is
|
|
@@ -369,16 +465,32 @@ export class Session {
|
|
|
369
465
|
* itself (takeover-canceled 'replaced').
|
|
370
466
|
*/
|
|
371
467
|
keepWaitingChange(change) {
|
|
372
|
-
|
|
373
|
-
|
|
468
|
+
const replaced = this.pendingChange;
|
|
469
|
+
if (replaced !== null && replaced.id !== change.id) {
|
|
470
|
+
console.error(`[moontide] A waiting change (${replaced.id}) was replaced by request ${change.id} without a cancel message`);
|
|
471
|
+
// The same rule as a cancel message: the request ends as replaced unless the new change answers it too.
|
|
472
|
+
const answering = replaced.intent?.answering;
|
|
473
|
+
if (answering !== undefined && answering !== change.intent?.answering) {
|
|
474
|
+
this.requests.replaced(answering);
|
|
475
|
+
}
|
|
374
476
|
}
|
|
375
477
|
this.pendingChange = change;
|
|
478
|
+
if (change.intent?.answering !== undefined) {
|
|
479
|
+
this.requests.line(change.intent.answering, change.at === 'phrase' ? 'phrase' : 'bar');
|
|
480
|
+
}
|
|
376
481
|
}
|
|
377
482
|
/**
|
|
378
483
|
* BPM: rounds the tempo of a reply to one decimal and says whether it differs from the last tempo a reply
|
|
379
484
|
* stated. A reply without a tempo leaves the last told tempo alone.
|
|
380
485
|
*/
|
|
381
|
-
noteTempo(bpm) {
|
|
486
|
+
noteTempo(bpm, glide) {
|
|
487
|
+
// A reply that states a ramp says the ramp, not a tempo change: the ramp's target is the tempo told from here on, so the
|
|
488
|
+
// next reply never announces it as a change when the ramp gets there.
|
|
489
|
+
if (glide !== undefined) {
|
|
490
|
+
const target = Math.round(glide.toBpm * 10) / 10;
|
|
491
|
+
this.lastToldBpm = target;
|
|
492
|
+
return { bpm: bpm === undefined ? null : Math.round(bpm * 10) / 10, tempoChanged: false };
|
|
493
|
+
}
|
|
382
494
|
if (bpm === undefined)
|
|
383
495
|
return { bpm: null, tempoChanged: false };
|
|
384
496
|
const rounded = Math.round(bpm * 10) / 10;
|
|
@@ -404,6 +516,7 @@ export class Session {
|
|
|
404
516
|
// A change that waits does not survive a page taking the role: the page that held its gate is
|
|
405
517
|
// gone or replays its code, so Claude is told once (the line is queued once however many causes meet).
|
|
406
518
|
this.dropWaitingChange('page');
|
|
519
|
+
this.dropGlide('page');
|
|
407
520
|
this.supersededPages.delete(conn);
|
|
408
521
|
if (previous !== null && previous !== conn) {
|
|
409
522
|
this.supersededPages.add(previous);
|
|
@@ -458,6 +571,7 @@ export class Session {
|
|
|
458
571
|
if (this.activePage === conn) {
|
|
459
572
|
this.activePage = null;
|
|
460
573
|
this.dropWaitingChange('page');
|
|
574
|
+
this.dropGlide('page');
|
|
461
575
|
this.midi.clear();
|
|
462
576
|
this.lastMidiFacts = null;
|
|
463
577
|
// A superseded tab says "playing in another tab": tell it that no tab plays any more, or it
|
|
@@ -582,6 +696,10 @@ export class Session {
|
|
|
582
696
|
this.handleTakeoverCanceled(conn, msg);
|
|
583
697
|
return;
|
|
584
698
|
}
|
|
699
|
+
if (msg.type === 'glide-ended') {
|
|
700
|
+
this.handleGlideEnded(conn, msg);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
585
703
|
if (msg.type === 'user-stopped') {
|
|
586
704
|
this.handleUserStopped(conn);
|
|
587
705
|
return;
|
|
@@ -623,6 +741,13 @@ export class Session {
|
|
|
623
741
|
this.ask.handleAsk(conn, msg);
|
|
624
742
|
return;
|
|
625
743
|
}
|
|
744
|
+
if (msg.type === 'ask-board-request') {
|
|
745
|
+
const requests = this.requests.snapshotFor(conn);
|
|
746
|
+
// The board told the page where each request stands; the hand-over messages that follow reach it too.
|
|
747
|
+
this.ask.adoptOrigin(conn, requests.map((row) => row.id));
|
|
748
|
+
this.sendToPage(conn, { type: 'ask-board', requests }, 'the request board');
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
626
751
|
if (msg.type === 'history-request') {
|
|
627
752
|
this.handleHistoryRequest(conn);
|
|
628
753
|
return;
|
|
@@ -1165,6 +1290,8 @@ export class Session {
|
|
|
1165
1290
|
return;
|
|
1166
1291
|
}
|
|
1167
1292
|
this.stopped = true;
|
|
1293
|
+
// Stop ends a ramp too; it is the user's own act, so Claude is not told.
|
|
1294
|
+
this.dropGlide('stop');
|
|
1168
1295
|
// A stop the page reports ends a wait too; the page's takeover-canceled for the same id (sent
|
|
1169
1296
|
// just before) already queued the line, and a second cause adds nothing.
|
|
1170
1297
|
this.dropWaitingChange('stop');
|
|
@@ -1186,6 +1313,7 @@ export class Session {
|
|
|
1186
1313
|
cycle: msg.pending.cycle,
|
|
1187
1314
|
at: lateIntent?.at ?? 'bar',
|
|
1188
1315
|
intent: lateIntent,
|
|
1316
|
+
...(msg.glide?.state === 'at-line' ? { glide: msg.glide } : {}),
|
|
1189
1317
|
});
|
|
1190
1318
|
console.error(`[moontide] Late pending confirmation kept for request ${msg.id}`);
|
|
1191
1319
|
}
|
|
@@ -1208,6 +1336,15 @@ export class Session {
|
|
|
1208
1336
|
this.handFailureLine = null;
|
|
1209
1337
|
const lateVersionId = this.recordConfirmed(conn, msg.code, lateOrigin, lateIntent);
|
|
1210
1338
|
this.rememberVersion(msg.id, lateVersionId);
|
|
1339
|
+
// A reply whose audio is stopped records no ramp: Stop ends every ramp, so none runs.
|
|
1340
|
+
// An at-line report on a reply with no pending line is a waiting change that already took over: its ramp started at its line.
|
|
1341
|
+
if ((msg.glide?.state === 'started' || msg.glide?.state === 'at-line') && msg.audio !== 'stopped') {
|
|
1342
|
+
this.glide = { toBpm: msg.glide.toBpm };
|
|
1343
|
+
}
|
|
1344
|
+
// This branch does not pass through confirmSuccess, so it tells the page itself.
|
|
1345
|
+
if (lateIntent?.answering !== undefined) {
|
|
1346
|
+
this.requests.applied(lateIntent.answering, lateVersionId);
|
|
1347
|
+
}
|
|
1211
1348
|
console.error(`[moontide] Late confirmation stored for request ${msg.id}`);
|
|
1212
1349
|
}
|
|
1213
1350
|
else if (msg.ok) {
|
|
@@ -1241,6 +1378,7 @@ export class Session {
|
|
|
1241
1378
|
cycle: msg.pending.cycle,
|
|
1242
1379
|
at: intent?.at ?? 'bar',
|
|
1243
1380
|
intent,
|
|
1381
|
+
...(msg.glide?.state === 'at-line' ? { glide: msg.glide } : {}),
|
|
1244
1382
|
});
|
|
1245
1383
|
entry.resolve({
|
|
1246
1384
|
code: msg.code,
|
|
@@ -1249,17 +1387,25 @@ export class Session {
|
|
|
1249
1387
|
replacedHandEdit: false,
|
|
1250
1388
|
versionId: null,
|
|
1251
1389
|
pending: msg.pending,
|
|
1252
|
-
...this.noteTempo(msg.bpm),
|
|
1390
|
+
...this.noteTempo(msg.bpm, msg.glide),
|
|
1391
|
+
...(msg.glide !== undefined ? { glide: msg.glide } : {}),
|
|
1253
1392
|
});
|
|
1254
1393
|
}
|
|
1255
1394
|
else {
|
|
1256
1395
|
const { replacedHandEdit, versionId } = this.confirmSuccess(conn, msg.id, msg.code, msg.audio, intent);
|
|
1396
|
+
// A reply whose audio is stopped states and records no ramp: Stop ends every ramp, so none runs.
|
|
1397
|
+
const glide = msg.audio === 'stopped' ? undefined : msg.glide;
|
|
1398
|
+
// A ramp this change started is the one the server tells about when it ends early; a running report leaves the record.
|
|
1399
|
+
// An at-line report here is a waiting change that already took over when it was answered: its ramp started at its line.
|
|
1400
|
+
if (glide?.state === 'started' || glide?.state === 'at-line')
|
|
1401
|
+
this.glide = { toBpm: glide.toBpm };
|
|
1257
1402
|
entry.resolve({
|
|
1258
1403
|
code: msg.code,
|
|
1259
1404
|
audio: msg.audio,
|
|
1260
1405
|
replacedHandEdit,
|
|
1261
1406
|
versionId,
|
|
1262
|
-
...this.noteTempo(msg.bpm),
|
|
1407
|
+
...this.noteTempo(msg.bpm, glide),
|
|
1408
|
+
...(glide !== undefined ? { glide } : {}),
|
|
1263
1409
|
});
|
|
1264
1410
|
}
|
|
1265
1411
|
}
|
|
@@ -1274,7 +1420,24 @@ export class Session {
|
|
|
1274
1420
|
}
|
|
1275
1421
|
}
|
|
1276
1422
|
else {
|
|
1423
|
+
const failedIntent = this.recordIntents.get(msg.id);
|
|
1277
1424
|
this.recordIntents.delete(msg.id);
|
|
1425
|
+
const failedFor = failedIntent?.answering;
|
|
1426
|
+
// The same rule as a replaced change: an earlier change for the same request that still waits for
|
|
1427
|
+
// its line, or is still on its way, carries the answer on and settles the request in its turn.
|
|
1428
|
+
// A ramp the page refused was refused whole and nothing changed, so the request stays open for the change Claude sends
|
|
1429
|
+
// next (without glideBars), which can still answer it.
|
|
1430
|
+
if (msg.glideRefused !== true &&
|
|
1431
|
+
failedFor !== undefined &&
|
|
1432
|
+
this.pendingChange?.intent?.answering !== failedFor &&
|
|
1433
|
+
!this.answeringStillCarried(failedFor, msg.id)) {
|
|
1434
|
+
this.requests.failed(failedFor, cleanErrorDetail(msg.error));
|
|
1435
|
+
}
|
|
1436
|
+
if (msg.glideRefused === true) {
|
|
1437
|
+
// The page's own refusal text, whole: it already says that nothing was changed, so no evaluation-failure tail is added.
|
|
1438
|
+
entry.reject(new Error(cleanRefusal(msg.error)));
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1278
1441
|
const tail = this.code === '' ? NOTHING_PLAYING_TAIL : this.stopped ? STILL_STOPPED_TAIL : STILL_PLAYING_TAIL;
|
|
1279
1442
|
entry.reject(new Error(evalFailedMessage(msg.error, msg.line, tail)));
|
|
1280
1443
|
}
|
|
@@ -1299,12 +1462,17 @@ export class Session {
|
|
|
1299
1462
|
this.handFailureLine = null;
|
|
1300
1463
|
const versionId = this.recordConfirmed(conn, code, origin, intent);
|
|
1301
1464
|
this.rememberVersion(id, versionId);
|
|
1465
|
+
if (intent?.answering !== undefined) {
|
|
1466
|
+
this.requests.applied(intent.answering, versionId);
|
|
1467
|
+
}
|
|
1302
1468
|
return { replacedHandEdit, versionId };
|
|
1303
1469
|
}
|
|
1304
1470
|
/**
|
|
1305
1471
|
* The page says the waiting change is audible now. Accepted only from the active page and only for
|
|
1306
1472
|
* the id of the change that waits; anything else is one stderr line and changes nothing. The message carries
|
|
1307
|
-
*
|
|
1473
|
+
* the id and, when the ramp the waiting change started at its line still runs, `glideRuns: true`; the recorded code
|
|
1474
|
+
* is the one stored from the pending reply, and the ramp record is set from `glideRuns` (or cleared without it) for a
|
|
1475
|
+
* change that asked for a ramp.
|
|
1308
1476
|
*/
|
|
1309
1477
|
handleTakeover(conn, msg) {
|
|
1310
1478
|
if (conn !== this.activePage) {
|
|
@@ -1318,6 +1486,11 @@ export class Session {
|
|
|
1318
1486
|
}
|
|
1319
1487
|
this.pendingChange = null;
|
|
1320
1488
|
const { replacedHandEdit } = this.confirmSuccess(conn, waiting.id, waiting.code, 'playing', waiting.intent);
|
|
1489
|
+
// A change that asked for a ramp at its line ended any older ramp there. Its own ramp is the recorded ramp from here on only
|
|
1490
|
+
// when the page says it still runs; otherwise none runs (nothing to ramp between, or it already ended) and the record is
|
|
1491
|
+
// cleared with no line. A change replaced or canceled before its line never gets here.
|
|
1492
|
+
if (waiting.glide !== undefined)
|
|
1493
|
+
this.glide = msg.glideRuns === true ? { toBpm: waiting.glide.toBpm } : null;
|
|
1321
1494
|
// An edit that carried a hand edit replaced nothing of it: the Mixer and Parts refuse while a change
|
|
1322
1495
|
// waits and a hand evaluation cancels the wait, so the code playing at this takeover is still the hand-edited
|
|
1323
1496
|
// text the edit was built on, and the note would be false.
|
|
@@ -1342,6 +1515,17 @@ export class Session {
|
|
|
1342
1515
|
}
|
|
1343
1516
|
this.dropWaitingChange(msg.reason);
|
|
1344
1517
|
}
|
|
1518
|
+
/**
|
|
1519
|
+
* The page says its tempo ramp ended on its own. Only the active page is trusted; the numbers are bounded by the
|
|
1520
|
+
* protocol schema and the wording is the server's.
|
|
1521
|
+
*/
|
|
1522
|
+
handleGlideEnded(conn, msg) {
|
|
1523
|
+
if (conn !== this.activePage) {
|
|
1524
|
+
console.error('[moontide] Ignoring glide-ended from a non-active page');
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
this.dropGlide(msg.reason, msg.bpm === undefined ? undefined : Math.round(msg.bpm * 10) / 10);
|
|
1528
|
+
}
|
|
1345
1529
|
/**
|
|
1346
1530
|
* The editedByHand flag after a confirmed evaluation: Claude's code makes the stored code
|
|
1347
1531
|
* Claude's again (false). A change made from the page (origin 'page') is the user's:
|
|
@@ -1509,6 +1693,7 @@ export class Session {
|
|
|
1509
1693
|
if (conn === this.activePage) {
|
|
1510
1694
|
// A late stop confirmation still reflects reality — the page did stop.
|
|
1511
1695
|
this.stopped = true;
|
|
1696
|
+
this.dropGlide('stop');
|
|
1512
1697
|
this.dropWaitingChange('stop');
|
|
1513
1698
|
console.error(`[moontide] Late stop confirmation stored for request ${msg.id}`);
|
|
1514
1699
|
}
|
|
@@ -1526,6 +1711,7 @@ export class Session {
|
|
|
1526
1711
|
this.pending.delete(msg.id);
|
|
1527
1712
|
if (conn === this.activePage) {
|
|
1528
1713
|
this.stopped = true;
|
|
1714
|
+
this.dropGlide('stop');
|
|
1529
1715
|
this.dropWaitingChange('stop');
|
|
1530
1716
|
entry.resolve(undefined);
|
|
1531
1717
|
}
|
|
@@ -1880,6 +2066,9 @@ export class Session {
|
|
|
1880
2066
|
// A change that waited for a line and did not take over, told once.
|
|
1881
2067
|
lines.push(...this.takeoverNotes);
|
|
1882
2068
|
this.takeoverNotes.length = 0;
|
|
2069
|
+
// A tempo ramp that ended early, told once.
|
|
2070
|
+
lines.push(...this.glideNotes);
|
|
2071
|
+
this.glideNotes.length = 0;
|
|
1883
2072
|
// The build line comes before the setup file lines, then the MIDI lines, then the ask
|
|
1884
2073
|
// lines (the arming line stays last). Runtime error lines and the history notes come earlier.
|
|
1885
2074
|
lines.push(...this.buildNotes);
|
|
@@ -1949,6 +2138,8 @@ export class Session {
|
|
|
1949
2138
|
force: false,
|
|
1950
2139
|
at: options?.at,
|
|
1951
2140
|
phraseCycles: options?.phraseCycles,
|
|
2141
|
+
glideBars: options?.glideBars,
|
|
2142
|
+
answering: options?.answering,
|
|
1952
2143
|
});
|
|
1953
2144
|
}
|
|
1954
2145
|
/**
|
|
@@ -2007,6 +2198,8 @@ export class Session {
|
|
|
2007
2198
|
force: false,
|
|
2008
2199
|
at: options?.at,
|
|
2009
2200
|
phraseCycles: options?.phraseCycles,
|
|
2201
|
+
glideBars: options?.glideBars,
|
|
2202
|
+
answering: options?.answering,
|
|
2010
2203
|
carriesHandEdit,
|
|
2011
2204
|
}).then((outcome) => ({ outcome, code: result.code, ranges: result.ranges, base, carriesHandEdit }));
|
|
2012
2205
|
}
|
|
@@ -2054,6 +2247,7 @@ export class Session {
|
|
|
2054
2247
|
label: restoredLabel(id),
|
|
2055
2248
|
restoredFrom: id,
|
|
2056
2249
|
origin: options?.origin,
|
|
2250
|
+
answering: options?.answering,
|
|
2057
2251
|
force: true,
|
|
2058
2252
|
});
|
|
2059
2253
|
}
|
|
@@ -2073,6 +2267,8 @@ export class Session {
|
|
|
2073
2267
|
message.at = intent.at;
|
|
2074
2268
|
if (intent.phraseCycles !== undefined)
|
|
2075
2269
|
message.phraseCycles = intent.phraseCycles;
|
|
2270
|
+
if (intent.glideBars !== undefined)
|
|
2271
|
+
message.glideBars = intent.glideBars;
|
|
2076
2272
|
return message;
|
|
2077
2273
|
});
|
|
2078
2274
|
}
|