@hraness/dawg 0.6.0 → 0.7.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/CHANGELOG.md +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -12
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/src/main.ts
CHANGED
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { TOPIC_ALIASES } from "./lang/glossary.ts";
|
|
3
|
+
import { isGuideInstrument, vocalChainPatch } from "../core/clips.ts";
|
|
4
|
+
import { isSingWord } from "../core/sing.ts";
|
|
5
|
+
import { commandParses, parseExact } from "./commands/parses.ts";
|
|
6
|
+
import { paramRangeError } from "./commands/param-range.ts";
|
|
7
|
+
import { noteName as midiNoteName } from "./media/notes.ts";
|
|
2
8
|
import {
|
|
9
|
+
EVERYDAY_VERBS,
|
|
10
|
+
EXPORT_USAGE,
|
|
11
|
+
elsewhereHint,
|
|
12
|
+
FREE_TEXT_HINTS,
|
|
13
|
+
noTrack,
|
|
14
|
+
stemsReceipt,
|
|
15
|
+
LOOP_USAGE,
|
|
16
|
+
WINDOW_VERBS,
|
|
17
|
+
NO_AGENT,
|
|
18
|
+
RANGES,
|
|
19
|
+
canonicalWindowForm,
|
|
20
|
+
friendlyCoreError,
|
|
21
|
+
knownVerbs,
|
|
22
|
+
nearest,
|
|
23
|
+
noNote,
|
|
24
|
+
parseExportCommand,
|
|
25
|
+
parseLoopCommand,
|
|
26
|
+
recover,
|
|
27
|
+
usageCard,
|
|
28
|
+
usageError,
|
|
29
|
+
verbOf,
|
|
30
|
+
} from "./commands/grammar.ts";
|
|
31
|
+
import {
|
|
32
|
+
instrumentListLines,
|
|
3
33
|
isUnknownInstrument,
|
|
4
34
|
plainSineAdvice,
|
|
35
|
+
unknownInstrumentMessage,
|
|
5
36
|
} from "./audio/instrument-check.ts";
|
|
6
37
|
import { randomUUID } from "node:crypto";
|
|
7
38
|
import { readFileSync, writeSync } from "node:fs";
|
|
8
|
-
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
39
|
+
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
40
|
+
import { tmpdir } from "node:os";
|
|
9
41
|
import { join, relative, resolve } from "node:path";
|
|
10
42
|
import { stdin, stdout } from "node:process";
|
|
11
43
|
import {
|
|
@@ -14,6 +46,12 @@ import {
|
|
|
14
46
|
type SessionRecord,
|
|
15
47
|
} from "./session/store.ts";
|
|
16
48
|
import { openSessionPort } from "./session/port.ts";
|
|
49
|
+
import {
|
|
50
|
+
OwnWrites,
|
|
51
|
+
editedTrack,
|
|
52
|
+
foreignEvents,
|
|
53
|
+
otherWindowName,
|
|
54
|
+
} from "./session/origin.ts";
|
|
17
55
|
import { compositionDigest, monotonicEpochMs } from "./session/protocol.ts";
|
|
18
56
|
import {
|
|
19
57
|
formatSessionLine,
|
|
@@ -69,11 +107,31 @@ import {
|
|
|
69
107
|
rigTrackFields,
|
|
70
108
|
rigWordPatch,
|
|
71
109
|
} from "./commands/rig.ts";
|
|
110
|
+
import {
|
|
111
|
+
applyProgressionCommand,
|
|
112
|
+
parseProgressionCommand,
|
|
113
|
+
} from "./commands/progression.ts";
|
|
114
|
+
import {
|
|
115
|
+
applyGuitarCommand,
|
|
116
|
+
applyStrumCommand,
|
|
117
|
+
parseGuitarCommand,
|
|
118
|
+
parseStrumCommand,
|
|
119
|
+
} from "./commands/strum.ts";
|
|
72
120
|
import {
|
|
73
121
|
applyModalCommand,
|
|
74
122
|
modalListLines,
|
|
75
123
|
parseModalCommand,
|
|
76
124
|
} from "./commands/modal.ts";
|
|
125
|
+
import {
|
|
126
|
+
applyWindCommand,
|
|
127
|
+
parseWindCommand,
|
|
128
|
+
windListLines,
|
|
129
|
+
} from "./commands/wind.ts";
|
|
130
|
+
import {
|
|
131
|
+
applySingCommand,
|
|
132
|
+
parseSingCommand,
|
|
133
|
+
singListLines,
|
|
134
|
+
} from "./commands/sing.ts";
|
|
77
135
|
import { applySynthCommand, parseSynthCommand } from "./commands/synth.ts";
|
|
78
136
|
import { applyStringCommand, parseStringCommand } from "./commands/string.ts";
|
|
79
137
|
import { instrumentPatchForWord, isModalWord } from "../core/resonators.ts";
|
|
@@ -94,26 +152,38 @@ import {
|
|
|
94
152
|
parseTuningCommand,
|
|
95
153
|
type TuningCommand,
|
|
96
154
|
} from "./commands/tuning.ts";
|
|
155
|
+
import {
|
|
156
|
+
applyCalibrationCommand,
|
|
157
|
+
parseCalibrationCommand,
|
|
158
|
+
} from "./commands/calibration.ts";
|
|
97
159
|
import { TuningError, displayTag, resolveTuning } from "../core/tuning.ts";
|
|
98
160
|
import {
|
|
99
161
|
applyMasterCommand,
|
|
100
162
|
measurementLine,
|
|
101
163
|
parseMasterCommand,
|
|
102
164
|
} from "./commands/master.ts";
|
|
165
|
+
import { applyStyleCommand, parseStyleCommand } from "./commands/style.ts";
|
|
103
166
|
import { exportSampleRate, measureScoreOffThread } from "./audio/measure.ts";
|
|
104
167
|
import {
|
|
105
168
|
applySectionCommand,
|
|
169
|
+
loopSpan,
|
|
106
170
|
parseSectionCommand,
|
|
107
171
|
} from "./commands/arrange.ts";
|
|
108
172
|
import {
|
|
109
|
-
|
|
173
|
+
helpMiss,
|
|
110
174
|
helpText,
|
|
175
|
+
helpTitle,
|
|
111
176
|
helpTopicLines,
|
|
112
177
|
nearestCommand,
|
|
113
178
|
typoFix,
|
|
114
179
|
usageHint,
|
|
115
180
|
} from "./commands/help.ts";
|
|
116
|
-
import {
|
|
181
|
+
import {
|
|
182
|
+
historyReceipt,
|
|
183
|
+
historyTarget,
|
|
184
|
+
REDO_KIND,
|
|
185
|
+
UNDO_KIND,
|
|
186
|
+
} from "./commands/history.ts";
|
|
117
187
|
|
|
118
188
|
import {
|
|
119
189
|
SamplePlacementError,
|
|
@@ -127,6 +197,32 @@ import {
|
|
|
127
197
|
voiceNameFrom,
|
|
128
198
|
} from "./commands/sample.ts";
|
|
129
199
|
import { applyFitCommand, fitVoice, parseFitCommand } from "./commands/fit.ts";
|
|
200
|
+
import { applyShiftCommand, parseShiftCommand } from "./commands/shift.ts";
|
|
201
|
+
import { parseVocalCommand, runVocalCommand } from "./commands/vocal.ts";
|
|
202
|
+
import {
|
|
203
|
+
applyFormantCommand,
|
|
204
|
+
parseFormantCommand,
|
|
205
|
+
parseVowelCommand,
|
|
206
|
+
} from "./commands/formant.ts";
|
|
207
|
+
import {
|
|
208
|
+
applyLyrics,
|
|
209
|
+
parseClipCommand,
|
|
210
|
+
parseLyricsCommand,
|
|
211
|
+
runClipCommand,
|
|
212
|
+
setClipImportDeps,
|
|
213
|
+
} from "./commands/clips.ts";
|
|
214
|
+
import { pitchTraceFor } from "./commands/vocal-pitch.ts";
|
|
215
|
+
import {
|
|
216
|
+
applyVocoderCommand,
|
|
217
|
+
parseVocoderCommand,
|
|
218
|
+
vocoderListLines,
|
|
219
|
+
} from "./commands/vocoder.ts";
|
|
220
|
+
import {
|
|
221
|
+
applyAutotuneCommand,
|
|
222
|
+
autotuneListLines,
|
|
223
|
+
parseAutotuneCommand,
|
|
224
|
+
} from "./commands/autotune.ts";
|
|
225
|
+
import { parseResampleCommand, runResample } from "./commands/resample.ts";
|
|
130
226
|
import { suggestFitMode } from "./audio/dsp/onset.ts";
|
|
131
227
|
import {
|
|
132
228
|
SampleLibrary,
|
|
@@ -135,7 +231,14 @@ import {
|
|
|
135
231
|
type SampleBank,
|
|
136
232
|
type SampleProblem,
|
|
137
233
|
} from "./audio/samples.ts";
|
|
234
|
+
import {
|
|
235
|
+
ANALYSIS_CACHE_BYTES,
|
|
236
|
+
analysisCacheStatus,
|
|
237
|
+
analysisDir,
|
|
238
|
+
pruneAnalysisCache,
|
|
239
|
+
} from "./audio/analysis.ts";
|
|
138
240
|
import { drumSnapshotFields, samplerSnapshotFields } from "../tui/drums.ts";
|
|
241
|
+
import { clipSnapshots, loadClipPeaks } from "../tui/clip-row.ts";
|
|
139
242
|
import { highwayLayers } from "../tui/layers.ts";
|
|
140
243
|
import { drumVoicePitch, isDrumInstrument } from "../core/drums.ts";
|
|
141
244
|
import {
|
|
@@ -144,6 +247,7 @@ import {
|
|
|
144
247
|
type AgentEvent,
|
|
145
248
|
type AgentHost,
|
|
146
249
|
} from "./agent/agent.ts";
|
|
250
|
+
import { routeDuringTurn } from "./agent/steer.ts";
|
|
147
251
|
import {
|
|
148
252
|
isApiSelection,
|
|
149
253
|
providerFingerprint,
|
|
@@ -162,8 +266,24 @@ import {
|
|
|
162
266
|
spendLine as formatSpendLine,
|
|
163
267
|
webHostFor,
|
|
164
268
|
} from "./agent/usage.ts";
|
|
165
|
-
import { configDir } from "./auth/credentials.ts";
|
|
166
|
-
import {
|
|
269
|
+
import { configDir, readConfig, writeConfig } from "./auth/credentials.ts";
|
|
270
|
+
import type { CommandOutcome } from "./agent/command-agent.ts";
|
|
271
|
+
import {
|
|
272
|
+
GLIDE_STEP_MS,
|
|
273
|
+
NoteScheduler,
|
|
274
|
+
finishHint,
|
|
275
|
+
gestureFor,
|
|
276
|
+
glideValues,
|
|
277
|
+
agentPathsAllowed,
|
|
278
|
+
isAgentCommand,
|
|
279
|
+
isBrokenCommand,
|
|
280
|
+
brokenCommandReceipt,
|
|
281
|
+
parseShowMe,
|
|
282
|
+
toolCaption,
|
|
283
|
+
type ShowMeLevel,
|
|
284
|
+
} from "./agent/show-me.ts";
|
|
285
|
+
import { firstRunCard, runAuthCommand, runTuiLogin } from "./auth/cli.ts";
|
|
286
|
+
import { musicalReceipt } from "./session/receipt.ts";
|
|
167
287
|
import {
|
|
168
288
|
modelPickerItems,
|
|
169
289
|
tuiAuthCommand,
|
|
@@ -179,10 +299,16 @@ import {
|
|
|
179
299
|
PlaySession,
|
|
180
300
|
type LiveEngine,
|
|
181
301
|
} from "./tui/play-session.ts";
|
|
182
|
-
import {
|
|
302
|
+
import {
|
|
303
|
+
EditMenu,
|
|
304
|
+
menuUsage,
|
|
305
|
+
menuSectionPath,
|
|
306
|
+
type MenuContext,
|
|
307
|
+
} from "./tui/menu.ts";
|
|
183
308
|
import {
|
|
184
309
|
drawerView,
|
|
185
310
|
faderChoose,
|
|
311
|
+
faderCommand,
|
|
186
312
|
faderKeyPress,
|
|
187
313
|
faderSetPosition,
|
|
188
314
|
faderStep,
|
|
@@ -204,7 +330,15 @@ import {
|
|
|
204
330
|
isStageable,
|
|
205
331
|
} from "./tui/audition.ts";
|
|
206
332
|
import { EuclidEditor, type EuclidContext } from "./tui/euclid.ts";
|
|
207
|
-
import {
|
|
333
|
+
import {
|
|
334
|
+
ARROW_DOWN,
|
|
335
|
+
ARROW_UP,
|
|
336
|
+
closesKeys,
|
|
337
|
+
HINTS,
|
|
338
|
+
KEYS,
|
|
339
|
+
keyLines,
|
|
340
|
+
type KeySection,
|
|
341
|
+
} from "../tui/grammar.ts";
|
|
208
342
|
import { renderAudition } from "./audio/audition.ts";
|
|
209
343
|
import { exportScore, playbackTime, scoreBeatAt } from "./audio/arrange.ts";
|
|
210
344
|
import { formatForm } from "../core/sections.ts";
|
|
@@ -252,6 +386,7 @@ import {
|
|
|
252
386
|
applyScoreOperation,
|
|
253
387
|
createScore,
|
|
254
388
|
isSamplerInstrument,
|
|
389
|
+
isTrackAudible,
|
|
255
390
|
PACK_PREFIX,
|
|
256
391
|
scoreFromJSON,
|
|
257
392
|
SCORE_LIMITS,
|
|
@@ -264,8 +399,19 @@ import { decodeLoop, encodeLoop } from "../core/loop.ts";
|
|
|
264
399
|
import { scoreToMidi } from "../core/midi.ts";
|
|
265
400
|
import type { TrackScoreSnapshot } from "../tui/render.ts";
|
|
266
401
|
import { PromptModel } from "../tui/prompt.ts";
|
|
267
|
-
import { TerminalInputDecoder } from "../tui/input.ts";
|
|
268
402
|
import {
|
|
403
|
+
ESCAPE_FLUSH_MS,
|
|
404
|
+
INPUT_FLUSH,
|
|
405
|
+
PASTE_FLUSH_MS,
|
|
406
|
+
TerminalInputDecoder,
|
|
407
|
+
} from "../tui/input.ts";
|
|
408
|
+
import {
|
|
409
|
+
FrameGate,
|
|
410
|
+
IDLE_HEARTBEAT_MS,
|
|
411
|
+
MIN_FRAME_GAP_MS,
|
|
412
|
+
} from "../tui/frame-gate.ts";
|
|
413
|
+
import {
|
|
414
|
+
CARD_GLOW_MS,
|
|
269
415
|
composeFrame,
|
|
270
416
|
TuiApp,
|
|
271
417
|
type AppView,
|
|
@@ -273,13 +419,19 @@ import {
|
|
|
273
419
|
type SyncState,
|
|
274
420
|
type TypesIndicator,
|
|
275
421
|
} from "../tui/app.ts";
|
|
276
|
-
import { fail, ok, toneOf, warn, type Receipt } from "../tui/activity.ts";
|
|
422
|
+
import { fail, note, ok, toneOf, warn, type Receipt } from "../tui/activity.ts";
|
|
277
423
|
import { systemRunner } from "./auth/runner.ts";
|
|
278
424
|
import type { MediaServices } from "./media/types.ts";
|
|
279
425
|
import { encodeBuffer } from "../tui/screen.ts";
|
|
280
426
|
import { parseThemeName } from "../tui/theme.ts";
|
|
281
427
|
import { formatDiagnostic } from "../core/sdk/eval.ts";
|
|
282
428
|
import { isProject } from "./project/init.ts";
|
|
429
|
+
import {
|
|
430
|
+
parseLaunchArgs,
|
|
431
|
+
parseSimpleArgv,
|
|
432
|
+
resolveTrackArg,
|
|
433
|
+
} from "./launch-args.ts";
|
|
434
|
+
import { RENDER_USAGE, runRenderCommand } from "./render.ts";
|
|
283
435
|
import { typecheckProject } from "./project/typecheck.ts";
|
|
284
436
|
import {
|
|
285
437
|
startProjectSync,
|
|
@@ -287,107 +439,100 @@ import {
|
|
|
287
439
|
type SyncHost,
|
|
288
440
|
} from "./project/sync.ts";
|
|
289
441
|
|
|
442
|
+
/** Set while `submit` runs a grammar reading, so it reads only once. */
|
|
443
|
+
let recovering = false;
|
|
444
|
+
|
|
445
|
+
/** Slash words `submit` handles itself (no parser in commandParses). */
|
|
446
|
+
const SLASH_HANDLED: ReadonlySet<string> = new Set([
|
|
447
|
+
...WINDOW_VERBS,
|
|
448
|
+
"chords",
|
|
449
|
+
"click",
|
|
450
|
+
"help",
|
|
451
|
+
]);
|
|
452
|
+
|
|
290
453
|
/** Whether a bare command parses (no side effects): for typo suggestions. */
|
|
291
454
|
function parsesLocally(text: string): boolean {
|
|
292
|
-
return
|
|
293
|
-
parsePrompt,
|
|
294
|
-
parseMusicCommand,
|
|
295
|
-
parseEditCommand,
|
|
296
|
-
parseRhythmCommand,
|
|
297
|
-
parseFxCommand,
|
|
298
|
-
parseSynthCommand,
|
|
299
|
-
parseStringCommand,
|
|
300
|
-
parseGranularCommand,
|
|
301
|
-
parseKeysCommand,
|
|
302
|
-
parseExpressionCommand,
|
|
303
|
-
parseMasterCommand,
|
|
304
|
-
(value: string) => parseSectionCommand(value, score),
|
|
305
|
-
parsePatternCommand,
|
|
306
|
-
parseKitCommand,
|
|
307
|
-
parsePackCommand,
|
|
308
|
-
parseSampleCommand,
|
|
309
|
-
parseFitCommand,
|
|
310
|
-
parseWavetableCommand,
|
|
311
|
-
parseTimeCommand,
|
|
312
|
-
parseTuningCommand,
|
|
313
|
-
parseRigCommand,
|
|
314
|
-
parseModalCommand,
|
|
315
|
-
].some((parse) => parse(text) !== undefined);
|
|
455
|
+
return commandParses(text, score);
|
|
316
456
|
}
|
|
317
457
|
|
|
318
458
|
const ESC = "\u001b[";
|
|
319
459
|
/** `/sessions` rows shown in the overlay. */
|
|
320
460
|
const MAX_LISTED_SESSIONS = 64;
|
|
321
461
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
"
|
|
326
|
-
"
|
|
327
|
-
"
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
"--help",
|
|
337
|
-
"-h",
|
|
338
|
-
"--version",
|
|
339
|
-
"-v",
|
|
340
|
-
"--reduce-motion",
|
|
341
|
-
"--no-mouse",
|
|
342
|
-
];
|
|
462
|
+
/** Wraps a usage line at spaces before `width`, indenting continuations. */
|
|
463
|
+
function wrapUsage(text: string, width: number, indent: string): string {
|
|
464
|
+
const lines: string[] = [];
|
|
465
|
+
let line = "";
|
|
466
|
+
for (const word of text.split(" ")) {
|
|
467
|
+
const prefix = lines.length === 0 ? " " : indent;
|
|
468
|
+
if (line && prefix.length + line.length + 1 + word.length > width) {
|
|
469
|
+
lines.push(line);
|
|
470
|
+
line = word;
|
|
471
|
+
} else line = line ? `${line} ${word}` : word;
|
|
472
|
+
}
|
|
473
|
+
lines.push(line);
|
|
474
|
+
return lines.join(`\n${indent}`);
|
|
475
|
+
}
|
|
343
476
|
const HELP_TEXT = `dawg · local-first terminal music workstation
|
|
344
477
|
|
|
345
478
|
Usage:
|
|
346
479
|
dawg [--new] [--session <name|id>] [--track <name>]
|
|
347
|
-
dawg --import <file> --export <file>
|
|
480
|
+
dawg --import <file> --export <file> convert a loop file (no session)
|
|
348
481
|
dawg sessions
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
dawg init [dir] project files: song.ts, tracks/<slug>/track.ts, .dawg/sdk
|
|
482
|
+
${wrapUsage(RENDER_USAGE.replace(/^usage: /, ""), 76, " ")}
|
|
483
|
+
dawg init [dir] song.ts, tracks/<slug>/track.ts and .dawg/sdk
|
|
352
484
|
dawg check typecheck + evaluate the project; exit 1 on problems
|
|
353
|
-
dawg
|
|
485
|
+
dawg <command> --help usage: sessions render init check media model
|
|
486
|
+
dawg doctor audio backend, native sink and devices
|
|
487
|
+
dawg media doctor|download|stems|analyze|notes|sample|lyrics (--help)
|
|
354
488
|
dawg --version
|
|
355
489
|
|
|
356
|
-
|
|
490
|
+
Display options:
|
|
357
491
|
--reduce-motion static hit/sustain states (also DAWG_REDUCE_MOTION=1)
|
|
358
492
|
--theme <name> default | high-contrast | mono (NO_COLOR forces mono)
|
|
359
493
|
--no-mouse keys only; no click/wheel reporting (also DAWG_MOUSE=0)
|
|
360
494
|
|
|
361
495
|
Prompt:
|
|
362
|
-
Enter submit · Shift-Enter newline · Alt-Enter
|
|
363
|
-
Ctrl-Z undo · Ctrl-Y redo · Ctrl-O transcript · Esc
|
|
496
|
+
Enter submit · Shift-Enter newline · Alt-Enter next · Ctrl-Q now / next
|
|
497
|
+
Ctrl-Z undo · Ctrl-Y redo · Ctrl-O transcript · Esc close · Ctrl-C exit
|
|
364
498
|
Space on an empty prompt toggles playback
|
|
365
499
|
|
|
366
500
|
Commands (bare music words; app commands take a slash):
|
|
367
501
|
${helpText()}
|
|
368
502
|
|
|
369
|
-
|
|
370
|
-
dawg
|
|
371
|
-
dawg
|
|
372
|
-
dawg model [alias] pick a model, with the
|
|
503
|
+
Agent (optional; the choice is saved and reused until you log out):
|
|
504
|
+
dawg model key find existing setups and pick a provider
|
|
505
|
+
dawg model key gateway|openrouter|codex|claude
|
|
506
|
+
dawg model [alias] pick a model, with the cost per prompt
|
|
373
507
|
dawg logout [provider] · dawg auth status [--check]
|
|
374
508
|
Unrecognized requests go to the agent once a provider is configured
|
|
375
509
|
(DAWG_PROVIDER=gateway|openrouter|codex|claude|auto, DAWG_MODEL=<alias>;
|
|
376
510
|
DAWG_AI=0 disables the agent).`;
|
|
377
511
|
const args = new Set(process.argv.slice(2));
|
|
378
|
-
|
|
379
|
-
const
|
|
512
|
+
/** One parse for every launch flag; problems are reported below. */
|
|
513
|
+
const launch = parseLaunchArgs(process.argv.slice(2));
|
|
514
|
+
const launchArgs = launch.ok ? launch.args : undefined;
|
|
515
|
+
const requestedSession = launchArgs?.session;
|
|
516
|
+
/** `--track` normalized as `/track` does; matched to an existing track below. */
|
|
517
|
+
let explicitTrack = launchArgs?.track?.id;
|
|
380
518
|
/** The focused track; claimed at startup unless `--track` is given. */
|
|
381
519
|
let requestedTrack = explicitTrack ?? "main";
|
|
382
520
|
const initialInstrument = isDrumInstrument(requestedTrack) ? "kit" : "sine";
|
|
383
|
-
const importPath =
|
|
384
|
-
const exportPath =
|
|
521
|
+
const importPath = launchArgs?.importPath;
|
|
522
|
+
const exportPath = launchArgs?.exportPath;
|
|
523
|
+
// `dawg model key [provider]` is the canonical spelling of `dawg login`.
|
|
524
|
+
if (process.argv[2] === "model" && process.argv[3] === "key")
|
|
525
|
+
process.exit(await runAuthCommand(["login", ...process.argv.slice(4)]));
|
|
385
526
|
if (["login", "logout", "auth", "model"].includes(process.argv[2] ?? ""))
|
|
386
527
|
process.exit(await runAuthCommand(process.argv.slice(2)));
|
|
387
528
|
{
|
|
388
529
|
// An unknown DAWG_MODEL is an error, never a silent fallback.
|
|
389
530
|
const fromEnv = process.env.DAWG_MODEL?.trim();
|
|
390
|
-
if (
|
|
531
|
+
if (
|
|
532
|
+
fromEnv &&
|
|
533
|
+
!resolveModelChoice("gateway", fromEnv) &&
|
|
534
|
+
!resolveModelChoice("openrouter", fromEnv)
|
|
535
|
+
) {
|
|
391
536
|
process.stderr.write(
|
|
392
537
|
`dawg: unknown DAWG_MODEL "${fromEnv.slice(0, 40)}"; use one of ${MODEL_CATALOG.map((row) => row.alias).join(", ")} or a vendor/model ID\n`,
|
|
393
538
|
);
|
|
@@ -395,10 +540,14 @@ if (["login", "logout", "auth", "model"].includes(process.argv[2] ?? ""))
|
|
|
395
540
|
}
|
|
396
541
|
}
|
|
397
542
|
if (process.argv[2] === "sessions") {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
543
|
+
const usage =
|
|
544
|
+
"usage: dawg sessions · lists this workspace's sessions, newest first (* marks the current one)";
|
|
545
|
+
const parsed = parseSimpleArgv(process.argv.slice(3), 0);
|
|
546
|
+
if (parsed.kind === "error") {
|
|
547
|
+
process.stderr.write(`${parsed.problem} · ${usage}\n`);
|
|
548
|
+
process.exit(2);
|
|
549
|
+
}
|
|
550
|
+
if (parsed.kind === "help") stdout.write(`${usage}\n`);
|
|
402
551
|
else await printSessions(process.cwd(), stdout);
|
|
403
552
|
process.exit(0);
|
|
404
553
|
}
|
|
@@ -411,6 +560,10 @@ if (process.argv[2] === "init" || process.argv[2] === "check") {
|
|
|
411
560
|
await command(process.argv.slice(2), process.cwd(), stdout, process.stderr),
|
|
412
561
|
);
|
|
413
562
|
}
|
|
563
|
+
if (process.argv[2] === "doctor") {
|
|
564
|
+
const { runDoctorCommand } = await import("./audio/doctor.ts");
|
|
565
|
+
process.exit(await runDoctorCommand(process.argv.slice(3), stdout));
|
|
566
|
+
}
|
|
414
567
|
if (process.argv[2] === "media") {
|
|
415
568
|
const { runMediaCommand } = await import("./media/cli.ts");
|
|
416
569
|
process.exit(
|
|
@@ -443,18 +596,37 @@ if (args.has("--help") || args.has("-h")) {
|
|
|
443
596
|
}
|
|
444
597
|
// Argument mistakes are rejected here, before `.dawg/` could be created.
|
|
445
598
|
{
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
process.stderr.write(`${problem} · dawg --help\n`);
|
|
599
|
+
if (!launch.ok) {
|
|
600
|
+
process.stderr.write(`${launch.problem} · dawg --help\n`);
|
|
449
601
|
process.exit(2);
|
|
450
602
|
}
|
|
451
603
|
}
|
|
604
|
+
// `--import X --export Y` converts one loop file to another: no session.
|
|
605
|
+
if (importPath && exportPath) {
|
|
606
|
+
try {
|
|
607
|
+
const converted = decodeLoop(await readLoopFile(importPath));
|
|
608
|
+
await writeFile(resolve(exportPath), encodeLoop(converted), "utf8");
|
|
609
|
+
} catch (error) {
|
|
610
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
611
|
+
const midi = /\.midi?$/i.test(importPath)
|
|
612
|
+
? " · MIDI files are not imported; --import takes a .track.json loop"
|
|
613
|
+
: "";
|
|
614
|
+
process.stderr.write(
|
|
615
|
+
`dawg: cannot import ${importPath} · ${reason.split("\n")[0]!.slice(0, 160)}${midi}\n`,
|
|
616
|
+
);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
stdout.write(`converted ${importPath} → ${exportPath}\n`);
|
|
620
|
+
process.exit(0);
|
|
621
|
+
}
|
|
452
622
|
const demo =
|
|
453
623
|
args.has("--demo") || process.env.DAWG_DEMO === "1" || !stdin.isTTY;
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
624
|
+
// Music first: the TUI opens directly. The first session with no provider
|
|
625
|
+
// (or a saved one that stopped working) gets one card instead of a picker.
|
|
626
|
+
const launchCard =
|
|
627
|
+
!demo && process.env.DAWG_AI !== "0" && stdout.isTTY
|
|
628
|
+
? firstRunCard().catch(() => undefined)
|
|
629
|
+
: undefined;
|
|
458
630
|
|
|
459
631
|
const initial = createScore({
|
|
460
632
|
tracks: [
|
|
@@ -487,7 +659,32 @@ const freshWorkspace = !(await stat(join(process.cwd(), ".dawg")).then(
|
|
|
487
659
|
() => true,
|
|
488
660
|
() => false,
|
|
489
661
|
));
|
|
490
|
-
|
|
662
|
+
// Decode the import before a session exists, so a bad file is one line.
|
|
663
|
+
let importedScore: TrackScore | undefined;
|
|
664
|
+
if (importPath) {
|
|
665
|
+
try {
|
|
666
|
+
importedScore = decodeLoop(await readLoopFile(importPath));
|
|
667
|
+
} catch (error) {
|
|
668
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
669
|
+
const midi = /\.midi?$/i.test(importPath)
|
|
670
|
+
? " · MIDI files are not imported; --import takes a .track.json loop"
|
|
671
|
+
: "";
|
|
672
|
+
process.stderr.write(
|
|
673
|
+
`dawg: cannot import ${importPath} · ${reason.split("\n")[0]!.slice(0, 160)}${midi}\n`,
|
|
674
|
+
);
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// A demo frame only renders: with no workspace yet (and nothing imported),
|
|
679
|
+
// it runs on a throwaway session so the cwd gets no `.dawg/`.
|
|
680
|
+
const ephemeralWorkspace =
|
|
681
|
+
demo && freshWorkspace && !importPath && selectedSession === undefined
|
|
682
|
+
? await mkdtemp(join(tmpdir(), "dawg-demo-"))
|
|
683
|
+
: undefined;
|
|
684
|
+
const session = await ensureSession(initial.toJSON(), {
|
|
685
|
+
...sessionOptions,
|
|
686
|
+
...(ephemeralWorkspace ? { workspace: ephemeralWorkspace } : {}),
|
|
687
|
+
});
|
|
491
688
|
// dawgd when connected, the file-lock path otherwise (see src/session/port.ts).
|
|
492
689
|
let port = await openSessionPort<ReturnType<TrackScore["toJSON"]>>({
|
|
493
690
|
paths: session.paths,
|
|
@@ -499,9 +696,14 @@ let port = await openSessionPort<ReturnType<TrackScore["toJSON"]>>({
|
|
|
499
696
|
let record: SessionRecord<ReturnType<TrackScore["toJSON"]>> =
|
|
500
697
|
port.mode === "daemon" ? await port.load() : session.record;
|
|
501
698
|
let score = scoreFromJSON(record.composition);
|
|
502
|
-
if (importPath)
|
|
503
|
-
|
|
504
|
-
|
|
699
|
+
if (importPath && importedScore) score = importedScore;
|
|
700
|
+
// `--track Bass` focuses an existing `bass` (or a track named "Bass").
|
|
701
|
+
if (launchArgs?.track) {
|
|
702
|
+
explicitTrack = resolveTrackArg(score.tracks, launchArgs.track);
|
|
703
|
+
requestedTrack = explicitTrack;
|
|
704
|
+
}
|
|
705
|
+
if (importPath && importedScore) {
|
|
706
|
+
score = importedScore;
|
|
505
707
|
record = await port.append(
|
|
506
708
|
record,
|
|
507
709
|
{
|
|
@@ -540,7 +742,7 @@ const tui = new TuiApp({
|
|
|
540
742
|
rows: () => stdout.rows ?? 24,
|
|
541
743
|
},
|
|
542
744
|
prompt,
|
|
543
|
-
theme:
|
|
745
|
+
theme: launchArgs?.theme ?? parseThemeName(process.env.DAWG_THEME),
|
|
544
746
|
reducedMotion:
|
|
545
747
|
args.has("--reduce-motion") || process.env.DAWG_REDUCE_MOTION === "1",
|
|
546
748
|
});
|
|
@@ -597,8 +799,25 @@ let requestFrame: () => void = () => undefined;
|
|
|
597
799
|
let runPromptLater: (command: string) => void = () => undefined;
|
|
598
800
|
/** The fader drawer's focus and typing, while one is open over the menu. */
|
|
599
801
|
let fader: FaderState | undefined;
|
|
600
|
-
|
|
601
|
-
const
|
|
802
|
+
/** Show-me state (see the show-me section below). */
|
|
803
|
+
const showMe: {
|
|
804
|
+
level: ShowMeLevel;
|
|
805
|
+
ghost?: string | undefined;
|
|
806
|
+
caption?: string | undefined;
|
|
807
|
+
commands: string[];
|
|
808
|
+
notes: NoteScheduler;
|
|
809
|
+
clear?: ReturnType<typeof setTimeout> | undefined;
|
|
810
|
+
} = {
|
|
811
|
+
level: parseShowMe(process.env.DAWG_SHOWME ?? "") ?? "on",
|
|
812
|
+
commands: [],
|
|
813
|
+
notes: new NoteScheduler(() => score.tempoBpm),
|
|
814
|
+
};
|
|
815
|
+
if (!process.env.DAWG_SHOWME)
|
|
816
|
+
void readConfig({ dir: configDir() })
|
|
817
|
+
.then((config) => {
|
|
818
|
+
if (config.showMe) showMe.level = config.showMe;
|
|
819
|
+
})
|
|
820
|
+
.catch(() => undefined);
|
|
602
821
|
/** The fader bar a left-button drag started on. */
|
|
603
822
|
let dragging: { field: number; left: number; width: number } | undefined;
|
|
604
823
|
/** The open drawer came from a command, not the menu. */
|
|
@@ -631,7 +850,8 @@ let handoff: <T>(flow: () => Promise<T>) => Promise<T> = (flow) => flow();
|
|
|
631
850
|
function spendLine(): string {
|
|
632
851
|
const width = stdout.columns ?? 80;
|
|
633
852
|
if (!providerName || providerName === "offline")
|
|
634
|
-
|
|
853
|
+
// A state, not a nag: the first session's card already named /login.
|
|
854
|
+
return width >= 40 ? formatSpendLine({ kind: "offline" }) : "";
|
|
635
855
|
const [model, provider] = providerName.split(" · ");
|
|
636
856
|
return formatSpendLine(
|
|
637
857
|
{
|
|
@@ -656,6 +876,10 @@ if (demo) {
|
|
|
656
876
|
"add C4 at 0 for 1",
|
|
657
877
|
0,
|
|
658
878
|
);
|
|
879
|
+
if (ephemeralWorkspace) {
|
|
880
|
+
await port.close().catch(() => undefined);
|
|
881
|
+
await rm(ephemeralWorkspace, { recursive: true, force: true });
|
|
882
|
+
}
|
|
659
883
|
process.exit(0);
|
|
660
884
|
}
|
|
661
885
|
|
|
@@ -666,36 +890,8 @@ if (exportPath) {
|
|
|
666
890
|
|
|
667
891
|
await runInteractive();
|
|
668
892
|
|
|
669
|
-
/** Describes an unknown subcommand or option in `argv`, or undefined. */
|
|
670
|
-
function argumentProblem(argv: readonly string[]): string | undefined {
|
|
671
|
-
if (argv[0] && !argv[0].startsWith("-") && !SUBCOMMANDS.includes(argv[0]))
|
|
672
|
-
return `unknown command · ${argv[0]}`;
|
|
673
|
-
if (argv[0] && SUBCOMMANDS.includes(argv[0])) return undefined;
|
|
674
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
675
|
-
const arg = argv[index]!;
|
|
676
|
-
if (VALUE_FLAGS.includes(arg)) {
|
|
677
|
-
index += 1;
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
if (arg.startsWith("-") && !FLAGS.includes(arg))
|
|
681
|
-
return `unknown option · ${arg}`;
|
|
682
|
-
if (!arg.startsWith("-")) return `unknown command · ${arg}`;
|
|
683
|
-
}
|
|
684
|
-
return undefined;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
893
|
async function packageVersion(): Promise<string> {
|
|
688
|
-
|
|
689
|
-
new URL("../package.json", import.meta.url),
|
|
690
|
-
"utf8",
|
|
691
|
-
);
|
|
692
|
-
return (JSON.parse(raw) as { version?: string }).version ?? "0.0.0";
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function optionValue(name: string): string | undefined {
|
|
696
|
-
const index = process.argv.indexOf(name);
|
|
697
|
-
const value = index >= 0 ? process.argv[index + 1] : undefined;
|
|
698
|
-
return value && !value.startsWith("--") ? value : undefined;
|
|
894
|
+
return (await import("./version.ts")).VERSION;
|
|
699
895
|
}
|
|
700
896
|
|
|
701
897
|
function seedDemo(value: TrackScore, trackId: string): TrackScore {
|
|
@@ -785,16 +981,24 @@ function snapshot(
|
|
|
785
981
|
muted: focused?.muted,
|
|
786
982
|
...(tag?.cents !== undefined ? { cents: tag.cents } : {}),
|
|
787
983
|
...(tag?.name !== undefined ? { centsFrom: tag.name } : {}),
|
|
984
|
+
...(note.lyric !== undefined ? { lyric: note.lyric } : {}),
|
|
788
985
|
};
|
|
789
986
|
});
|
|
987
|
+
// 0.7 clips: the clip row; peaks load off the frame path, then redraw.
|
|
988
|
+
const clips = clipSnapshots(value, requestedTrack);
|
|
989
|
+
if (clips)
|
|
990
|
+
void loadClipPeaks(process.cwd(), focused).then((changed) => {
|
|
991
|
+
if (changed) requestFrame();
|
|
992
|
+
});
|
|
790
993
|
return {
|
|
791
994
|
notes,
|
|
995
|
+
...(clips ? { clips } : {}),
|
|
792
996
|
trackName:
|
|
793
997
|
value.tracks.find((track) => track.id === requestedTrack)?.name ??
|
|
794
998
|
requestedTrack,
|
|
795
999
|
trackId: requestedTrack,
|
|
796
1000
|
sessionId: record.sessionId,
|
|
797
|
-
revision: record
|
|
1001
|
+
revision: shownRevision(record),
|
|
798
1002
|
bpm: value.time?.tempo
|
|
799
1003
|
? bpmAtTick(value, beat * value.ticksPerBeat)
|
|
800
1004
|
: value.tempoBpm,
|
|
@@ -813,6 +1017,7 @@ function snapshot(
|
|
|
813
1017
|
currentBeat: scoreBeatAt(value, beat),
|
|
814
1018
|
playing: clock.playing,
|
|
815
1019
|
activity,
|
|
1020
|
+
...(focused?.instrument === "vocal" ? { vocal: true } : {}),
|
|
816
1021
|
...drumSnapshotFields(
|
|
817
1022
|
value.tracks.find((track) => track.id === requestedTrack)?.instrument,
|
|
818
1023
|
notes,
|
|
@@ -824,6 +1029,7 @@ function snapshot(
|
|
|
824
1029
|
...(table && table.linear && table.size !== 12
|
|
825
1030
|
? { tuningPeriod: { size: table.size, root: table.root } }
|
|
826
1031
|
: {}),
|
|
1032
|
+
pitchTrace: pitchTraceFor(requestedTrack, value),
|
|
827
1033
|
layers:
|
|
828
1034
|
tui.highwayView === "all"
|
|
829
1035
|
? highwayLayers(
|
|
@@ -865,6 +1071,10 @@ function appView(value: TrackScore, beat: number): AppView {
|
|
|
865
1071
|
providerName && providerName !== "offline" ? providerName : undefined,
|
|
866
1072
|
spend: spendLine(),
|
|
867
1073
|
agentOffline: providerName === "offline" || process.env.DAWG_AI === "0",
|
|
1074
|
+
showMe:
|
|
1075
|
+
showMe.ghost || showMe.caption
|
|
1076
|
+
? { ghost: showMe.ghost, caption: showMe.caption }
|
|
1077
|
+
: undefined,
|
|
868
1078
|
sync: syncState,
|
|
869
1079
|
sessionName: record.meta.name,
|
|
870
1080
|
windows: windowCount,
|
|
@@ -968,17 +1178,20 @@ function receipt(result: string | Receipt, base?: Baseline): void {
|
|
|
968
1178
|
tui.activity.pushError(message);
|
|
969
1179
|
return;
|
|
970
1180
|
}
|
|
1181
|
+
// A receipt that names its own undo key needs no second hint.
|
|
971
1182
|
const hint =
|
|
972
|
-
scoreChanged &&
|
|
1183
|
+
scoreChanged &&
|
|
1184
|
+
undoHintsShown < MAX_UNDO_HINTS &&
|
|
1185
|
+
!message.endsWith("ctrl-z undo")
|
|
973
1186
|
? message.startsWith("undid")
|
|
974
|
-
? "
|
|
975
|
-
: "
|
|
1187
|
+
? "ctrl-y redo"
|
|
1188
|
+
: "ctrl-z undo"
|
|
976
1189
|
: undefined;
|
|
977
1190
|
if (hint) undoHintsShown += 1;
|
|
978
1191
|
tui.activity.pushCard(message, {
|
|
979
1192
|
tone,
|
|
980
|
-
baseRevision: changed ? base.revision : undefined,
|
|
981
|
-
resultRevision: changed ? record
|
|
1193
|
+
baseRevision: changed ? shownRevision(record, base.revision) : undefined,
|
|
1194
|
+
resultRevision: changed ? shownRevision(record) : undefined,
|
|
982
1195
|
hint,
|
|
983
1196
|
trackId: requestedTrack,
|
|
984
1197
|
});
|
|
@@ -990,6 +1203,10 @@ function describeError(command: string, error: unknown): string {
|
|
|
990
1203
|
const message = error instanceof Error ? error.message : String(error);
|
|
991
1204
|
if (detail?.code === "ENOENT" && typeof detail.path === "string")
|
|
992
1205
|
return `no such file · ${relative(process.cwd(), detail.path)}`;
|
|
1206
|
+
// `tempo 900` → `tempo 900 · tempo takes 20…300 BPM · tempo 128`: no raw
|
|
1207
|
+
// core keys (`tempoBpm`) on a card.
|
|
1208
|
+
const friendly = friendlyCoreError(truncateForCard(command), message);
|
|
1209
|
+
if (friendly) return friendly;
|
|
993
1210
|
const verb = command.trim().split(/\s+/)[0]?.replace(/^\//, "") || "command";
|
|
994
1211
|
return `${verb} failed · ${message}`;
|
|
995
1212
|
}
|
|
@@ -1016,25 +1233,108 @@ function mouseOn(): string {
|
|
|
1016
1233
|
return mouseEnabled() ? MOUSE_ON : "";
|
|
1017
1234
|
}
|
|
1018
1235
|
|
|
1236
|
+
/**
|
|
1237
|
+
* Mouse off, plain colors, plain paste, cursor shown, main screen. A
|
|
1238
|
+
* function, not a const: `await runInteractive()` runs above this line.
|
|
1239
|
+
*/
|
|
1240
|
+
function terminalRestore(): string {
|
|
1241
|
+
return `${MOUSE_OFF}${ESC}0m${ESC}?2004l${ESC}?25h${ESC}?1049l`;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/** `dawg: <stack>` for a crash printed after leaving the alternate screen. */
|
|
1245
|
+
function crashText(error: unknown): string {
|
|
1246
|
+
const text =
|
|
1247
|
+
error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
1248
|
+
return `dawg: ${text}\n`;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1019
1251
|
async function runInteractive(): Promise<void> {
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1252
|
+
// Every way out (quit, a failing teardown step, a crash, a kill, a stray
|
|
1253
|
+
// process.exit) restores the whole terminal, once, synchronously: the
|
|
1254
|
+
// shell must not stay on the alternate screen with paste brackets on.
|
|
1255
|
+
let terminalRestored = false;
|
|
1256
|
+
const restoreTerminal = () => {
|
|
1257
|
+
if (terminalRestored) return;
|
|
1258
|
+
terminalRestored = true;
|
|
1259
|
+
try {
|
|
1260
|
+
stdin.setRawMode?.(false);
|
|
1261
|
+
} catch {
|
|
1262
|
+
// stdin may already be closed.
|
|
1263
|
+
}
|
|
1264
|
+
try {
|
|
1265
|
+
writeSync(1, terminalRestore());
|
|
1266
|
+
} catch {
|
|
1267
|
+
// The terminal may already be gone (SIGHUP).
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
process.once("exit", restoreTerminal);
|
|
1271
|
+
// A kill or a closed terminal: restore it, then exit as killed.
|
|
1026
1272
|
for (const [signal, code] of [
|
|
1027
1273
|
["SIGTERM", 143],
|
|
1028
1274
|
["SIGHUP", 129],
|
|
1029
1275
|
] as const)
|
|
1030
1276
|
process.once(signal, () => {
|
|
1277
|
+
restoreTerminal();
|
|
1278
|
+
process.exit(code);
|
|
1279
|
+
});
|
|
1280
|
+
// An exception nothing caught leaves the editor in an unknown state: leave
|
|
1281
|
+
// the alternate screen first, so the stack lands in the shell, then exit.
|
|
1282
|
+
const onUncaught = (error: unknown) => {
|
|
1283
|
+
restoreTerminal();
|
|
1284
|
+
try {
|
|
1285
|
+
writeSync(2, crashText(error));
|
|
1286
|
+
} catch {
|
|
1287
|
+
// Nowhere left to report it.
|
|
1288
|
+
}
|
|
1289
|
+
process.exit(1);
|
|
1290
|
+
};
|
|
1291
|
+
// A rejected fire-and-forget promise is one failed action, not a broken
|
|
1292
|
+
// editor: report it on the activity strip and repaint over anything the
|
|
1293
|
+
// runtime wrote.
|
|
1294
|
+
const onRejection = (error: unknown) => {
|
|
1295
|
+
if (terminalRestored || screenSuspended) {
|
|
1031
1296
|
try {
|
|
1032
|
-
writeSync(
|
|
1297
|
+
writeSync(2, crashText(error));
|
|
1033
1298
|
} catch {
|
|
1034
|
-
//
|
|
1299
|
+
// Nowhere left to report it.
|
|
1035
1300
|
}
|
|
1036
|
-
|
|
1037
|
-
}
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
tui.activity.pushError(
|
|
1304
|
+
`failed · ${error instanceof Error ? error.message : String(error)}`,
|
|
1305
|
+
);
|
|
1306
|
+
tui.invalidate();
|
|
1307
|
+
requestFrame();
|
|
1308
|
+
};
|
|
1309
|
+
// Runtime warnings and console output would land on top of the frame; the
|
|
1310
|
+
// writer never repaints rows it did not change. Route them to the
|
|
1311
|
+
// transcript instead, and repaint.
|
|
1312
|
+
const onWarning = (warning: Error) => {
|
|
1313
|
+
if (screenSuspended || terminalRestored) return;
|
|
1314
|
+
tui.activity.pushNote(`warning · ${warning.message}`, "error");
|
|
1315
|
+
tui.invalidate();
|
|
1316
|
+
requestFrame();
|
|
1317
|
+
};
|
|
1318
|
+
const consoleError = console.error;
|
|
1319
|
+
const consoleWarn = console.warn;
|
|
1320
|
+
const consoleToTranscript =
|
|
1321
|
+
(original: (...data: unknown[]) => void) =>
|
|
1322
|
+
(...data: unknown[]) => {
|
|
1323
|
+
if (screenSuspended || terminalRestored) return original(...data);
|
|
1324
|
+
tui.activity.pushNote(
|
|
1325
|
+
data
|
|
1326
|
+
.map((item) => (item instanceof Error ? item.message : String(item)))
|
|
1327
|
+
.join(" "),
|
|
1328
|
+
"error",
|
|
1329
|
+
);
|
|
1330
|
+
tui.invalidate();
|
|
1331
|
+
requestFrame();
|
|
1332
|
+
};
|
|
1333
|
+
process.on("uncaughtException", onUncaught);
|
|
1334
|
+
process.on("unhandledRejection", onRejection);
|
|
1335
|
+
process.on("warning", onWarning);
|
|
1336
|
+
console.error = consoleToTranscript(consoleError);
|
|
1337
|
+
console.warn = consoleToTranscript(consoleWarn);
|
|
1038
1338
|
stdin.setRawMode?.(true);
|
|
1039
1339
|
stdin.resume();
|
|
1040
1340
|
// Alternate screen, hidden cursor, bracketed paste.
|
|
@@ -1045,7 +1345,8 @@ async function runInteractive(): Promise<void> {
|
|
|
1045
1345
|
const runPrompt = async (text: string): Promise<void> => {
|
|
1046
1346
|
const ui = tui.command(text);
|
|
1047
1347
|
if (ui !== undefined) {
|
|
1048
|
-
tui.activity.pushCard(ui, { tone: "info" });
|
|
1348
|
+
if (typeof ui === "string") tui.activity.pushCard(ui, { tone: "info" });
|
|
1349
|
+
else tui.activity.pushCard(ui.text, { tone: toneOf(ui) });
|
|
1049
1350
|
return;
|
|
1050
1351
|
}
|
|
1051
1352
|
tui.activity.pushRequest(text);
|
|
@@ -1069,6 +1370,16 @@ async function runInteractive(): Promise<void> {
|
|
|
1069
1370
|
}
|
|
1070
1371
|
};
|
|
1071
1372
|
let agentReported = false;
|
|
1373
|
+
/** A typed command during an agent turn; leaves the turn's receipt alone. */
|
|
1374
|
+
const runLocalBesideAgent = async (text: string): Promise<void> => {
|
|
1375
|
+
tui.activity.pushRequest(text);
|
|
1376
|
+
const base = baseline();
|
|
1377
|
+
try {
|
|
1378
|
+
receipt(await submit(text), base);
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
tui.activity.pushError(describeError(text, error));
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1072
1383
|
const drainQueue = async (): Promise<void> => {
|
|
1073
1384
|
if (processingQueue) return;
|
|
1074
1385
|
processingQueue = true;
|
|
@@ -1089,17 +1400,64 @@ async function runInteractive(): Promise<void> {
|
|
|
1089
1400
|
tui.activity.setQueueDepth(queuedPrompts.length);
|
|
1090
1401
|
}
|
|
1091
1402
|
};
|
|
1403
|
+
// Builds a frame only when something can have changed (tui/frame-gate.ts):
|
|
1404
|
+
// an idle editor no longer rebuilds the whole view 30 times a second.
|
|
1405
|
+
const frameGate = new FrameGate(IDLE_HEARTBEAT_MS, MIN_FRAME_GAP_MS);
|
|
1406
|
+
const unwatchActivity = tui.activity.subscribe(() => frameGate.markDirty());
|
|
1407
|
+
const animating = (): boolean => {
|
|
1408
|
+
if (clock.playing || play?.on || auditionLoop?.looping) return true;
|
|
1409
|
+
const activity = tui.activity;
|
|
1410
|
+
if (activity.spinner || activity.streaming) return true;
|
|
1411
|
+
if (tui.delight.animating(Date.now())) return true;
|
|
1412
|
+
const latest = activity.latest;
|
|
1413
|
+
return (
|
|
1414
|
+
!tui.ui.reducedMotion &&
|
|
1415
|
+
latest !== undefined &&
|
|
1416
|
+
Date.now() - latest.atMs < CARD_GLOW_MS
|
|
1417
|
+
);
|
|
1418
|
+
};
|
|
1092
1419
|
const tick = (force = false) => {
|
|
1093
1420
|
if (screenSuspended) return;
|
|
1421
|
+
if (
|
|
1422
|
+
!frameGate.shouldBuild({
|
|
1423
|
+
nowMs: Date.now(),
|
|
1424
|
+
force,
|
|
1425
|
+
animating: animating(),
|
|
1426
|
+
keys: [
|
|
1427
|
+
score,
|
|
1428
|
+
record.revision,
|
|
1429
|
+
record.sessionId,
|
|
1430
|
+
record.meta.name,
|
|
1431
|
+
syncState,
|
|
1432
|
+
windowCount,
|
|
1433
|
+
typesIndicator,
|
|
1434
|
+
menu.open,
|
|
1435
|
+
euclid.open,
|
|
1436
|
+
tui.ui.overlay,
|
|
1437
|
+
],
|
|
1438
|
+
})
|
|
1439
|
+
)
|
|
1440
|
+
return;
|
|
1094
1441
|
followCommitted();
|
|
1442
|
+
tui.delight.song(record.sessionId, record.meta.heardLoop === true);
|
|
1095
1443
|
play?.tick();
|
|
1096
1444
|
// Values in the menu follow the score as edits land.
|
|
1097
1445
|
if (menu.open) refreshMenu();
|
|
1098
1446
|
if (euclid.open) refreshEuclid();
|
|
1099
1447
|
refreshAuditionPicker();
|
|
1100
|
-
|
|
1448
|
+
// The gate already paces frames; the app's own throttle would drop an
|
|
1449
|
+
// approved frame that lands just after a forced one, and the gate would
|
|
1450
|
+
// not ask again until the heartbeat.
|
|
1451
|
+
tui.render(appView(score, clock.beatAt()), { force: true });
|
|
1101
1452
|
};
|
|
1102
1453
|
requestFrame = () => tick(true);
|
|
1454
|
+
// The first wrap is remembered per song in .dawg metadata, never the score.
|
|
1455
|
+
tui.onFirstLoop = () => {
|
|
1456
|
+
void port
|
|
1457
|
+
.updateMeta({ heardLoop: true })
|
|
1458
|
+
.then((result) => adoptMeta(result.meta))
|
|
1459
|
+
.catch(() => undefined);
|
|
1460
|
+
};
|
|
1103
1461
|
runPromptLater = (command) => {
|
|
1104
1462
|
queuedPrompts.unshift(command);
|
|
1105
1463
|
void drainQueue();
|
|
@@ -1109,6 +1467,16 @@ async function runInteractive(): Promise<void> {
|
|
|
1109
1467
|
};
|
|
1110
1468
|
agentEventSink = (event) => {
|
|
1111
1469
|
if (event.type === "usage") return;
|
|
1470
|
+
if (event.type === "command-typing") {
|
|
1471
|
+
showMeTyping(event.text);
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (event.type === "command") return;
|
|
1475
|
+
if (event.type === "tool-start") {
|
|
1476
|
+
const caption = toolCaption(event.name);
|
|
1477
|
+
if (caption) showMeCaption(caption);
|
|
1478
|
+
}
|
|
1479
|
+
if (event.type === "done" || event.type === "error") showMeFinish();
|
|
1112
1480
|
tui.activity.applyAgentEvent(event);
|
|
1113
1481
|
if (event.type === "done" || event.type === "error") agentReported = true;
|
|
1114
1482
|
};
|
|
@@ -1119,13 +1487,21 @@ async function runInteractive(): Promise<void> {
|
|
|
1119
1487
|
tick(true);
|
|
1120
1488
|
};
|
|
1121
1489
|
stdout.on("resize", onResize);
|
|
1490
|
+
const ownWrites = new OwnWrites();
|
|
1491
|
+
let presenceClients: readonly {
|
|
1492
|
+
clientId: string;
|
|
1493
|
+
focusedTrackId?: string | null;
|
|
1494
|
+
}[] = [];
|
|
1122
1495
|
let applying: Promise<void> = Promise.resolve();
|
|
1123
1496
|
const applyLatest = (latest: typeof record): Promise<void> =>
|
|
1124
1497
|
(applying = applying.then(() => applyRecord(latest)));
|
|
1125
1498
|
const applyRecord = async (latest: typeof record): Promise<void> => {
|
|
1126
1499
|
try {
|
|
1500
|
+
// Queued before a /fork or /resume swapped the session: stale.
|
|
1501
|
+
if (latest.sessionId !== record.sessionId) return;
|
|
1127
1502
|
if (latest.revision > record.revision) {
|
|
1128
1503
|
const previousRevision = record.revision;
|
|
1504
|
+
const before = score;
|
|
1129
1505
|
record = latest;
|
|
1130
1506
|
if (stageCapture)
|
|
1131
1507
|
stageCapture.committed = scoreFromJSON(record.composition);
|
|
@@ -1162,11 +1538,27 @@ async function runInteractive(): Promise<void> {
|
|
|
1162
1538
|
else if (payload.action === "toggle") await setTransport("toggle");
|
|
1163
1539
|
}
|
|
1164
1540
|
projectSync?.scoreChanged(score);
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1541
|
+
// Another window's play/pause is followed, not announced, and this
|
|
1542
|
+
// window's own writes (which can echo back before the append
|
|
1543
|
+
// resolves) are never "synced".
|
|
1544
|
+
const from = shownRevision(record, previousRevision);
|
|
1545
|
+
const to = shownRevision(record);
|
|
1546
|
+
await ownWrites.settled();
|
|
1547
|
+
const foreign = foreignEvents(latest, previousRevision, ownWrites).some(
|
|
1548
|
+
(event) => event.kind !== "transport",
|
|
1549
|
+
);
|
|
1550
|
+
if (to !== from && foreign)
|
|
1551
|
+
tui.activity.pushCard(
|
|
1552
|
+
`synced · ${otherWindowName(presenceClients, port.clientId, {
|
|
1553
|
+
editedTrackId: editedTrack(
|
|
1554
|
+
before,
|
|
1555
|
+
scoreFromJSON(latest.composition),
|
|
1556
|
+
),
|
|
1557
|
+
trackName: (id) =>
|
|
1558
|
+
score.tracks.find((track) => track.id === id)?.name ?? id,
|
|
1559
|
+
})}`,
|
|
1560
|
+
{ tone: "info", baseRevision: from, resultRevision: to },
|
|
1561
|
+
);
|
|
1170
1562
|
}
|
|
1171
1563
|
} catch {
|
|
1172
1564
|
// An invalid composition is skipped; the next update retries.
|
|
@@ -1177,8 +1569,10 @@ async function runInteractive(): Promise<void> {
|
|
|
1177
1569
|
void applyLatest(update.record);
|
|
1178
1570
|
adoptMeta(update.record.meta);
|
|
1179
1571
|
} else if (update.type === "meta") adoptMeta(update.meta);
|
|
1180
|
-
else if (update.type === "presence")
|
|
1181
|
-
|
|
1572
|
+
else if (update.type === "presence") {
|
|
1573
|
+
presenceClients = update.clients;
|
|
1574
|
+
windowCount = update.clients.length;
|
|
1575
|
+
} else if (update.type === "sync") syncState = update.sync;
|
|
1182
1576
|
else if (update.type === "transport") {
|
|
1183
1577
|
// Every window renders the same hit line from dawgd's timestamp.
|
|
1184
1578
|
const { playing, beat, bpm, atMs } = update.transport;
|
|
@@ -1192,11 +1586,23 @@ async function runInteractive(): Promise<void> {
|
|
|
1192
1586
|
});
|
|
1193
1587
|
}
|
|
1194
1588
|
};
|
|
1195
|
-
|
|
1589
|
+
// Each subscription answers only while its port is the live one: /fork and
|
|
1590
|
+
// /resume swap the port, and a load the old port started before the swap
|
|
1591
|
+
// could otherwise land afterwards and pull the old session's name and
|
|
1592
|
+
// score into the new one.
|
|
1593
|
+
const subscribeLive = (): (() => void) => {
|
|
1594
|
+
const bound = port;
|
|
1595
|
+
ownWrites.attach(bound);
|
|
1596
|
+
return bound.subscribe((update) => {
|
|
1597
|
+
if (bound === port) onUpdate(update);
|
|
1598
|
+
});
|
|
1599
|
+
};
|
|
1600
|
+
let unsubscribe = subscribeLive();
|
|
1196
1601
|
const refreshPresence = () =>
|
|
1197
1602
|
void port
|
|
1198
1603
|
.presence()
|
|
1199
1604
|
.then((clients) => {
|
|
1605
|
+
presenceClients = clients;
|
|
1200
1606
|
windowCount = Math.max(1, clients.length);
|
|
1201
1607
|
})
|
|
1202
1608
|
.catch(() => undefined);
|
|
@@ -1208,12 +1614,13 @@ async function runInteractive(): Promise<void> {
|
|
|
1208
1614
|
rebindPort = () => {
|
|
1209
1615
|
unsubscribe();
|
|
1210
1616
|
syncState = port.sync;
|
|
1211
|
-
unsubscribe =
|
|
1617
|
+
unsubscribe = subscribeLive();
|
|
1212
1618
|
refreshPresence();
|
|
1213
1619
|
};
|
|
1214
1620
|
if (freshWorkspace)
|
|
1215
1621
|
tui.activity.pushCard("created .dawg/ · add it to .gitignore", {
|
|
1216
1622
|
tone: "info",
|
|
1623
|
+
once: true,
|
|
1217
1624
|
});
|
|
1218
1625
|
if (attachNotice)
|
|
1219
1626
|
tui.activity.pushCard(attachNotice, {
|
|
@@ -1223,6 +1630,9 @@ async function runInteractive(): Promise<void> {
|
|
|
1223
1630
|
});
|
|
1224
1631
|
if (port.status !== "file session")
|
|
1225
1632
|
tui.activity.pushCard(port.status, { tone: "info" });
|
|
1633
|
+
void launchCard?.then((card) => {
|
|
1634
|
+
if (card) tui.activity.pushCard(card.text, { tone: card.tone, once: true });
|
|
1635
|
+
});
|
|
1226
1636
|
if (await isProject(process.cwd()))
|
|
1227
1637
|
projectSync = startProjectSync(syncHost());
|
|
1228
1638
|
void currentProvider().then(() => tick(true));
|
|
@@ -1244,8 +1654,28 @@ async function runInteractive(): Promise<void> {
|
|
|
1244
1654
|
stdin.on("end", onEnd);
|
|
1245
1655
|
async function* chunks(): AsyncGenerator<string> {
|
|
1246
1656
|
for (;;) {
|
|
1247
|
-
while (inbox.length === 0)
|
|
1248
|
-
|
|
1657
|
+
while (inbox.length === 0) {
|
|
1658
|
+
// A partial escape (Alt+[, a cut-off paste) is flushed after a short
|
|
1659
|
+
// idle so it never holds back the keys behind it.
|
|
1660
|
+
const held = inputDecoder.pending();
|
|
1661
|
+
const idle =
|
|
1662
|
+
held === "paste"
|
|
1663
|
+
? PASTE_FLUSH_MS
|
|
1664
|
+
: held === "escape"
|
|
1665
|
+
? ESCAPE_FLUSH_MS
|
|
1666
|
+
: undefined;
|
|
1667
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1668
|
+
const timedOut = await new Promise<boolean>((resolve) => {
|
|
1669
|
+
wake = () => resolve(false);
|
|
1670
|
+
if (idle !== undefined)
|
|
1671
|
+
timeout = setTimeout(() => resolve(true), idle);
|
|
1672
|
+
});
|
|
1673
|
+
clearTimeout(timeout);
|
|
1674
|
+
if (timedOut && inbox.length === 0) {
|
|
1675
|
+
wake = undefined;
|
|
1676
|
+
yield INPUT_FLUSH;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1249
1679
|
wake = undefined;
|
|
1250
1680
|
const next = inbox.shift()!;
|
|
1251
1681
|
if (next === "\u0000eof") return;
|
|
@@ -1260,7 +1690,7 @@ async function runInteractive(): Promise<void> {
|
|
|
1260
1690
|
stdin.pause();
|
|
1261
1691
|
stdin.setRawMode?.(false);
|
|
1262
1692
|
// Leave the alternate screen; restore the cursor and plain paste.
|
|
1263
|
-
stdout.write(
|
|
1693
|
+
stdout.write(terminalRestore());
|
|
1264
1694
|
try {
|
|
1265
1695
|
return await flow();
|
|
1266
1696
|
} finally {
|
|
@@ -1279,10 +1709,13 @@ async function runInteractive(): Promise<void> {
|
|
|
1279
1709
|
let exiting = false;
|
|
1280
1710
|
for await (const text of chunks()) {
|
|
1281
1711
|
// A read that is exactly ESC is the Esc key, not the start of a sequence.
|
|
1282
|
-
const values =
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1712
|
+
const values =
|
|
1713
|
+
text === INPUT_FLUSH
|
|
1714
|
+
? inputDecoder.flush()
|
|
1715
|
+
: [
|
|
1716
|
+
...inputDecoder.push(text),
|
|
1717
|
+
...(text === "\u001b" ? inputDecoder.flush() : []),
|
|
1718
|
+
];
|
|
1286
1719
|
while (values.length) {
|
|
1287
1720
|
const value = values.shift()!;
|
|
1288
1721
|
// A mouse report acts on what the last frame painted under it; some
|
|
@@ -1308,9 +1741,9 @@ async function runInteractive(): Promise<void> {
|
|
|
1308
1741
|
continue;
|
|
1309
1742
|
}
|
|
1310
1743
|
}
|
|
1311
|
-
// The `?` panel closes on
|
|
1744
|
+
// The `?` panel closes on esc or ?; other keys wait (Ctrl-C quits).
|
|
1312
1745
|
if (tui.ui.keys && typeof value === "string" && value !== "\u0003") {
|
|
1313
|
-
tui.closeKeys();
|
|
1746
|
+
if (closesKeys(value)) tui.closeKeys();
|
|
1314
1747
|
tick(true);
|
|
1315
1748
|
continue;
|
|
1316
1749
|
}
|
|
@@ -1441,12 +1874,7 @@ async function runInteractive(): Promise<void> {
|
|
|
1441
1874
|
// Never await a daemon round trip here: the key loop must stay
|
|
1442
1875
|
// live for Esc, quit and redraws while the toggle is in flight.
|
|
1443
1876
|
void toggleTransport()
|
|
1444
|
-
.catch((error: unknown) =>
|
|
1445
|
-
tui.activity.pushCard(
|
|
1446
|
-
`transport failed · ${error instanceof Error ? error.message : String(error)}`,
|
|
1447
|
-
{ tone: "error" },
|
|
1448
|
-
);
|
|
1449
|
-
})
|
|
1877
|
+
.catch((error: unknown) => transportFailed(error))
|
|
1450
1878
|
.finally(() => tick(true));
|
|
1451
1879
|
} else {
|
|
1452
1880
|
const input = tui.input(value);
|
|
@@ -1475,7 +1903,9 @@ async function runInteractive(): Promise<void> {
|
|
|
1475
1903
|
if (auditionLoop?.looping && isStageable(input.value))
|
|
1476
1904
|
hoverItem(input.value, `picker:${input.picker}`);
|
|
1477
1905
|
else if (input.picker === "pattern")
|
|
1478
|
-
previewPattern(
|
|
1906
|
+
previewPattern(
|
|
1907
|
+
input.value.replace(/^\/(?:pattern|groove)\s+/, ""),
|
|
1908
|
+
);
|
|
1479
1909
|
} else if (input.type === "pick-audition") {
|
|
1480
1910
|
pickerAuditionKey(input.key);
|
|
1481
1911
|
} else if (input.type === "pick-cancel") {
|
|
@@ -1527,6 +1957,16 @@ async function runInteractive(): Promise<void> {
|
|
|
1527
1957
|
else if (action?.kind === "cancel" && agentTurn) {
|
|
1528
1958
|
agentTurn.controller.abort();
|
|
1529
1959
|
tui.activity.setSpinner("cancelling");
|
|
1960
|
+
} else if (
|
|
1961
|
+
action?.kind === "submit" &&
|
|
1962
|
+
action.value &&
|
|
1963
|
+
agentTurn &&
|
|
1964
|
+
routeDuringTurn(action.value, parsesLocally) === "local"
|
|
1965
|
+
) {
|
|
1966
|
+
// A typed command never waits on the agent: it commits its own
|
|
1967
|
+
// revision now, and the agent's next call re-reads the score.
|
|
1968
|
+
const value = action.value;
|
|
1969
|
+
void runLocalBesideAgent(value).finally(() => tick(true));
|
|
1530
1970
|
} else if (action?.kind === "submit" && action.value && agentTurn) {
|
|
1531
1971
|
// Enter during a turn steers it; Alt-Enter still queues a follow-up.
|
|
1532
1972
|
agentTurn.steering.push(action.value);
|
|
@@ -1557,17 +1997,36 @@ async function runInteractive(): Promise<void> {
|
|
|
1557
1997
|
stdin.off("end", onEnd);
|
|
1558
1998
|
clearInterval(timer);
|
|
1559
1999
|
clearInterval(presenceTimer);
|
|
1560
|
-
await projectSync?.stop();
|
|
1561
|
-
namer.dispose();
|
|
1562
2000
|
stdout.off("resize", onResize);
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
audio.stop();
|
|
1567
|
-
await port.close();
|
|
1568
|
-
stdin.setRawMode?.(false);
|
|
2001
|
+
// The terminal first: a teardown step that throws (a read-only .dawg,
|
|
2002
|
+
// a dead engine) must not leave the shell on the alternate screen.
|
|
2003
|
+
restoreTerminal();
|
|
1569
2004
|
stdin.pause();
|
|
1570
|
-
|
|
2005
|
+
console.error = consoleError;
|
|
2006
|
+
console.warn = consoleWarn;
|
|
2007
|
+
process.off("warning", onWarning);
|
|
2008
|
+
unwatchActivity();
|
|
2009
|
+
// Each step on its own: one failure does not skip the rest.
|
|
2010
|
+
const failures: unknown[] = [];
|
|
2011
|
+
const steps: Array<() => unknown> = [
|
|
2012
|
+
() => projectSync?.stop(),
|
|
2013
|
+
() => namer.dispose(),
|
|
2014
|
+
() => unsubscribe(),
|
|
2015
|
+
() => play?.exit(),
|
|
2016
|
+
() => monitorEngine?.dispose(),
|
|
2017
|
+
() => audio.stop(),
|
|
2018
|
+
() => port.close(),
|
|
2019
|
+
];
|
|
2020
|
+
for (const step of steps) {
|
|
2021
|
+
try {
|
|
2022
|
+
await step();
|
|
2023
|
+
} catch (error) {
|
|
2024
|
+
failures.push(error);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
process.off("unhandledRejection", onRejection);
|
|
2028
|
+
process.off("uncaughtException", onUncaught);
|
|
2029
|
+
if (failures.length > 0) throw failures[0];
|
|
1571
2030
|
}
|
|
1572
2031
|
}
|
|
1573
2032
|
|
|
@@ -1582,23 +2041,23 @@ function unknownCommand(command: string): string {
|
|
|
1582
2041
|
|
|
1583
2042
|
async function submit(prompt: string): Promise<string | Receipt> {
|
|
1584
2043
|
const command = prompt.trim();
|
|
2044
|
+
// Canonical names for window commands: `model key` (login), `models`,
|
|
2045
|
+
// `voice` (the Voice topic).
|
|
2046
|
+
const canonical = canonicalWindowForm(command);
|
|
2047
|
+
if (canonical) return submit(canonical);
|
|
1585
2048
|
const helpCommand = command.match(/^\/?(?:help|\?)(?:\s+(\S+))?$/i);
|
|
1586
2049
|
if (helpCommand) {
|
|
1587
2050
|
const topic = helpCommand[1];
|
|
2051
|
+
// The panel's inner width (tui/app.ts paintText): rows clip there with
|
|
2052
|
+
// an ellipsis, the same as every other panel.
|
|
2053
|
+
const columns = stdout.columns ?? 80;
|
|
1588
2054
|
const lines = helpTopicLines(
|
|
1589
2055
|
topic,
|
|
1590
|
-
Math.max(
|
|
1591
|
-
);
|
|
1592
|
-
if (!lines)
|
|
1593
|
-
return fail(
|
|
1594
|
-
`no help topic ${topic} · /help all · ${HELP_TOPICS.join(" ")}`,
|
|
1595
|
-
);
|
|
1596
|
-
tui.openText(topic ? `help · ${topic.toLowerCase()}` : "help", lines);
|
|
1597
|
-
return ok(
|
|
1598
|
-
topic
|
|
1599
|
-
? `help · ${topic.toLowerCase()}`
|
|
1600
|
-
: "help · /help all for every command",
|
|
2056
|
+
Math.max(10, columns - (columns >= 60 ? 8 : 4)),
|
|
1601
2057
|
);
|
|
2058
|
+
if (!lines) return fail(helpMiss(topic!));
|
|
2059
|
+
tui.openText(helpTitle(topic), lines);
|
|
2060
|
+
return ok(topic ? helpTitle(topic) : "help · help all for every command");
|
|
1602
2061
|
}
|
|
1603
2062
|
if (/^\/?tracks$/i.test(command)) {
|
|
1604
2063
|
const problems = await sampleProblems(score);
|
|
@@ -1630,7 +2089,32 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1630
2089
|
items.findIndex((item) => item.current),
|
|
1631
2090
|
),
|
|
1632
2091
|
});
|
|
1633
|
-
return
|
|
2092
|
+
return note(`${items.length} track${items.length === 1 ? "" : "s"}`);
|
|
2093
|
+
}
|
|
2094
|
+
if (/^\/?(?:instruments|instrument\s+(?:list|ls|presets))$/i.test(command)) {
|
|
2095
|
+
// `instrument list`: every word `instrument <name>` takes, by family.
|
|
2096
|
+
const columns = stdout.columns ?? 80;
|
|
2097
|
+
const lines = instrumentListLines(
|
|
2098
|
+
Math.max(10, columns - (columns >= 60 ? 8 : 4)),
|
|
2099
|
+
);
|
|
2100
|
+
tui.openText("instruments", lines);
|
|
2101
|
+
return note("instruments · instrument <name> on the focused track");
|
|
2102
|
+
}
|
|
2103
|
+
if (/^\/?notes(?:\s+(?:list|ls))?$/i.test(command)) {
|
|
2104
|
+
// `notes`: the focused track's note ids, the names `remove <id>` takes.
|
|
2105
|
+
const notes = score.notes
|
|
2106
|
+
.filter((entry) => entry.trackId === requestedTrack)
|
|
2107
|
+
.slice()
|
|
2108
|
+
.sort((a, b) => a.startTick - b.startTick);
|
|
2109
|
+
const lines = notes.map(
|
|
2110
|
+
(entry) =>
|
|
2111
|
+
`${entry.id} · ${midiNoteName(entry.pitch)} · beat ${+(entry.startTick / score.ticksPerBeat).toFixed(2)}`,
|
|
2112
|
+
);
|
|
2113
|
+
if (lines.length === 0) return note(`notes · ${requestedTrack} has none`);
|
|
2114
|
+
tui.openText(`notes · ${requestedTrack}`, lines);
|
|
2115
|
+
return note(
|
|
2116
|
+
`${lines.length} note${lines.length === 1 ? "" : "s"} · ${requestedTrack} · remove <id>`,
|
|
2117
|
+
);
|
|
1634
2118
|
}
|
|
1635
2119
|
const playCommand = command.match(
|
|
1636
2120
|
/^\/play(?:\s+(on|off|degrees|in-key|chromatic))?$/i,
|
|
@@ -1656,9 +2140,31 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1656
2140
|
}
|
|
1657
2141
|
const chordsCommand = command.match(/^\/chords(?:\s+(.+))?$/i);
|
|
1658
2142
|
if (chordsCommand) {
|
|
1659
|
-
const result = applyChordsCommand(
|
|
2143
|
+
const result = applyChordsCommand(
|
|
2144
|
+
chordSettings,
|
|
2145
|
+
chordsCommand[1] ?? "",
|
|
2146
|
+
score.tempoBpm,
|
|
2147
|
+
);
|
|
1660
2148
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1661
2149
|
}
|
|
2150
|
+
const showMeCommand = command.match(/^\/show-?me(?:\s+(\S+))?$/i);
|
|
2151
|
+
if (showMeCommand) {
|
|
2152
|
+
if (!showMeCommand[1])
|
|
2153
|
+
return ok(`show me · ${showMe.level} · /showme on|quiet|off`);
|
|
2154
|
+
const level = parseShowMe(showMeCommand[1]);
|
|
2155
|
+
if (!level) return fail("usage · /showme on|quiet|off");
|
|
2156
|
+
showMe.level = level;
|
|
2157
|
+
void writeConfig({ dir: configDir() }, { showMe: level }).catch(
|
|
2158
|
+
() => undefined,
|
|
2159
|
+
);
|
|
2160
|
+
return ok(
|
|
2161
|
+
level === "off"
|
|
2162
|
+
? "show me off · the agent edits with tools"
|
|
2163
|
+
: level === "quiet"
|
|
2164
|
+
? "show me quiet · the agent types commands, no captions"
|
|
2165
|
+
: "show me on · the agent types commands, slides faders, plays keys",
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
1662
2168
|
const countIn = command.match(/^\/count-?in\s+([0-2])$/i);
|
|
1663
2169
|
if (countIn) return ok(playSession().setCountIn(Number(countIn[1])));
|
|
1664
2170
|
const euclidCommand = command.match(/^\/euclid(?:\s+(\S+))?$/i);
|
|
@@ -1673,21 +2179,45 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1673
2179
|
}
|
|
1674
2180
|
const tryCommand = command.match(/^\/try(?:\s+(.+))?$/i);
|
|
1675
2181
|
if (tryCommand) return tryPrompt(tryCommand[1]?.trim() ?? "");
|
|
1676
|
-
// A bare
|
|
1677
|
-
// opens the fader drawer on it, with its
|
|
1678
|
-
|
|
1679
|
-
|
|
2182
|
+
// A bare scalar (`volume`, `pan`, `fx reverb mix`, and the song's
|
|
2183
|
+
// `tempo`, `bars` and `meter`) opens the fader drawer on it, with its
|
|
2184
|
+
// related params stacked below.
|
|
2185
|
+
if (
|
|
2186
|
+
/^\/?(?:volume|pan|tempo|bpm|bars|meter|fx\s+\S+(?:\s+\S+)?)$/i.test(
|
|
2187
|
+
command,
|
|
2188
|
+
)
|
|
2189
|
+
) {
|
|
2190
|
+
const context = menuContext();
|
|
2191
|
+
const label =
|
|
2192
|
+
menu.showFader(context, command) ?? songFader(context, command);
|
|
1680
2193
|
if (label) {
|
|
2194
|
+
const field = menu
|
|
2195
|
+
.faderFields(context)
|
|
2196
|
+
.find((candidate) => candidate.label === label);
|
|
2197
|
+
// The receipt names the value it opened on: `tempo · 120 BPM · …`.
|
|
2198
|
+
const value =
|
|
2199
|
+
field?.kind === "number"
|
|
2200
|
+
? field.value !== undefined
|
|
2201
|
+
? field.format(field.value)
|
|
2202
|
+
: field.off
|
|
2203
|
+
: undefined;
|
|
1681
2204
|
openFader(label, true);
|
|
1682
2205
|
refreshMenu();
|
|
1683
|
-
return ok(
|
|
2206
|
+
return ok(
|
|
2207
|
+
`${label}${value ? ` · ${value}` : ""} · ←→ adjust · enter keep · esc revert`,
|
|
2208
|
+
);
|
|
1684
2209
|
}
|
|
1685
2210
|
}
|
|
1686
2211
|
const menuCommand = command.match(/^\/menu(?:\s+(\S+))?$/i);
|
|
1687
2212
|
if (menuCommand) {
|
|
1688
2213
|
const section = menuCommand[1]?.toLowerCase();
|
|
1689
|
-
|
|
1690
|
-
|
|
2214
|
+
// §4: the keys topic has no menu page; it opens the ? panel.
|
|
2215
|
+
if (section && (section === "keys" || TOPIC_ALIASES[section] === "keys")) {
|
|
2216
|
+
tui.showKeys("keys", keyLines(KEYS.prompt));
|
|
2217
|
+
return ok("keys");
|
|
2218
|
+
}
|
|
2219
|
+
if (section && !menuSectionPath(menuContext(), section))
|
|
2220
|
+
return fail(menuUsage(menuContext(), section));
|
|
1691
2221
|
openMenu(section);
|
|
1692
2222
|
return ok("menu");
|
|
1693
2223
|
}
|
|
@@ -1717,6 +2247,73 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1717
2247
|
if (requestedTrack !== grainTrack[1]!.toLowerCase()) return focused;
|
|
1718
2248
|
return submit(grainTrack[2]!);
|
|
1719
2249
|
}
|
|
2250
|
+
// `/track rm <name>` (aliases remove, delete) and `/track move <name>
|
|
2251
|
+
// <position>`: the human surface for the removeTrack and moveTrack
|
|
2252
|
+
// operations. Removing a track also drops any vocoder src or autotune from
|
|
2253
|
+
// that named it; ^z brings everything back.
|
|
2254
|
+
const trackEdit = command.match(
|
|
2255
|
+
/^\/?track\s+(rm|remove|delete|move)\s+(.{1,64}?)\s*$/i,
|
|
2256
|
+
);
|
|
2257
|
+
if (trackEdit) {
|
|
2258
|
+
const verb = trackEdit[1]!.toLowerCase() === "move" ? "move" : "rm";
|
|
2259
|
+
let rest = trackEdit[2]!.trim();
|
|
2260
|
+
let position: number | undefined;
|
|
2261
|
+
if (verb === "move") {
|
|
2262
|
+
const tail = rest.match(/^(.+?)\s+(\d{1,3})$/);
|
|
2263
|
+
if (!tail)
|
|
2264
|
+
return fail(`usage · /track move <name> <1..${score.tracks.length}>`);
|
|
2265
|
+
rest = tail[1]!;
|
|
2266
|
+
position = Number(tail[2]);
|
|
2267
|
+
}
|
|
2268
|
+
const wanted = rest
|
|
2269
|
+
.replace(/^["']|["']$/g, "")
|
|
2270
|
+
.trim()
|
|
2271
|
+
.replace(/\s+/g, " ")
|
|
2272
|
+
.toLowerCase();
|
|
2273
|
+
const found = score.tracks.find(
|
|
2274
|
+
(track) =>
|
|
2275
|
+
track.id.toLowerCase() === wanted ||
|
|
2276
|
+
track.id.toLowerCase() === wanted.replace(/ /g, "-") ||
|
|
2277
|
+
(track.name ?? "").toLowerCase() === wanted,
|
|
2278
|
+
);
|
|
2279
|
+
if (!found) return fail(noTrack(rest));
|
|
2280
|
+
if (verb === "move") {
|
|
2281
|
+
if (position! < 1 || position! > score.tracks.length)
|
|
2282
|
+
return fail(`usage · /track move <name> <1..${score.tracks.length}>`);
|
|
2283
|
+
const next = applyScoreOperation(score, {
|
|
2284
|
+
type: "moveTrack",
|
|
2285
|
+
trackId: found.id,
|
|
2286
|
+
index: position! - 1,
|
|
2287
|
+
});
|
|
2288
|
+
await commitScore(next, "track.move", { trackId: found.id });
|
|
2289
|
+
await projectSync?.flushScore();
|
|
2290
|
+
return ok(`moved ${found.id} to position ${position}`);
|
|
2291
|
+
}
|
|
2292
|
+
if (score.tracks.length <= 1)
|
|
2293
|
+
return fail("the last track stays · /clear empties it");
|
|
2294
|
+
const next = applyScoreOperation(score, {
|
|
2295
|
+
type: "removeTrack",
|
|
2296
|
+
trackId: found.id,
|
|
2297
|
+
});
|
|
2298
|
+
const dropped = score.tracks
|
|
2299
|
+
.filter((track) => track.id !== found.id)
|
|
2300
|
+
.filter((track) => {
|
|
2301
|
+
const after = next.tracks.find((t) => t.id === track.id);
|
|
2302
|
+
return (
|
|
2303
|
+
(track.vocoder?.src !== undefined &&
|
|
2304
|
+
after?.vocoder?.src === undefined) ||
|
|
2305
|
+
(track.autotune?.from !== undefined &&
|
|
2306
|
+
after?.autotune?.from === undefined)
|
|
2307
|
+
);
|
|
2308
|
+
})
|
|
2309
|
+
.map((track) => track.id);
|
|
2310
|
+
await commitScore(next, "track.remove", { trackId: found.id });
|
|
2311
|
+
await projectSync?.flushScore();
|
|
2312
|
+
if (found.id === requestedTrack) await focusTrack(next.tracks[0]!.id);
|
|
2313
|
+
return ok(
|
|
2314
|
+
`removed ${found.id}${dropped.length ? ` · dropped references on ${dropped.join(", ")}` : ""} · ctrl-z undoes`,
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
1720
2317
|
// `/track piano b`: a name with spaces focuses the track of that name, or
|
|
1721
2318
|
// creates `piano-b` named "piano b".
|
|
1722
2319
|
const namedTrack = command.match(/^\/track\s+([a-z0-9._ -]{1,64})$/i);
|
|
@@ -1731,12 +2328,100 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1731
2328
|
if (sample) return sampleCommand(sample);
|
|
1732
2329
|
const fit = parseFitCommand(command);
|
|
1733
2330
|
if (fit) return fitCommand(fit);
|
|
2331
|
+
const shift = parseShiftCommand(command);
|
|
2332
|
+
if (shift) {
|
|
2333
|
+
const result = applyShiftCommand(score, requestedTrack, shift);
|
|
2334
|
+
if (!result.ok) return fail(result.message);
|
|
2335
|
+
await commitScore(result.next, "sample.set", { trackId: requestedTrack });
|
|
2336
|
+
await projectSync?.flushScore();
|
|
2337
|
+
return ok(result.message);
|
|
2338
|
+
}
|
|
2339
|
+
const resample = parseResampleCommand(command);
|
|
2340
|
+
if (resample) return resampleCommand(resample);
|
|
2341
|
+
// 0.7 clips: `/clip` edits and `/lyrics` on the focused track.
|
|
2342
|
+
const clipCommand = parseClipCommand(command);
|
|
2343
|
+
if (clipCommand) {
|
|
2344
|
+
if ("error" in clipCommand) return fail(clipCommand.error);
|
|
2345
|
+
const result = await runClipCommand(clipCommand, {
|
|
2346
|
+
score,
|
|
2347
|
+
trackId: requestedTrack,
|
|
2348
|
+
cwd: process.cwd(),
|
|
2349
|
+
});
|
|
2350
|
+
if (!result.ok) return fail(result.message);
|
|
2351
|
+
if (result.next && result.kind) {
|
|
2352
|
+
await commitScore(result.next, result.kind, {
|
|
2353
|
+
trackId: requestedTrack,
|
|
2354
|
+
...result.payload,
|
|
2355
|
+
});
|
|
2356
|
+
await projectSync?.flushScore();
|
|
2357
|
+
}
|
|
2358
|
+
return ok(result.message);
|
|
2359
|
+
}
|
|
2360
|
+
const lyricsCommand = parseLyricsCommand(command);
|
|
2361
|
+
if (lyricsCommand) {
|
|
2362
|
+
const result = applyLyrics(score, requestedTrack, lyricsCommand);
|
|
2363
|
+
if (!result.ok) return fail(result.message);
|
|
2364
|
+
if (result.next && result.kind) {
|
|
2365
|
+
await commitScore(result.next, result.kind, {
|
|
2366
|
+
trackId: requestedTrack,
|
|
2367
|
+
...result.payload,
|
|
2368
|
+
});
|
|
2369
|
+
await projectSync?.flushScore();
|
|
2370
|
+
}
|
|
2371
|
+
return ok(result.message);
|
|
2372
|
+
}
|
|
2373
|
+
// 0.7 autotune: `/autotune …` (and `/tune <preset>` pointing here).
|
|
2374
|
+
// `/vocal autotune …` takes the same path (presets panel, draft, commit).
|
|
2375
|
+
const autotune = parseAutotuneCommand(
|
|
2376
|
+
command.replace(/^\/?vocal\s+(?=autotune\b)/i, ""),
|
|
2377
|
+
);
|
|
2378
|
+
if (autotune) {
|
|
2379
|
+
if (autotune.type === "autotune-list")
|
|
2380
|
+
tui.openText("autotune presets", autotuneListLines());
|
|
2381
|
+
if (autotune.type !== "autotune-usage" && autotune.type !== "autotune-list")
|
|
2382
|
+
await materializeDraft();
|
|
2383
|
+
const result = applyAutotuneCommand(score, requestedTrack, autotune);
|
|
2384
|
+
if (result.next && result.kind)
|
|
2385
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
2386
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
2387
|
+
}
|
|
2388
|
+
// 0.7 Voice: `/vocal <verb>`; lanes register verbs in VOCAL_VERBS.
|
|
2389
|
+
setClipImportDeps({
|
|
2390
|
+
media: {
|
|
2391
|
+
...mediaServices(),
|
|
2392
|
+
signal: new AbortController().signal,
|
|
2393
|
+
progress: () => undefined,
|
|
2394
|
+
},
|
|
2395
|
+
});
|
|
2396
|
+
const vocal = parseVocalCommand(command);
|
|
2397
|
+
if (vocal) {
|
|
2398
|
+
if (vocal.kind === "verb") await materializeDraft();
|
|
2399
|
+
const result = await runVocalCommand(vocal, {
|
|
2400
|
+
score,
|
|
2401
|
+
trackId: requestedTrack,
|
|
2402
|
+
cwd: process.cwd(),
|
|
2403
|
+
});
|
|
2404
|
+
if (!result.ok) return fail(result.message);
|
|
2405
|
+
if (result.next && result.kind) {
|
|
2406
|
+
await commitScore(result.next, result.kind, {
|
|
2407
|
+
trackId: requestedTrack,
|
|
2408
|
+
...result.payload,
|
|
2409
|
+
});
|
|
2410
|
+
await projectSync?.flushScore();
|
|
2411
|
+
}
|
|
2412
|
+
// A verb that made or chose another track (a vocoder carrier): focus it.
|
|
2413
|
+
if (result.trackId && result.trackId !== requestedTrack) {
|
|
2414
|
+
await port.focus(result.trackId);
|
|
2415
|
+
requestedTrack = result.trackId;
|
|
2416
|
+
}
|
|
2417
|
+
return ok(result.message);
|
|
2418
|
+
}
|
|
1734
2419
|
const pack = parsePackCommand(command);
|
|
1735
2420
|
if (pack) return packCommand(pack);
|
|
1736
2421
|
const pattern = parsePatternCommand(command);
|
|
1737
2422
|
if (pattern) return patternCommand(pattern);
|
|
1738
2423
|
const kit = parseKitCommand(command);
|
|
1739
|
-
if (kit) return kitCommand(kit,
|
|
2424
|
+
if (kit) return kitCommand(kit, /^\/?kits?\s*$/i.test(command.trim()));
|
|
1740
2425
|
const wavetable = parseWavetableCommand(command);
|
|
1741
2426
|
if (wavetable) return wavetableCommand(wavetable);
|
|
1742
2427
|
const sessionReply = await sessionCommand(command);
|
|
@@ -1765,11 +2450,52 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1765
2450
|
await commitScore(result.next, result.kind, result.payload);
|
|
1766
2451
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1767
2452
|
}
|
|
2453
|
+
const guitar = parseGuitarCommand(command);
|
|
2454
|
+
if (guitar) {
|
|
2455
|
+
if (guitar.type !== "guitar-show" && guitar.type !== "guitar-hint")
|
|
2456
|
+
await materializeDraft();
|
|
2457
|
+
const result = applyGuitarCommand(score, requestedTrack, guitar);
|
|
2458
|
+
if (result.next && result.kind)
|
|
2459
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
2460
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
2461
|
+
}
|
|
2462
|
+
const progression = parseProgressionCommand(command);
|
|
2463
|
+
if (progression) {
|
|
2464
|
+
if (progression.type === "progression") await materializeDraft();
|
|
2465
|
+
const result = applyProgressionCommand(
|
|
2466
|
+
score,
|
|
2467
|
+
requestedTrack,
|
|
2468
|
+
progression,
|
|
2469
|
+
() => randomUUID().slice(0, 12),
|
|
2470
|
+
);
|
|
2471
|
+
if (result.next && result.kind)
|
|
2472
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
2473
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
2474
|
+
}
|
|
2475
|
+
const strum = parseStrumCommand(command);
|
|
2476
|
+
if (strum) {
|
|
2477
|
+
if (strum.type === "strum") await materializeDraft();
|
|
2478
|
+
const result = applyStrumCommand(score, requestedTrack, strum, () =>
|
|
2479
|
+
randomUUID().slice(0, 12),
|
|
2480
|
+
);
|
|
2481
|
+
if (result.next && result.kind)
|
|
2482
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
2483
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
2484
|
+
}
|
|
1768
2485
|
const rig = parseRigCommand(command);
|
|
1769
2486
|
if (rig) {
|
|
1770
2487
|
if (rig.type !== "rig-show" && rig.type !== "rig-hint")
|
|
1771
2488
|
await materializeDraft();
|
|
1772
2489
|
const result = applyRigCommand(score, requestedTrack, rig);
|
|
2490
|
+
if (result.next && result.kind)
|
|
2491
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
2492
|
+
return readOrDone(result);
|
|
2493
|
+
}
|
|
2494
|
+
// 0.7 `/formant` and `/vowel`: short forms of `fx formant|vowel`.
|
|
2495
|
+
const formant = parseFormantCommand(command) ?? parseVowelCommand(command);
|
|
2496
|
+
if (formant) {
|
|
2497
|
+
if (formant.type === "fx") await materializeDraft();
|
|
2498
|
+
const result = applyFormantCommand(score, requestedTrack, formant);
|
|
1773
2499
|
if (result.next && result.kind)
|
|
1774
2500
|
await commitScore(result.next, result.kind, result.payload);
|
|
1775
2501
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
@@ -1791,6 +2517,8 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1791
2517
|
const result = applyFxCommand(score, requestedTrack, fx, pinnedIr);
|
|
1792
2518
|
if (result.next && result.kind)
|
|
1793
2519
|
await commitScore(result.next, result.kind, result.payload);
|
|
2520
|
+
// Bare `fx` reads the chain (`•`); it changes nothing.
|
|
2521
|
+
if (result.ok && fx.type === "fx-list") return note(result.message);
|
|
1794
2522
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1795
2523
|
}
|
|
1796
2524
|
const unknownFx = unknownFxMessage(command);
|
|
@@ -1806,6 +2534,13 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1806
2534
|
}
|
|
1807
2535
|
const tuning = parseTuningCommand(command);
|
|
1808
2536
|
if (tuning) return tuningCommand(tuning);
|
|
2537
|
+
const calibration = parseCalibrationCommand(command);
|
|
2538
|
+
if (calibration) {
|
|
2539
|
+
const result = applyCalibrationCommand(score, calibration);
|
|
2540
|
+
if (result.next && result.kind)
|
|
2541
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
2542
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
2543
|
+
}
|
|
1809
2544
|
const keysCommand = parseKeysCommand(command);
|
|
1810
2545
|
if (keysCommand) {
|
|
1811
2546
|
const reads =
|
|
@@ -1827,13 +2562,54 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1827
2562
|
await commitScore(result.next, result.kind, result.payload);
|
|
1828
2563
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1829
2564
|
}
|
|
2565
|
+
const vocoderCommand = parseVocoderCommand(command);
|
|
2566
|
+
if (vocoderCommand) {
|
|
2567
|
+
if (vocoderCommand.type === "vocoder-list")
|
|
2568
|
+
tui.openText("vocoder presets", vocoderListLines());
|
|
2569
|
+
if (
|
|
2570
|
+
vocoderCommand.type !== "vocoder-list" &&
|
|
2571
|
+
vocoderCommand.type !== "vocoder-usage"
|
|
2572
|
+
)
|
|
2573
|
+
await materializeDraft();
|
|
2574
|
+
const result = applyVocoderCommand(score, requestedTrack, vocoderCommand);
|
|
2575
|
+
if (result.next && result.kind)
|
|
2576
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
2577
|
+
// `/vocoder` on a vocal makes a carrier track: focus it.
|
|
2578
|
+
if (result.trackId && result.trackId !== requestedTrack) {
|
|
2579
|
+
await port.focus(result.trackId);
|
|
2580
|
+
requestedTrack = result.trackId;
|
|
2581
|
+
}
|
|
2582
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
2583
|
+
}
|
|
2584
|
+
const windCommand = parseWindCommand(command);
|
|
2585
|
+
if (windCommand) {
|
|
2586
|
+
if (windCommand.type === "wind-list")
|
|
2587
|
+
tui.openText("wind presets", windListLines());
|
|
2588
|
+
if (windCommand.type !== "wind-show" && windCommand.type !== "wind-list")
|
|
2589
|
+
await materializeDraft();
|
|
2590
|
+
const result = applyWindCommand(score, requestedTrack, windCommand);
|
|
2591
|
+
if (result.next && result.kind)
|
|
2592
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
2593
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
2594
|
+
}
|
|
2595
|
+
const singCommand = parseSingCommand(command);
|
|
2596
|
+
if (singCommand) {
|
|
2597
|
+
if (singCommand.type === "sing-list")
|
|
2598
|
+
tui.openText("sing presets", singListLines());
|
|
2599
|
+
if (singCommand.type !== "sing-show" && singCommand.type !== "sing-list")
|
|
2600
|
+
await materializeDraft();
|
|
2601
|
+
const result = applySingCommand(score, requestedTrack, singCommand);
|
|
2602
|
+
if (result.next && result.kind)
|
|
2603
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
2604
|
+
return result.ok ? ok(result.message) : fail(result.message);
|
|
2605
|
+
}
|
|
1830
2606
|
const synth = parseSynthCommand(command);
|
|
1831
2607
|
if (synth) {
|
|
1832
2608
|
if (synth.type !== "synth-list") await materializeDraft();
|
|
1833
2609
|
const result = applySynthCommand(score, requestedTrack, synth);
|
|
1834
2610
|
if (result.next && result.kind)
|
|
1835
2611
|
await commitScore(result.next, result.kind, result.payload);
|
|
1836
|
-
return
|
|
2612
|
+
return readOrDone(result);
|
|
1837
2613
|
}
|
|
1838
2614
|
const stringCommand = parseStringCommand(command);
|
|
1839
2615
|
if (stringCommand) {
|
|
@@ -1845,7 +2621,7 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1845
2621
|
const result = applyStringCommand(score, requestedTrack, stringCommand);
|
|
1846
2622
|
if (result.next && result.kind)
|
|
1847
2623
|
await commitScore(result.next, result.kind, result.payload);
|
|
1848
|
-
return
|
|
2624
|
+
return readOrDone(result);
|
|
1849
2625
|
}
|
|
1850
2626
|
const grainHint = grainSrcHint(command);
|
|
1851
2627
|
if (grainHint) return fail(grainHint);
|
|
@@ -1858,6 +2634,50 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1858
2634
|
await commitScore(result.next, result.kind, result.payload);
|
|
1859
2635
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1860
2636
|
}
|
|
2637
|
+
const styleCommand = parseStyleCommand(
|
|
2638
|
+
/^\/?styles?\s+presets$/i.test(command) ? "style list" : command,
|
|
2639
|
+
);
|
|
2640
|
+
if (styleCommand) {
|
|
2641
|
+
const writes =
|
|
2642
|
+
styleCommand.type === "style-apply" ||
|
|
2643
|
+
styleCommand.type === "style-again";
|
|
2644
|
+
// Bare `style` opens Arrange › style; `style list|ls|presets` lists.
|
|
2645
|
+
if (
|
|
2646
|
+
styleCommand.type === "style-families" &&
|
|
2647
|
+
/^\/?styles?$/i.test(command)
|
|
2648
|
+
) {
|
|
2649
|
+
openMenu("style");
|
|
2650
|
+
return ok(
|
|
2651
|
+
"style · ↑/↓ browse · enter makes the song · style list prints it",
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
if (writes) await materializeDraft();
|
|
2655
|
+
const result = applyStyleCommand(score, styleCommand);
|
|
2656
|
+
if (result.next && result.kind)
|
|
2657
|
+
await commitScore(result.next, result.kind, result.payload);
|
|
2658
|
+
if (result.log) tui.activity.pushNote(result.log);
|
|
2659
|
+
if (!result.ok) return fail(result.message);
|
|
2660
|
+
// A multi-line listing opens as text; the card keeps its first line
|
|
2661
|
+
// so lines never run together on the one-line strip.
|
|
2662
|
+
if (!writes && result.message.includes("\n")) {
|
|
2663
|
+
const [head = "styles", ...rest] = result.message.split("\n");
|
|
2664
|
+
tui.openText(head, rest);
|
|
2665
|
+
return note(head);
|
|
2666
|
+
}
|
|
2667
|
+
// A new song: focus its first melodic track, so play and the menu
|
|
2668
|
+
// land on something tonal instead of a track the style replaced.
|
|
2669
|
+
const melodic = writes
|
|
2670
|
+
? result.next?.tracks.find((track) => !isDrumInstrument(track.instrument))
|
|
2671
|
+
: undefined;
|
|
2672
|
+
if (melodic && melodic.id !== requestedTrack) {
|
|
2673
|
+
await port.focus(melodic.id);
|
|
2674
|
+
requestedTrack = melodic.id;
|
|
2675
|
+
draftTrack = false;
|
|
2676
|
+
}
|
|
2677
|
+
return ok(
|
|
2678
|
+
melodic ? `${result.message} · focused ${melodic.id}` : result.message,
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
1861
2681
|
const masterCommand = parseMasterCommand(command);
|
|
1862
2682
|
if (masterCommand) {
|
|
1863
2683
|
if (masterCommand.type === "master-measure")
|
|
@@ -1870,7 +2690,33 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1870
2690
|
const result = applyMasterCommand(score, masterCommand);
|
|
1871
2691
|
if (result.next && result.kind)
|
|
1872
2692
|
await commitScore(result.next, result.kind, result.payload);
|
|
1873
|
-
return
|
|
2693
|
+
return readOrDone(result);
|
|
2694
|
+
}
|
|
2695
|
+
// `loop <section> | <a>-<b> | off`: what playback loops, a section.
|
|
2696
|
+
const loop = parseLoopCommand(command);
|
|
2697
|
+
if (loop) {
|
|
2698
|
+
if (loop.type === "loop-show")
|
|
2699
|
+
return note(
|
|
2700
|
+
score.loopSection
|
|
2701
|
+
? `loop · ${score.loopSection} · loop off plays the song`
|
|
2702
|
+
: `loop · the song · ${LOOP_USAGE}`,
|
|
2703
|
+
);
|
|
2704
|
+
if (loop.type === "loop-off") return submit("section loop off");
|
|
2705
|
+
if (loop.type === "loop-section")
|
|
2706
|
+
return submit(`section loop ${loop.name}`);
|
|
2707
|
+
const span = score.sections.find(
|
|
2708
|
+
(section) =>
|
|
2709
|
+
section.startBar + 1 === loop.from &&
|
|
2710
|
+
section.startBar + section.bars === loop.to,
|
|
2711
|
+
);
|
|
2712
|
+
if (span) return submit(`section loop ${span.name}`);
|
|
2713
|
+
// No section spans the bars: mark (or move) the section named `loop`
|
|
2714
|
+
// and loop it, as one revision.
|
|
2715
|
+
await materializeDraft();
|
|
2716
|
+
const result = loopSpan(score, requestedTrack, loop.from, loop.to);
|
|
2717
|
+
if (!result.ok) return fail(result.message);
|
|
2718
|
+
await commitScore(result.next, "score.sections", { loop: result.name });
|
|
2719
|
+
return ok(result.message);
|
|
1874
2720
|
}
|
|
1875
2721
|
const arrange = parseSectionCommand(command, score);
|
|
1876
2722
|
if (arrange) {
|
|
@@ -1884,7 +2730,7 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1884
2730
|
if (result.next && result.kind)
|
|
1885
2731
|
await commitScore(result.next, result.kind, result.payload);
|
|
1886
2732
|
if (result.seekBeat !== undefined) await seekTransport(result.seekBeat);
|
|
1887
|
-
return
|
|
2733
|
+
return readOrDone(result);
|
|
1888
2734
|
}
|
|
1889
2735
|
const music = parseMusicCommand(command);
|
|
1890
2736
|
if (music) {
|
|
@@ -1896,20 +2742,21 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1896
2742
|
await commitScore(result.next, result.kind, result.payload);
|
|
1897
2743
|
return result.ok ? ok(result.message) : fail(result.message);
|
|
1898
2744
|
}
|
|
1899
|
-
const exportCommand = command
|
|
2745
|
+
const exportCommand = parseExportCommand(command);
|
|
1900
2746
|
if (exportCommand) {
|
|
1901
|
-
const path = resolve(exportCommand
|
|
1902
|
-
if (
|
|
2747
|
+
const path = resolve(exportCommand.path);
|
|
2748
|
+
if (exportCommand.format === "mid") {
|
|
1903
2749
|
await writeFile(path, scoreToMidi(exportScore(score)));
|
|
1904
|
-
return `exported midi · ${exportCommand
|
|
2750
|
+
return `exported midi · ${exportCommand.path}`;
|
|
1905
2751
|
}
|
|
1906
|
-
// Audio
|
|
1907
|
-
if (
|
|
1908
|
-
return
|
|
1909
|
-
|
|
1910
|
-
|
|
2752
|
+
// Audio goes through the `dawg render` path, the same bytes as the CLI.
|
|
2753
|
+
if (exportCommand.format === "wav")
|
|
2754
|
+
return exportWav(exportCommand.path, exportCommand.stems);
|
|
2755
|
+
// Other audio names are not formats dawg writes.
|
|
2756
|
+
if (/\.(aiff?|flac|mp3|ogg|m4a)$/i.test(path))
|
|
2757
|
+
return fail(`export · ${exportCommand.path} · ${EXPORT_USAGE}`);
|
|
1911
2758
|
await writeFile(path, encodeLoop(score), "utf8");
|
|
1912
|
-
return `exported · ${exportCommand
|
|
2759
|
+
return `exported · ${exportCommand.path}`;
|
|
1913
2760
|
}
|
|
1914
2761
|
const importCommand = command.match(/^\/?import\s+([^\s]+)$/i);
|
|
1915
2762
|
if (importCommand) {
|
|
@@ -1944,14 +2791,14 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1944
2791
|
tui.activity.setSpinner(undefined);
|
|
1945
2792
|
}
|
|
1946
2793
|
}
|
|
1947
|
-
const
|
|
2794
|
+
const result = await tuiSetModel(modelMatch[1]);
|
|
1948
2795
|
provider = undefined;
|
|
1949
2796
|
await currentProvider();
|
|
1950
|
-
return
|
|
2797
|
+
return result.ok ? result.text : fail(result.text);
|
|
1951
2798
|
}
|
|
1952
2799
|
if (/^\/login\b/i.test(prompt.trim())) {
|
|
1953
2800
|
const parsed = tuiLoginArgs(prompt.trim());
|
|
1954
|
-
if (typeof parsed === "string") return `
|
|
2801
|
+
if (typeof parsed === "string") return `model key · ${parsed}`;
|
|
1955
2802
|
await handoff(() =>
|
|
1956
2803
|
runTuiLogin(
|
|
1957
2804
|
parsed.target === "auto" ? "pick" : parsed.target,
|
|
@@ -1961,8 +2808,8 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1961
2808
|
provider = undefined;
|
|
1962
2809
|
await currentProvider();
|
|
1963
2810
|
return providerName === "offline"
|
|
1964
|
-
? "
|
|
1965
|
-
: `
|
|
2811
|
+
? "no agent key · model key adds one · direct commands still work"
|
|
2812
|
+
: `agent key · ${providerName}`;
|
|
1966
2813
|
}
|
|
1967
2814
|
if (/^\/(logout|auth)\b/i.test(prompt.trim())) {
|
|
1968
2815
|
tui.activity.setSpinner(prompt.trim().split(/\s+/)[0]!.slice(1));
|
|
@@ -1977,8 +2824,46 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1977
2824
|
tui.activity.setSpinner(undefined);
|
|
1978
2825
|
}
|
|
1979
2826
|
}
|
|
2827
|
+
// The grammar's second reading (design §3): a line no handler took runs
|
|
2828
|
+
// in its other spellings — `/x` ≡ `x`, aliases, `rm|remove|delete`,
|
|
2829
|
+
// `list|presets|ls` — so only lines that failed before get one.
|
|
2830
|
+
// A line the core prompt parser takes is already understood.
|
|
2831
|
+
if (!recovering && !parsePrompt(prompt)) {
|
|
2832
|
+
const retry = recover(
|
|
2833
|
+
command,
|
|
2834
|
+
(candidate) =>
|
|
2835
|
+
parseExact(candidate, score) !== undefined ||
|
|
2836
|
+
// Window verbs get only the slash toggle, never a listing word.
|
|
2837
|
+
(candidate.startsWith("/") &&
|
|
2838
|
+
SLASH_HANDLED.has(verbOf(candidate)) &&
|
|
2839
|
+
candidate.split(/\s+/).length === command.trim().split(/\s+/).length),
|
|
2840
|
+
);
|
|
2841
|
+
if (retry !== undefined) {
|
|
2842
|
+
recovering = true;
|
|
2843
|
+
try {
|
|
2844
|
+
return await submit(retry);
|
|
2845
|
+
} finally {
|
|
2846
|
+
recovering = false;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
// Bare `rename`, `fork`, `resume`, `login` take free text: a hint only.
|
|
2851
|
+
const freeText = FREE_TEXT_HINTS[command.toLowerCase()];
|
|
2852
|
+
if (freeText !== undefined) return note(freeText);
|
|
2853
|
+
const elsewhere = elsewhereHint(command);
|
|
2854
|
+
if (elsewhere !== undefined) return note(elsewhere);
|
|
1980
2855
|
// `/model` belongs to its own handler above; every other slash word that
|
|
1981
2856
|
// reached here is unknown or misused, and never a question for the agent.
|
|
2857
|
+
// `/instrument aah` is `instrument aah`; `instrument sing choir` is
|
|
2858
|
+
// `instrument choir`.
|
|
2859
|
+
const singWord = command.match(/^\/?instrument\s+sing\s+([a-z]+)$/i);
|
|
2860
|
+
if (singWord && isSingWord(singWord[1]!.toLowerCase()))
|
|
2861
|
+
return submit(`instrument ${singWord[1]!.toLowerCase()}`);
|
|
2862
|
+
if (/^\/instrument\s/i.test(command) && parsePrompt(command.slice(1)))
|
|
2863
|
+
return submit(command.slice(1));
|
|
2864
|
+
// A synth or effect number out of range names its range.
|
|
2865
|
+
const range = paramRangeError(command);
|
|
2866
|
+
if (range) return fail(range);
|
|
1982
2867
|
if (command.startsWith("/") && !/^\/model\b/i.test(command)) {
|
|
1983
2868
|
const hint = usageHint(command);
|
|
1984
2869
|
return fail(
|
|
@@ -1993,6 +2878,10 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
1993
2878
|
// A one-letter slip on a command whose arguments parse stays local.
|
|
1994
2879
|
const fix = typoFix(command, parsesLocally);
|
|
1995
2880
|
if (fix) return fail(`${truncateForCard(command)} · did you mean ${fix}?`);
|
|
2881
|
+
// A grammar verb (not everyday English) never reaches the agent.
|
|
2882
|
+
const verb = verbOf(command);
|
|
2883
|
+
if (knownVerbs().has(verb) && !EVERYDAY_VERBS.has(verb))
|
|
2884
|
+
return fail(usageCard(truncateForCard(command), `/help ${verb}`));
|
|
1996
2885
|
if (process.env.DAWG_AI === "0")
|
|
1997
2886
|
return fail(`unrecognized · ${truncateForCard(command)} · /help`);
|
|
1998
2887
|
await materializeDraft();
|
|
@@ -2022,6 +2911,9 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
2022
2911
|
return parsed.action;
|
|
2023
2912
|
}
|
|
2024
2913
|
if (parsed.type === "set-tempo") {
|
|
2914
|
+
const range = RANGES.tempo!;
|
|
2915
|
+
if (parsed.tempoBpm < range.min || parsed.tempoBpm > range.max)
|
|
2916
|
+
return fail(usageError(truncateForCard(command), range));
|
|
2025
2917
|
const next = score.withTempo(parsed.tempoBpm);
|
|
2026
2918
|
await commitScore(next, "score.tempo", { tempoBpm: parsed.tempoBpm });
|
|
2027
2919
|
clock.follow(next);
|
|
@@ -2049,15 +2941,10 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
2049
2941
|
if (parsed.type === "track") {
|
|
2050
2942
|
// An unknown word would store and play a plain sine: say so instead.
|
|
2051
2943
|
const word = parsed.patch.instrument;
|
|
2052
|
-
if (word !== undefined && isUnknownInstrument(word))
|
|
2053
|
-
|
|
2054
|
-
const near = advice.match(/did you mean (\S+)\?/)?.[1];
|
|
2055
|
-
return fail(
|
|
2056
|
-
`instrument ${truncateForCard(word)} · not a dawg instrument${near ? ` · did you mean ${near}?` : ""} · /menu sounds lists them`,
|
|
2057
|
-
);
|
|
2058
|
-
}
|
|
2944
|
+
if (word !== undefined && isUnknownInstrument(word))
|
|
2945
|
+
return fail(unknownInstrumentMessage(word));
|
|
2059
2946
|
// `instrument jangle`: a guitar alias also loads its rig.
|
|
2060
|
-
const
|
|
2947
|
+
const rigged = parsed.word
|
|
2061
2948
|
? {
|
|
2062
2949
|
...parsed.patch,
|
|
2063
2950
|
...rigWordPatch(
|
|
@@ -2066,6 +2953,11 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
2066
2953
|
),
|
|
2067
2954
|
}
|
|
2068
2955
|
: parsed.patch;
|
|
2956
|
+
// `instrument vocal` (0.7): the vocal chain fills unset effects.
|
|
2957
|
+
const chain = isGuideInstrument(parsed.patch.instrument)
|
|
2958
|
+
? vocalChainPatch(score.tracks.find((t) => t.id === requestedTrack))
|
|
2959
|
+
: {};
|
|
2960
|
+
const patch = { ...chain, ...rigged };
|
|
2069
2961
|
const next = applyScoreOperation(score, {
|
|
2070
2962
|
type: "updateTrack",
|
|
2071
2963
|
trackId: requestedTrack,
|
|
@@ -2078,11 +2970,21 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
2078
2970
|
// Plain `marimba` keeps the legacy tone; point at the mallet engine.
|
|
2079
2971
|
if (parsed.patch.instrument === "marimba" && !("modal" in parsed.patch))
|
|
2080
2972
|
return `track · ${requestedTrack} · marimba (legacy tone) · modal marimba for the mallet engine`;
|
|
2973
|
+
// Plain `wind` keeps the legacy tone; point at the wind engine.
|
|
2974
|
+
if (parsed.patch.instrument === "wind" && !("wind" in parsed.patch))
|
|
2975
|
+
return `track · ${requestedTrack} · wind (legacy tone) · wind flute (or sax, trumpet …) for the wind engine`;
|
|
2081
2976
|
const sine =
|
|
2082
2977
|
word !== undefined && !("string" in parsed.patch)
|
|
2083
2978
|
? plainSineAdvice(word)
|
|
2084
2979
|
: undefined;
|
|
2085
2980
|
if (sine) return `track · ${requestedTrack} · ${sine}`;
|
|
2981
|
+
const added = [
|
|
2982
|
+
chain.filter ? "hpf 90 Hz" : "",
|
|
2983
|
+
chain.fx?.compressor ? "compressor 3:1" : "",
|
|
2984
|
+
chain.reverb ? "plate 0.14" : "",
|
|
2985
|
+
].filter(Boolean);
|
|
2986
|
+
if (added.length)
|
|
2987
|
+
return `track · ${requestedTrack} · vocal · added ${added.join(", ")} · fx … off to remove`;
|
|
2086
2988
|
return `track · ${requestedTrack}`;
|
|
2087
2989
|
}
|
|
2088
2990
|
if (parsed.type === "automation") {
|
|
@@ -2127,6 +3029,9 @@ async function submit(prompt: string): Promise<string | Receipt> {
|
|
|
2127
3029
|
return `cleared · ${requestedTrack}`;
|
|
2128
3030
|
}
|
|
2129
3031
|
if (parsed.type === "remove-note") {
|
|
3032
|
+
// A receipt never claims a removal that did not happen.
|
|
3033
|
+
if (!score.notes.some((note) => note.id === parsed.noteId))
|
|
3034
|
+
return fail(noNote(parsed.noteId));
|
|
2130
3035
|
const next = applyScoreOperation(score, {
|
|
2131
3036
|
type: "removeNote",
|
|
2132
3037
|
noteId: parsed.noteId,
|
|
@@ -2244,6 +3149,10 @@ async function focusTrack(trackId: string): Promise<Receipt> {
|
|
|
2244
3149
|
...newPianoTrack(trackId),
|
|
2245
3150
|
// `track vibes`: a 0.6 modal word names the track and its preset.
|
|
2246
3151
|
...(isModalWord(trackId) ? instrumentPatchForWord(trackId) : {}),
|
|
3152
|
+
// `track vocal` (0.7): a clip track with the vocal chain.
|
|
3153
|
+
...(isGuideInstrument(trackId)
|
|
3154
|
+
? { instrument: "vocal", ...vocalChainPatch(undefined) }
|
|
3155
|
+
: {}),
|
|
2247
3156
|
},
|
|
2248
3157
|
});
|
|
2249
3158
|
await commitScore(next, "track.create", { trackId });
|
|
@@ -2375,6 +3284,36 @@ async function fitCommand(
|
|
|
2375
3284
|
);
|
|
2376
3285
|
}
|
|
2377
3286
|
|
|
3287
|
+
async function resampleCommand(
|
|
3288
|
+
command: NonNullable<ReturnType<typeof parseResampleCommand>>,
|
|
3289
|
+
): Promise<Receipt> {
|
|
3290
|
+
await materializeDraft();
|
|
3291
|
+
// The source's own samples must be loaded for the render.
|
|
3292
|
+
if (hasSamplerTracks(score)) await sampleProblems(score);
|
|
3293
|
+
const result = await runResample({
|
|
3294
|
+
projectRoot: process.cwd(),
|
|
3295
|
+
score,
|
|
3296
|
+
command,
|
|
3297
|
+
...(liveSampleBank ? { samples: liveSampleBank } : {}),
|
|
3298
|
+
});
|
|
3299
|
+
if (!result.ok) return fail(result.message);
|
|
3300
|
+
const problems = (await sampleProblems(result.next)).filter(
|
|
3301
|
+
(problem) =>
|
|
3302
|
+
problem.trackId === result.trackId && problem.level === "error",
|
|
3303
|
+
);
|
|
3304
|
+
if (problems.length > 0) return fail(`resample · ${problems[0]!.message}`);
|
|
3305
|
+
await commitScore(result.next, "resample", {
|
|
3306
|
+
trackId: result.trackId,
|
|
3307
|
+
src: result.src,
|
|
3308
|
+
sha256: result.sha256,
|
|
3309
|
+
});
|
|
3310
|
+
await port.focus(result.trackId);
|
|
3311
|
+
requestedTrack = result.trackId;
|
|
3312
|
+
draftTrack = false;
|
|
3313
|
+
await projectSync?.flushScore();
|
|
3314
|
+
return ok(result.message);
|
|
3315
|
+
}
|
|
3316
|
+
|
|
2378
3317
|
async function sampleCommand(
|
|
2379
3318
|
command: NonNullable<ReturnType<typeof parseSampleCommand>>,
|
|
2380
3319
|
): Promise<Receipt> {
|
|
@@ -2514,11 +3453,19 @@ async function packCommand(command: PackCommand): Promise<Receipt> {
|
|
|
2514
3453
|
const store = packs();
|
|
2515
3454
|
const a = await store.pruneCache(to(store.maxFileCacheBytes));
|
|
2516
3455
|
const b = await library.pruneCache(to(library.maxCacheBytes));
|
|
2517
|
-
|
|
3456
|
+
const c = await pruneAnalysisCache(
|
|
3457
|
+
analysisDir(process.cwd()),
|
|
3458
|
+
to(ANALYSIS_CACHE_BYTES),
|
|
3459
|
+
);
|
|
3460
|
+
freed = {
|
|
3461
|
+
files: a.removed + b.removed + c.removed,
|
|
3462
|
+
bytes: a.freed + b.freed + c.freed,
|
|
3463
|
+
};
|
|
2518
3464
|
}
|
|
2519
3465
|
const lines = cacheLines({
|
|
2520
3466
|
packs: await packs().cacheStatus(),
|
|
2521
3467
|
assets: await library.cacheStatus(),
|
|
3468
|
+
analysis: await analysisCacheStatus(process.cwd()),
|
|
2522
3469
|
...(freed ? { freed } : {}),
|
|
2523
3470
|
});
|
|
2524
3471
|
tui.openText("cache", lines);
|
|
@@ -2593,27 +3540,38 @@ async function packCommand(command: PackCommand): Promise<Receipt> {
|
|
|
2593
3540
|
}
|
|
2594
3541
|
}
|
|
2595
3542
|
|
|
3543
|
+
/**
|
|
3544
|
+
* A command result as a receipt: a refusal is ✗, a change is ✓, and a read
|
|
3545
|
+
* that changed nothing (a listing or a show) is •.
|
|
3546
|
+
*/
|
|
3547
|
+
function readOrDone(
|
|
3548
|
+
result: Readonly<{ ok: boolean; message: string; next?: unknown }>,
|
|
3549
|
+
): Receipt {
|
|
3550
|
+
if (!result.ok) return fail(result.message);
|
|
3551
|
+
return result.next ? ok(result.message) : note(result.message);
|
|
3552
|
+
}
|
|
3553
|
+
|
|
2596
3554
|
async function patternCommand(command: PatternCommand): Promise<Receipt> {
|
|
2597
3555
|
if (command.kind === "list") {
|
|
2598
|
-
tui.openText("
|
|
2599
|
-
return
|
|
3556
|
+
tui.openText("grooves", DRUM_PATTERNS.map(patternLine));
|
|
3557
|
+
return note(`grooves · ${DRUM_PATTERNS.length} · groove <name>`);
|
|
2600
3558
|
}
|
|
2601
3559
|
if (command.kind === "browse") {
|
|
2602
3560
|
tui.openPicker({
|
|
2603
3561
|
id: "pattern",
|
|
2604
|
-
title: "
|
|
3562
|
+
title: "grooves",
|
|
2605
3563
|
hint: HINTS.audition,
|
|
2606
3564
|
audition: true,
|
|
2607
3565
|
items: DRUM_PATTERNS.map((entry) => ({
|
|
2608
3566
|
label: `${entry.label.padEnd(22)} ${entry.tempo.bpm} BPM · ${entry.tags.join(", ")}`,
|
|
2609
|
-
value: `/
|
|
3567
|
+
value: `/groove ${entry.name}`,
|
|
2610
3568
|
})),
|
|
2611
3569
|
filterable: true,
|
|
2612
3570
|
index: 0,
|
|
2613
3571
|
});
|
|
2614
3572
|
patternPreview = undefined;
|
|
2615
3573
|
previewPattern(DRUM_PATTERNS[0]?.name);
|
|
2616
|
-
return
|
|
3574
|
+
return note("grooves · ↑/↓ preview · Enter applies on the focused track");
|
|
2617
3575
|
}
|
|
2618
3576
|
await materializeDraft();
|
|
2619
3577
|
const result = applyDrumPattern(
|
|
@@ -2674,7 +3632,7 @@ function openKitPicker(): Receipt {
|
|
|
2674
3632
|
}));
|
|
2675
3633
|
tui.openPicker({
|
|
2676
3634
|
id: "kit",
|
|
2677
|
-
title: "
|
|
3635
|
+
title: "kits",
|
|
2678
3636
|
hint: HINTS.audition,
|
|
2679
3637
|
audition: true,
|
|
2680
3638
|
items,
|
|
@@ -2706,7 +3664,7 @@ async function kitCommand(command: KitCommand, bare = false): Promise<Receipt> {
|
|
|
2706
3664
|
.map((entry) => `${entry.name} · synth · ${entry.detail}`);
|
|
2707
3665
|
lines.push(...kitListLines(await packs().bankAliases(ALIASED_PACK)));
|
|
2708
3666
|
tui.openText("kits", lines);
|
|
2709
|
-
return
|
|
3667
|
+
return note("kits · /kit <name or nickname> on the focused drum track");
|
|
2710
3668
|
}
|
|
2711
3669
|
await materializeDraft();
|
|
2712
3670
|
const trackId = kitTarget(score, requestedTrack);
|
|
@@ -2817,12 +3775,10 @@ function adoptMeta(meta: typeof record.meta): void {
|
|
|
2817
3775
|
record = { ...record, meta };
|
|
2818
3776
|
if (meta.name !== announcedName) {
|
|
2819
3777
|
announcedName = meta.name;
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
{ tone: "info" },
|
|
2825
|
-
);
|
|
3778
|
+
// The auto-name joins the receipt that earned it, as a quiet suffix.
|
|
3779
|
+
if (meta.nameSource === "auto")
|
|
3780
|
+
tui.activity.attachNote(`named “${meta.name}” · /rename`);
|
|
3781
|
+
else tui.activity.pushCard(`session · ${meta.name}`, { tone: "info" });
|
|
2826
3782
|
}
|
|
2827
3783
|
}
|
|
2828
3784
|
|
|
@@ -2986,11 +3942,13 @@ function menuContext(): MenuContext {
|
|
|
2986
3942
|
committedChords: chordSettings,
|
|
2987
3943
|
},
|
|
2988
3944
|
trackId: requestedTrack,
|
|
3945
|
+
sessionName: record.meta.name,
|
|
2989
3946
|
playing: clock.playing,
|
|
2990
3947
|
grid: session?.grid ?? DEFAULT_GRID,
|
|
2991
3948
|
grids: GRIDS.map((grid) => grid.label),
|
|
2992
3949
|
clickOn: session?.clickOn ?? false,
|
|
2993
3950
|
countInBars: session?.countInBars ?? 1,
|
|
3951
|
+
showMe: showMe.level,
|
|
2994
3952
|
chords: stagedChordSettings() ?? session?.chords.settings ?? chordSettings,
|
|
2995
3953
|
projectRoot: process.cwd(),
|
|
2996
3954
|
};
|
|
@@ -3167,7 +4125,11 @@ function stagedChordSettings(): ChordSettings | undefined {
|
|
|
3167
4125
|
if (chords.length === 0) return undefined;
|
|
3168
4126
|
const settings = { ...chordSettings };
|
|
3169
4127
|
for (const command of chords)
|
|
3170
|
-
applyChordsCommand(
|
|
4128
|
+
applyChordsCommand(
|
|
4129
|
+
settings,
|
|
4130
|
+
command.match(CHORDS_COMMAND)![1]!,
|
|
4131
|
+
score.tempoBpm,
|
|
4132
|
+
);
|
|
3171
4133
|
return settings;
|
|
3172
4134
|
}
|
|
3173
4135
|
|
|
@@ -3224,7 +4186,11 @@ async function applyStaged(
|
|
|
3224
4186
|
const chords = command.trim().match(CHORDS_COMMAND);
|
|
3225
4187
|
if (chords) {
|
|
3226
4188
|
// Window state: checked on a copy; kept settings apply on Enter.
|
|
3227
|
-
const result = applyChordsCommand(
|
|
4189
|
+
const result = applyChordsCommand(
|
|
4190
|
+
{ ...chordSettings },
|
|
4191
|
+
chords[1]!,
|
|
4192
|
+
base.tempoBpm,
|
|
4193
|
+
);
|
|
3228
4194
|
return { next: base, ok: result.ok, message: result.message };
|
|
3229
4195
|
}
|
|
3230
4196
|
await prefetchStaged(base, command);
|
|
@@ -3272,7 +4238,7 @@ async function startAuditionLoop(): Promise<void> {
|
|
|
3272
4238
|
loop.focus(requestedTrack);
|
|
3273
4239
|
// One sound at a time: the song stops while the loop plays.
|
|
3274
4240
|
if (clock.playing) await setTransport("pause");
|
|
3275
|
-
previewEngine().setLeadMs(
|
|
4241
|
+
previewEngine().setLeadMs(previewEngine().playLeadMs);
|
|
3276
4242
|
loop.start();
|
|
3277
4243
|
}
|
|
3278
4244
|
|
|
@@ -3417,7 +4383,11 @@ async function keepStaged(): Promise<Receipt> {
|
|
|
3417
4383
|
CHORDS_COMMAND.test(command),
|
|
3418
4384
|
);
|
|
3419
4385
|
for (const command of chordCommands)
|
|
3420
|
-
applyChordsCommand(
|
|
4386
|
+
applyChordsCommand(
|
|
4387
|
+
chordSettings,
|
|
4388
|
+
command.match(CHORDS_COMMAND)![1]!,
|
|
4389
|
+
score.tempoBpm,
|
|
4390
|
+
);
|
|
3421
4391
|
const scoreCommands = taken.commands.filter(
|
|
3422
4392
|
(command) => !CHORDS_COMMAND.test(command),
|
|
3423
4393
|
);
|
|
@@ -3484,12 +4454,17 @@ function refreshMenu(): void {
|
|
|
3484
4454
|
if (fader) {
|
|
3485
4455
|
const fields = menu.faderFields(context);
|
|
3486
4456
|
if (fields.length === 0) closeFader();
|
|
3487
|
-
else
|
|
4457
|
+
else {
|
|
4458
|
+
const focusedField = fields[focusIndex(fader, fields)];
|
|
3488
4459
|
tui.drawer = drawerView(fader, fields, menu.faderCommitted(context), {
|
|
3489
4460
|
title: menu.crumbs,
|
|
3490
4461
|
dirty: context.audition?.dirty ?? false,
|
|
3491
4462
|
status: context.audition?.status,
|
|
4463
|
+
atOnce:
|
|
4464
|
+
focusedField !== undefined &&
|
|
4465
|
+
!stageableNow(faderCommand(focusedField)),
|
|
3492
4466
|
});
|
|
4467
|
+
}
|
|
3493
4468
|
}
|
|
3494
4469
|
const view = menu.view(context);
|
|
3495
4470
|
tui.openPicker({
|
|
@@ -3507,6 +4482,73 @@ function refreshMenu(): void {
|
|
|
3507
4482
|
// ── the fader drawer ─────────────────────────────────────────────────
|
|
3508
4483
|
|
|
3509
4484
|
/** Enter on a number row (or a bare `volume`, `fx filter`): the drawer. */
|
|
4485
|
+
/**
|
|
4486
|
+
* `export song.wav [stems]`: render the song as `dawg render song.wav` would
|
|
4487
|
+
* (the offline renderer, deterministic), then with `stems` one WAV per
|
|
4488
|
+
* audible track beside it (`song-bass.wav`), each that track soloed.
|
|
4489
|
+
*/
|
|
4490
|
+
async function exportWav(path: string, stems: boolean): Promise<Receipt> {
|
|
4491
|
+
const directory = await mkdtemp(join(tmpdir(), "dawg-export-"));
|
|
4492
|
+
const errors: string[] = [];
|
|
4493
|
+
const sink = { write: (text: string) => errors.push(text.trim()) };
|
|
4494
|
+
const quiet = { write: () => undefined };
|
|
4495
|
+
const render = async (song: TrackScore, target: string): Promise<boolean> => {
|
|
4496
|
+
const file = join(directory, "song.track.json");
|
|
4497
|
+
await writeFile(file, encodeLoop(song), "utf8");
|
|
4498
|
+
const code = await runRenderCommand(
|
|
4499
|
+
["render", target, "--import", file],
|
|
4500
|
+
process.cwd(),
|
|
4501
|
+
quiet,
|
|
4502
|
+
sink,
|
|
4503
|
+
);
|
|
4504
|
+
return code === 0;
|
|
4505
|
+
};
|
|
4506
|
+
tui.activity.setSpinner(`export ${path}`);
|
|
4507
|
+
try {
|
|
4508
|
+
if (!(await render(score, path)))
|
|
4509
|
+
return fail(errors.at(-1) ?? `export failed · ${path}`);
|
|
4510
|
+
if (!stems) return ok(`exported · ${path}`);
|
|
4511
|
+
const written: string[] = [];
|
|
4512
|
+
const base = path.replace(/\.wav$/i, "");
|
|
4513
|
+
for (const track of score.tracks) {
|
|
4514
|
+
if (!isTrackAudible(score, track.id)) continue;
|
|
4515
|
+
let solo = score;
|
|
4516
|
+
for (const other of score.tracks)
|
|
4517
|
+
solo = applyScoreOperation(solo, {
|
|
4518
|
+
type: "updateTrack",
|
|
4519
|
+
trackId: other.id,
|
|
4520
|
+
patch: { solo: other.id === track.id },
|
|
4521
|
+
});
|
|
4522
|
+
const target = `${base}-${track.id}.wav`;
|
|
4523
|
+
if (!(await render(solo, target)))
|
|
4524
|
+
return fail(errors.at(-1) ?? `export failed · ${target}`);
|
|
4525
|
+
written.push(target);
|
|
4526
|
+
}
|
|
4527
|
+
return ok(stemsReceipt(path, written));
|
|
4528
|
+
} finally {
|
|
4529
|
+
tui.activity.setSpinner(undefined);
|
|
4530
|
+
await rm(directory, { recursive: true, force: true });
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
/** `tempo`, `bars`, `meter`: open Project and name its fader, if it has one. */
|
|
4535
|
+
function songFader(context: MenuContext, command: string): string | undefined {
|
|
4536
|
+
// Bare song scalars and the Project fader each opens.
|
|
4537
|
+
const faders: Readonly<Record<string, string>> = {
|
|
4538
|
+
tempo: "tempo",
|
|
4539
|
+
bpm: "tempo",
|
|
4540
|
+
bars: "loop length",
|
|
4541
|
+
meter: "beats per bar",
|
|
4542
|
+
};
|
|
4543
|
+
const label = faders[command.replace(/^\//, "").toLowerCase()];
|
|
4544
|
+
if (!label) return undefined;
|
|
4545
|
+
menu.show(context, "project");
|
|
4546
|
+
if (menu.faderFields(context).some((field) => field.label === label))
|
|
4547
|
+
return label;
|
|
4548
|
+
menu.close();
|
|
4549
|
+
return undefined;
|
|
4550
|
+
}
|
|
4551
|
+
|
|
3510
4552
|
/**
|
|
3511
4553
|
* Open the drawer on `label`. `standalone` drawers came from a command or a
|
|
3512
4554
|
* click, not from inside the menu, so closing one closes the menu too.
|
|
@@ -3609,7 +4651,7 @@ function mouseInput(event: MouseEvent): string[] {
|
|
|
3609
4651
|
return [];
|
|
3610
4652
|
}
|
|
3611
4653
|
if (fader) return [];
|
|
3612
|
-
if (tui.ui.overlay) return [event.delta < 0 ?
|
|
4654
|
+
if (tui.ui.overlay) return [event.delta < 0 ? ARROW_UP : ARROW_DOWN];
|
|
3613
4655
|
return [];
|
|
3614
4656
|
}
|
|
3615
4657
|
// A drag keeps moving the fader it started on, even past the bar's ends.
|
|
@@ -3699,7 +4741,9 @@ function mouseInput(event: MouseEvent): string[] {
|
|
|
3699
4741
|
}
|
|
3700
4742
|
case "transport":
|
|
3701
4743
|
if (tui.ui.overlay) return [];
|
|
3702
|
-
void toggleTransport()
|
|
4744
|
+
void toggleTransport()
|
|
4745
|
+
.catch((error: unknown) => transportFailed(error))
|
|
4746
|
+
.finally(() => requestFrame());
|
|
3703
4747
|
return [];
|
|
3704
4748
|
case "tracks":
|
|
3705
4749
|
if (tui.ui.overlay) return [];
|
|
@@ -3730,13 +4774,13 @@ function playSession(): PlaySession {
|
|
|
3730
4774
|
}
|
|
3731
4775
|
|
|
3732
4776
|
async function enterPlay(): Promise<Receipt> {
|
|
3733
|
-
if (play?.on) return ok(`play · ${play.track} · esc leaves`);
|
|
4777
|
+
if (play?.on) return ok(`play mode · ${play.track} · esc leaves`);
|
|
3734
4778
|
await materializeDraft();
|
|
3735
4779
|
const session = playSession();
|
|
3736
4780
|
await session.enter();
|
|
3737
4781
|
if (hasSamplerTracks(score)) void sampleProblems(score);
|
|
3738
4782
|
return ok(
|
|
3739
|
-
`play · ${session.track} · ${session.keyboard.range} · ? keys · esc leaves`,
|
|
4783
|
+
`play mode · ${session.track} · ${session.layout.drums ? "kit" : session.keyboard.range} · ? keys · esc leaves`,
|
|
3740
4784
|
);
|
|
3741
4785
|
}
|
|
3742
4786
|
|
|
@@ -3766,9 +4810,7 @@ function playKey(value: string): boolean {
|
|
|
3766
4810
|
else if (result.command === "transport") {
|
|
3767
4811
|
if (!session.startWithCountIn())
|
|
3768
4812
|
void toggleTransport().catch((error: unknown) =>
|
|
3769
|
-
|
|
3770
|
-
`transport failed · ${error instanceof Error ? error.message : String(error)}`,
|
|
3771
|
-
),
|
|
4813
|
+
transportFailed(error),
|
|
3772
4814
|
);
|
|
3773
4815
|
} else if (result.command === "menu") return false;
|
|
3774
4816
|
return true;
|
|
@@ -3830,13 +4872,18 @@ function playHost() {
|
|
|
3830
4872
|
async stopTransport(): Promise<void> {
|
|
3831
4873
|
await setTransport("pause");
|
|
3832
4874
|
},
|
|
4875
|
+
ghost(pitch: number) {
|
|
4876
|
+
tui.delight.ghost(pitch, Date.now());
|
|
4877
|
+
requestFrame();
|
|
4878
|
+
},
|
|
3833
4879
|
card(text: string, tone: "info" | "success" | "warning" | "error") {
|
|
3834
4880
|
if (tone === "error") tui.activity.pushError(text);
|
|
3835
4881
|
else
|
|
3836
4882
|
tui.activity.pushCard(text, {
|
|
3837
4883
|
tone,
|
|
3838
4884
|
trackId: requestedTrack,
|
|
3839
|
-
resultRevision:
|
|
4885
|
+
resultRevision:
|
|
4886
|
+
tone === "success" ? shownRevision(record) : undefined,
|
|
3840
4887
|
});
|
|
3841
4888
|
},
|
|
3842
4889
|
newNoteId: () => randomUUID().slice(0, 12),
|
|
@@ -3883,6 +4930,9 @@ function timingChanged(previous: TrackScore, next: TrackScore): boolean {
|
|
|
3883
4930
|
function syncHost(): SyncHost {
|
|
3884
4931
|
return {
|
|
3885
4932
|
project: process.cwd(),
|
|
4933
|
+
// The files belong to one session at a time; another session's window
|
|
4934
|
+
// stays detached instead of reprinting over them.
|
|
4935
|
+
sessionId: () => record.sessionId,
|
|
3886
4936
|
current: () => score,
|
|
3887
4937
|
async commit(plan, summary) {
|
|
3888
4938
|
const baseRevision = record.revision;
|
|
@@ -3908,7 +4958,8 @@ function syncHost(): SyncHost {
|
|
|
3908
4958
|
tui.activity.pushCard(text, {
|
|
3909
4959
|
tone,
|
|
3910
4960
|
hint,
|
|
3911
|
-
resultRevision:
|
|
4961
|
+
resultRevision:
|
|
4962
|
+
tone === "success" ? shownRevision(record) : undefined,
|
|
3912
4963
|
});
|
|
3913
4964
|
},
|
|
3914
4965
|
types(state) {
|
|
@@ -3939,10 +4990,17 @@ async function stepHistory(direction: "undo" | "redo"): Promise<Receipt> {
|
|
|
3939
4990
|
},
|
|
3940
4991
|
restored.toJSON(),
|
|
3941
4992
|
);
|
|
4993
|
+
// Compare with what was undone, which another window may have written.
|
|
4994
|
+
const before = scoreFromJSON(latest.composition);
|
|
3942
4995
|
score = restored;
|
|
3943
4996
|
if (clock.playing) void audio.play(score);
|
|
3944
4997
|
return ok(
|
|
3945
|
-
|
|
4998
|
+
historyReceipt(
|
|
4999
|
+
direction,
|
|
5000
|
+
before,
|
|
5001
|
+
restored,
|
|
5002
|
+
shownRevision(record, target.revision),
|
|
5003
|
+
),
|
|
3946
5004
|
);
|
|
3947
5005
|
} catch (error) {
|
|
3948
5006
|
if (error instanceof SessionConflictError)
|
|
@@ -3953,7 +5011,36 @@ async function stepHistory(direction: "undo" | "redo"): Promise<Receipt> {
|
|
|
3953
5011
|
}
|
|
3954
5012
|
}
|
|
3955
5013
|
|
|
3956
|
-
/**
|
|
5014
|
+
/** Space, the menu and a header click all report a failed toggle the same way. */
|
|
5015
|
+
function transportFailed(error: unknown): void {
|
|
5016
|
+
tui.activity.pushCard(
|
|
5017
|
+
`transport failed · ${error instanceof Error ? error.message : String(error)}`,
|
|
5018
|
+
{ tone: "error" },
|
|
5019
|
+
);
|
|
5020
|
+
}
|
|
5021
|
+
|
|
5022
|
+
/**
|
|
5023
|
+
* The revision dawg shows (header, receipts, undo): edits to the song.
|
|
5024
|
+
* Play and pause are logged so other windows follow them, but they are not
|
|
5025
|
+
* edits, so they never move the number: a revision shows as itself minus
|
|
5026
|
+
* the transport events at or before it.
|
|
5027
|
+
*/
|
|
5028
|
+
function shownRevision(
|
|
5029
|
+
value: typeof record,
|
|
5030
|
+
revision = value.revision,
|
|
5031
|
+
): number {
|
|
5032
|
+
let transports = 0;
|
|
5033
|
+
for (const event of value.events)
|
|
5034
|
+
if (event.revision <= revision && event.kind === "transport")
|
|
5035
|
+
transports += 1;
|
|
5036
|
+
return revision - transports;
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
/**
|
|
5040
|
+
* The space-bar toggle: flip the transport, then record it for other
|
|
5041
|
+
* windows. The log entry keeps windows in step; the header's revision
|
|
5042
|
+
* (`shownRevision`) skips it, since play/pause is not an edit.
|
|
5043
|
+
*/
|
|
3957
5044
|
async function toggleTransport(): Promise<void> {
|
|
3958
5045
|
await setTransport("toggle");
|
|
3959
5046
|
try {
|
|
@@ -4023,11 +5110,11 @@ async function runAgent(text: string): Promise<string | Receipt> {
|
|
|
4023
5110
|
if (agentTurn) return warn("agent busy · Esc cancels");
|
|
4024
5111
|
const selection = await currentProvider();
|
|
4025
5112
|
if (selection.kind === "offline")
|
|
4026
|
-
return fail(
|
|
4027
|
-
`unrecognized · ${truncateForCard(text)} · ${selection.reason}`,
|
|
4028
|
-
);
|
|
5113
|
+
return fail(`${truncateForCard(text)} · ${NO_AGENT}`);
|
|
4029
5114
|
const turn = { controller: new AbortController(), steering: [] as string[] };
|
|
4030
5115
|
agentTurn = turn;
|
|
5116
|
+
// The turn ends on one musical receipt of what it changed.
|
|
5117
|
+
const before = score;
|
|
4031
5118
|
reportAgentActivity(`${providerName} · thinking…`);
|
|
4032
5119
|
// xcb admits a pending account on its first call, which takes longer.
|
|
4033
5120
|
let admitting = selection.kind === "xcb" && selection.admissionPending;
|
|
@@ -4060,6 +5147,8 @@ async function runAgent(text: string): Promise<string | Receipt> {
|
|
|
4060
5147
|
}
|
|
4061
5148
|
return;
|
|
4062
5149
|
}
|
|
5150
|
+
if (event.type === "done")
|
|
5151
|
+
tui.activity.setTurnReceipt(musicalReceipt(before, score));
|
|
4063
5152
|
agentEventSink(event);
|
|
4064
5153
|
},
|
|
4065
5154
|
});
|
|
@@ -4091,7 +5180,7 @@ function currentProvider(): Promise<ProviderSelection> {
|
|
|
4091
5180
|
provider = selectProvider().catch((): ProviderSelection => ({
|
|
4092
5181
|
kind: "offline",
|
|
4093
5182
|
choice: "auto",
|
|
4094
|
-
reason: "provider unavailable; run `dawg
|
|
5183
|
+
reason: "provider unavailable; run `dawg model key`",
|
|
4095
5184
|
}));
|
|
4096
5185
|
}
|
|
4097
5186
|
return provider.then((selection) => {
|
|
@@ -4103,9 +5192,9 @@ function currentProvider(): Promise<ProviderSelection> {
|
|
|
4103
5192
|
!invalidNoticeShown
|
|
4104
5193
|
) {
|
|
4105
5194
|
invalidNoticeShown = true;
|
|
4106
|
-
tui.activity.pushCard(`
|
|
5195
|
+
tui.activity.pushCard(`agent key stopped working · ${selection.reason}`, {
|
|
4107
5196
|
tone: "warning",
|
|
4108
|
-
hint: "/
|
|
5197
|
+
hint: "/model key",
|
|
4109
5198
|
});
|
|
4110
5199
|
}
|
|
4111
5200
|
providerStamp.offline = selection.kind === "offline";
|
|
@@ -4180,6 +5269,180 @@ function agentPreviewHost(): PreviewHost {
|
|
|
4180
5269
|
};
|
|
4181
5270
|
}
|
|
4182
5271
|
|
|
5272
|
+
// ── show-me ──────────────────────────────────────────────────────────
|
|
5273
|
+
|
|
5274
|
+
/**
|
|
5275
|
+
* Show-me (docs/show-me.md): with an API provider the agent writes prompt
|
|
5276
|
+
* commands, streamed. The line being written is ghost text in the empty
|
|
5277
|
+
* prompt bar at the model's own speed; each complete line runs through
|
|
5278
|
+
* `submit()`, the path a typed Enter takes, at once. A parameter value
|
|
5279
|
+
* glides there over ~150 ms while the loop plays (you hear it), notes and
|
|
5280
|
+
* hits sound as they arrive (in time when the stream is ahead of the tempo,
|
|
5281
|
+
* as step entry when it is behind), and the caption names the key or fader
|
|
5282
|
+
* a person would use. Nothing waits for the turn to end.
|
|
5283
|
+
*/
|
|
5284
|
+
|
|
5285
|
+
function showMeTyping(text: string): void {
|
|
5286
|
+
showMe.ghost = text || undefined;
|
|
5287
|
+
if (text && showMe.level === "on") showMe.caption = undefined;
|
|
5288
|
+
requestFrame();
|
|
5289
|
+
}
|
|
5290
|
+
|
|
5291
|
+
function showMeCaption(text: string | undefined): void {
|
|
5292
|
+
if (showMe.level !== "on") return;
|
|
5293
|
+
showMe.caption = text;
|
|
5294
|
+
if (showMe.clear) clearTimeout(showMe.clear);
|
|
5295
|
+
showMe.clear = undefined;
|
|
5296
|
+
requestFrame();
|
|
5297
|
+
}
|
|
5298
|
+
|
|
5299
|
+
/** End of a turn: the ghost goes, the caption becomes the do-it-yourself hint. */
|
|
5300
|
+
function showMeFinish(): void {
|
|
5301
|
+
showMe.ghost = undefined;
|
|
5302
|
+
showMe.notes.reset();
|
|
5303
|
+
const hint = finishHint(showMe.commands);
|
|
5304
|
+
showMe.commands = [];
|
|
5305
|
+
if (hint && showMe.level === "on") {
|
|
5306
|
+
showMeCaption(hint);
|
|
5307
|
+
showMe.clear = setTimeout(() => {
|
|
5308
|
+
showMe.caption = undefined;
|
|
5309
|
+
showMe.clear = undefined;
|
|
5310
|
+
requestFrame();
|
|
5311
|
+
}, 8_000);
|
|
5312
|
+
} else {
|
|
5313
|
+
showMe.caption = undefined;
|
|
5314
|
+
requestFrame();
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
|
|
5318
|
+
/**
|
|
5319
|
+
* Slide a fader to its new value the way the drawer does: each eased step is
|
|
5320
|
+
* the field's own command, staged on a scratch score and played, so the loop
|
|
5321
|
+
* moves through the values. Only while the loop plays (otherwise nothing is
|
|
5322
|
+
* heard and the glide would only cost time); never longer than ~150 ms.
|
|
5323
|
+
*/
|
|
5324
|
+
async function glideFader(param: string, target: number): Promise<void> {
|
|
5325
|
+
if (!clock.playing || !stdout.isTTY) return;
|
|
5326
|
+
const scratch = new EditMenu();
|
|
5327
|
+
const context = { ...menuContext(), score };
|
|
5328
|
+
const label = scratch.showFader(context, param);
|
|
5329
|
+
if (!label) return;
|
|
5330
|
+
const field = scratch
|
|
5331
|
+
.faderFields(context)
|
|
5332
|
+
.find((candidate) => candidate.label === label);
|
|
5333
|
+
scratch.close();
|
|
5334
|
+
if (field?.kind !== "number") return;
|
|
5335
|
+
const from = field.value ?? field.start ?? target;
|
|
5336
|
+
const steps = glideValues(from, target).slice(0, -1);
|
|
5337
|
+
for (const value of steps) {
|
|
5338
|
+
const staged = await applyStaged(score, field.command(value));
|
|
5339
|
+
if (staged.next && clock.playing) void audio.play(staged.next);
|
|
5340
|
+
await Bun.sleep(GLIDE_STEP_MS);
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
5343
|
+
|
|
5344
|
+
/** Sound one streamed note on the audition voice (the loop is stopped). */
|
|
5345
|
+
function soundStreamedNote(
|
|
5346
|
+
value: TrackScore,
|
|
5347
|
+
trackId: string,
|
|
5348
|
+
pitch: number,
|
|
5349
|
+
startBeat: number,
|
|
5350
|
+
): void {
|
|
5351
|
+
if (clock.playing) return; // The loop itself plays it.
|
|
5352
|
+
const engine = liveEngine();
|
|
5353
|
+
if (!engine?.canMonitor) return;
|
|
5354
|
+
const tick = Math.round(startBeat * value.ticksPerBeat);
|
|
5355
|
+
const note = value.notes.find(
|
|
5356
|
+
(candidate) =>
|
|
5357
|
+
candidate.trackId === trackId &&
|
|
5358
|
+
candidate.pitch === pitch &&
|
|
5359
|
+
candidate.startTick === tick,
|
|
5360
|
+
);
|
|
5361
|
+
if (!note) return;
|
|
5362
|
+
try {
|
|
5363
|
+
const json = value.toJSON() as unknown as Record<string, unknown>;
|
|
5364
|
+
const single = scoreFromJSON({
|
|
5365
|
+
...json,
|
|
5366
|
+
notes: [{ ...note, startTick: 0 }],
|
|
5367
|
+
});
|
|
5368
|
+
const pcm = renderAudition({
|
|
5369
|
+
score: single,
|
|
5370
|
+
trackId,
|
|
5371
|
+
sampleRate: engine.sampleRate,
|
|
5372
|
+
...(liveSampleBank ? { samples: liveSampleBank } : {}),
|
|
5373
|
+
});
|
|
5374
|
+
if (!pcm) return;
|
|
5375
|
+
void engine
|
|
5376
|
+
.monitor(true)
|
|
5377
|
+
.then(() => engine.noteOn(AUDITION_VOICE, pcm))
|
|
5378
|
+
.catch(() => undefined);
|
|
5379
|
+
} catch {
|
|
5380
|
+
// A note that cannot render is still in the score.
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
|
|
5384
|
+
/** The command host the show-me agent loop runs lines through. */
|
|
5385
|
+
function showMeCommandHost(): AgentHost["commands"] {
|
|
5386
|
+
return {
|
|
5387
|
+
isCommand: (line) =>
|
|
5388
|
+
isAgentCommand(line, score) || isBrokenCommand(line, score),
|
|
5389
|
+
async run(line): Promise<CommandOutcome> {
|
|
5390
|
+
if (!agentPathsAllowed(line)) {
|
|
5391
|
+
const revision = shownRevision(record);
|
|
5392
|
+
return {
|
|
5393
|
+
ok: false,
|
|
5394
|
+
message: `${line} · the agent works only inside this folder`,
|
|
5395
|
+
baseRevision: revision,
|
|
5396
|
+
resultRevision: revision,
|
|
5397
|
+
};
|
|
5398
|
+
}
|
|
5399
|
+
if (isBrokenCommand(line, score)) {
|
|
5400
|
+
// A command that does not parse is a red receipt, never prose.
|
|
5401
|
+
const base = baseline();
|
|
5402
|
+
receipt(fail(brokenCommandReceipt(line)), base);
|
|
5403
|
+
return {
|
|
5404
|
+
ok: false,
|
|
5405
|
+
message: brokenCommandReceipt(line),
|
|
5406
|
+
baseRevision: base.revision,
|
|
5407
|
+
resultRevision: base.revision,
|
|
5408
|
+
};
|
|
5409
|
+
}
|
|
5410
|
+
const gesture = gestureFor(line, { score, trackId: requestedTrack });
|
|
5411
|
+
showMeCaption(gesture.caption);
|
|
5412
|
+
showMe.commands.push(line);
|
|
5413
|
+
if (gesture.kind === "fader" && showMe.level === "on")
|
|
5414
|
+
await glideFader(gesture.param, gesture.value);
|
|
5415
|
+
const base = baseline();
|
|
5416
|
+
tui.activity.pushNote(`agent › ${line}`, "request");
|
|
5417
|
+
let result: string | Receipt;
|
|
5418
|
+
try {
|
|
5419
|
+
result = await submit(line);
|
|
5420
|
+
} catch (error) {
|
|
5421
|
+
result = fail(describeError(line, error));
|
|
5422
|
+
}
|
|
5423
|
+
receipt(result, base);
|
|
5424
|
+
if (gesture.kind === "keys" && showMe.level === "on") {
|
|
5425
|
+
const trackId = requestedTrack;
|
|
5426
|
+
const after = score;
|
|
5427
|
+
for (const note of gesture.notes) {
|
|
5428
|
+
const { atMs } = showMe.notes.schedule(note.start, performance.now());
|
|
5429
|
+
const delay = Math.max(0, atMs - performance.now());
|
|
5430
|
+
setTimeout(
|
|
5431
|
+
() => soundStreamedNote(after, trackId, note.pitch, note.start),
|
|
5432
|
+
delay,
|
|
5433
|
+
);
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5436
|
+
return {
|
|
5437
|
+
ok: toneOf(result) !== "error",
|
|
5438
|
+
message: typeof result === "string" ? result : result.text,
|
|
5439
|
+
baseRevision: shownRevision(record, base.revision),
|
|
5440
|
+
resultRevision: shownRevision(record),
|
|
5441
|
+
};
|
|
5442
|
+
},
|
|
5443
|
+
};
|
|
5444
|
+
}
|
|
5445
|
+
|
|
4183
5446
|
function agentHost(
|
|
4184
5447
|
turn: { steering: string[] },
|
|
4185
5448
|
selection: ProviderSelection,
|
|
@@ -4254,6 +5517,9 @@ function agentHost(
|
|
|
4254
5517
|
}
|
|
4255
5518
|
},
|
|
4256
5519
|
takeSteering: () => turn.steering.splice(0),
|
|
5520
|
+
...(showMe.level !== "off" && stdout.isTTY
|
|
5521
|
+
? { commands: showMeCommandHost() }
|
|
5522
|
+
: {}),
|
|
4257
5523
|
workspace: { root: process.cwd() },
|
|
4258
5524
|
// A written project source is applied before the tool result returns,
|
|
4259
5525
|
// so the model reads the outcome and any type errors in the same step.
|