@hraness/dawg 0.6.1 → 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 +92 -0
- package/DAWG.md +343 -116
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +832 -13
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +276 -3
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +1849 -42
- package/core/sections.ts +292 -26
- package/core/sing.ts +815 -0
- 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/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- 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 -13
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- 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 -13
- 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 +126 -14
- package/src/agent/calibration-tools.ts +53 -0
- 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 +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +12 -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 +4 -1
- package/src/agent/provider.ts +22 -8
- 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 +108 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +11 -7
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +37 -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 +10 -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/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +160 -28
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- 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 +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- 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 +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +558 -0
- package/src/commands/help.ts +610 -380
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +8 -8
- package/src/commands/modal.ts +1 -1
- 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/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sing.ts +478 -0
- package/src/commands/strum.ts +13 -1
- 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 +2 -2
- 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 +1397 -253
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- 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 +20 -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 +34 -4
- 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 +759 -177
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +47 -7
- 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 +3 -3
- 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/tui/activity.ts
CHANGED
|
@@ -23,6 +23,33 @@ export interface ActivityCard {
|
|
|
23
23
|
trackId?: string | undefined;
|
|
24
24
|
/** Key hint shown after the card, e.g. "u undo". */
|
|
25
25
|
hint?: string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* A one-time info card (a launch note, a tip): it leaves the strip after
|
|
28
|
+
* `ONCE_TTL_MS` or `ONCE_ACTIONS` later actions, whichever comes first.
|
|
29
|
+
*/
|
|
30
|
+
once?: boolean | undefined;
|
|
31
|
+
/** Repeats of the same text and tone, shown as `×N` (absent is 1). */
|
|
32
|
+
count?: number | undefined;
|
|
33
|
+
/** A quieter note joined to the card, e.g. the auto-name. */
|
|
34
|
+
suffix?: string | undefined;
|
|
35
|
+
/** `actions` when the card was pushed, for the `once` expiry. */
|
|
36
|
+
atAction?: number | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A `once` card's lifetime in milliseconds. */
|
|
40
|
+
export const ONCE_TTL_MS = 8_000;
|
|
41
|
+
/** Actions (receipts, requests) after which a `once` card leaves. */
|
|
42
|
+
export const ONCE_ACTIONS = 3;
|
|
43
|
+
/** Age after which an older error fades to the faint role. */
|
|
44
|
+
export const ERROR_FADE_MS = 6_000;
|
|
45
|
+
|
|
46
|
+
/** One strip line: newlines read as ` · `, runs of space collapse. */
|
|
47
|
+
export function stripLine(text: string): string {
|
|
48
|
+
return text
|
|
49
|
+
.split(/\s*\n\s*/)
|
|
50
|
+
.map((part) => part.replace(/[ \t]+/g, " ").trim())
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.join(" · ");
|
|
26
53
|
}
|
|
27
54
|
|
|
28
55
|
export type TranscriptKind =
|
|
@@ -134,7 +161,10 @@ export class ActivityFeed {
|
|
|
134
161
|
private spinnerLabel: string | undefined;
|
|
135
162
|
private spinnerSince = 0;
|
|
136
163
|
private streamText = "";
|
|
164
|
+
private turnReceipt = "";
|
|
137
165
|
private queue = 0;
|
|
166
|
+
/** Receipts and requests so far: the clock `once` cards expire on. */
|
|
167
|
+
private actions = 0;
|
|
138
168
|
private listeners = new Set<() => void>();
|
|
139
169
|
/** Increments on every change; renderers can skip work when unchanged. */
|
|
140
170
|
version = 0;
|
|
@@ -167,6 +197,36 @@ export class ActivityFeed {
|
|
|
167
197
|
return this.cardList[this.cardList.length - 1];
|
|
168
198
|
}
|
|
169
199
|
|
|
200
|
+
/**
|
|
201
|
+
* The cards the strip shows at `nowMs`, first slot first: the newest
|
|
202
|
+
* receipt leads, live `once` notes follow it, and expired `once` notes
|
|
203
|
+
* are gone.
|
|
204
|
+
*/
|
|
205
|
+
visible(nowMs: number = this.clock()): ActivityCard[] {
|
|
206
|
+
const live = this.cardList.filter(
|
|
207
|
+
(card) =>
|
|
208
|
+
!card.once ||
|
|
209
|
+
(nowMs - card.atMs < ONCE_TTL_MS &&
|
|
210
|
+
this.actions - (card.atAction ?? 0) < ONCE_ACTIONS),
|
|
211
|
+
);
|
|
212
|
+
const newest = [...live].reverse();
|
|
213
|
+
return [
|
|
214
|
+
...newest.filter((card) => !card.once),
|
|
215
|
+
...newest.filter((card) => card.once),
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The next time a `once` card expires on the clock, for frame wakeups. */
|
|
220
|
+
nextExpiry(nowMs: number = this.clock()): number | undefined {
|
|
221
|
+
let next: number | undefined;
|
|
222
|
+
for (const card of this.cardList) {
|
|
223
|
+
if (!card.once) continue;
|
|
224
|
+
const at = card.atMs + ONCE_TTL_MS;
|
|
225
|
+
if (at > nowMs && (next === undefined || at < next)) next = at;
|
|
226
|
+
}
|
|
227
|
+
return next;
|
|
228
|
+
}
|
|
229
|
+
|
|
170
230
|
subscribe(listener: () => void): () => void {
|
|
171
231
|
this.listeners.add(listener);
|
|
172
232
|
return () => this.listeners.delete(listener);
|
|
@@ -176,32 +236,85 @@ export class ActivityFeed {
|
|
|
176
236
|
text: string,
|
|
177
237
|
details: Omit<Partial<ActivityCard>, "id" | "atMs" | "text"> = {},
|
|
178
238
|
): ActivityCard {
|
|
239
|
+
const line = stripLine(text);
|
|
240
|
+
const once = details.once === true;
|
|
241
|
+
if (!once) this.actions += 1;
|
|
242
|
+
const last = this.cardList[this.cardList.length - 1];
|
|
243
|
+
const tone = details.tone ?? "info";
|
|
244
|
+
// The same message again reads `×N` instead of filling the strip.
|
|
245
|
+
if (
|
|
246
|
+
last &&
|
|
247
|
+
last.text === line &&
|
|
248
|
+
last.tone === tone &&
|
|
249
|
+
last.resultRevision === details.resultRevision
|
|
250
|
+
) {
|
|
251
|
+
const repeat: ActivityCard = {
|
|
252
|
+
...last,
|
|
253
|
+
count: (last.count ?? 1) + 1,
|
|
254
|
+
atMs: this.clock(),
|
|
255
|
+
atAction: this.actions,
|
|
256
|
+
};
|
|
257
|
+
this.cardList = [...this.cardList.slice(0, -1), repeat];
|
|
258
|
+
this.record(tone === "error" ? "error" : "op", line);
|
|
259
|
+
this.changed();
|
|
260
|
+
return repeat;
|
|
261
|
+
}
|
|
179
262
|
const card: ActivityCard = {
|
|
180
263
|
id: this.nextId++,
|
|
181
|
-
text,
|
|
182
|
-
tone
|
|
264
|
+
text: line,
|
|
265
|
+
tone,
|
|
183
266
|
atMs: this.clock(),
|
|
184
267
|
baseRevision: details.baseRevision,
|
|
185
268
|
resultRevision: details.resultRevision,
|
|
186
269
|
trackId: details.trackId,
|
|
187
270
|
hint: details.hint,
|
|
271
|
+
...(once ? { once: true } : {}),
|
|
272
|
+
...(details.suffix ? { suffix: stripLine(details.suffix) } : {}),
|
|
273
|
+
atAction: this.actions,
|
|
188
274
|
};
|
|
189
275
|
this.cardList = [...this.cardList, card].slice(-this.maxCards);
|
|
190
276
|
const revision = revisionLabel(card.baseRevision, card.resultRevision);
|
|
191
277
|
this.record(
|
|
192
278
|
card.tone === "error" ? "error" : "op",
|
|
193
|
-
revision ? `${
|
|
279
|
+
revision ? `${line} · ${revision}` : line,
|
|
194
280
|
);
|
|
195
281
|
this.changed();
|
|
196
282
|
return card;
|
|
197
283
|
}
|
|
198
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Join a quiet note to the newest receipt (`… · named "dusk loop"`), so
|
|
287
|
+
* it rides in the first slot instead of pushing the receipt aside. With
|
|
288
|
+
* no receipt in the last few seconds it becomes a `once` card.
|
|
289
|
+
*/
|
|
290
|
+
attachNote(text: string, withinMs = ONCE_TTL_MS): ActivityCard {
|
|
291
|
+
const line = stripLine(text);
|
|
292
|
+
const last = this.cardList[this.cardList.length - 1];
|
|
293
|
+
if (
|
|
294
|
+
last &&
|
|
295
|
+
!last.once &&
|
|
296
|
+
last.tone !== "error" &&
|
|
297
|
+
this.clock() - last.atMs < withinMs
|
|
298
|
+
) {
|
|
299
|
+
const joined: ActivityCard = {
|
|
300
|
+
...last,
|
|
301
|
+
suffix: last.suffix ? `${last.suffix} · ${line}` : line,
|
|
302
|
+
};
|
|
303
|
+
this.cardList = [...this.cardList.slice(0, -1), joined];
|
|
304
|
+
this.record("note", line);
|
|
305
|
+
this.changed();
|
|
306
|
+
return joined;
|
|
307
|
+
}
|
|
308
|
+
return this.pushCard(line, { tone: "info", once: true });
|
|
309
|
+
}
|
|
310
|
+
|
|
199
311
|
pushError(message: string): ActivityCard {
|
|
200
312
|
return this.pushCard(message, { tone: "error" });
|
|
201
313
|
}
|
|
202
314
|
|
|
203
315
|
/** Record a user request in the transcript without adding a strip card. */
|
|
204
316
|
pushRequest(text: string): void {
|
|
317
|
+
this.actions += 1;
|
|
205
318
|
this.record("request", text);
|
|
206
319
|
this.changed();
|
|
207
320
|
}
|
|
@@ -227,6 +340,23 @@ export class ActivityFeed {
|
|
|
227
340
|
this.changed();
|
|
228
341
|
}
|
|
229
342
|
|
|
343
|
+
/**
|
|
344
|
+
* The musical receipt of the running agent turn (`musicalReceipt` of the
|
|
345
|
+
* score before and after), shown as the turn's closing card.
|
|
346
|
+
*/
|
|
347
|
+
setTurnReceipt(text: string): void {
|
|
348
|
+
this.turnReceipt = text;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The latest prose sentence of the streaming agent text, for the faint
|
|
353
|
+
* tail of the spinner: a complete sentence when one has ended, else the
|
|
354
|
+
* one being written.
|
|
355
|
+
*/
|
|
356
|
+
get streamSentence(): string {
|
|
357
|
+
return latestSentence(this.streamText);
|
|
358
|
+
}
|
|
359
|
+
|
|
230
360
|
/** Adapter for the agent lane's streaming events. */
|
|
231
361
|
applyAgentEvent(event: AgentActivityEvent): void {
|
|
232
362
|
switch (event.type) {
|
|
@@ -271,7 +401,7 @@ export class ActivityFeed {
|
|
|
271
401
|
baseRevision: event.baseRevision,
|
|
272
402
|
resultRevision: event.resultRevision,
|
|
273
403
|
trackId: event.trackId,
|
|
274
|
-
hint: unchanged ? undefined : "
|
|
404
|
+
hint: unchanged ? undefined : "ctrl-z undo",
|
|
275
405
|
});
|
|
276
406
|
return;
|
|
277
407
|
}
|
|
@@ -287,9 +417,20 @@ export class ActivityFeed {
|
|
|
287
417
|
case "done": {
|
|
288
418
|
if (this.streamText) this.record("agent", this.streamText);
|
|
289
419
|
this.streamText = "";
|
|
420
|
+
// A turn that changed the music ends on what it changed, in musical
|
|
421
|
+
// terms; its prose stays in the ctrl-o log.
|
|
422
|
+
const receipt = this.turnReceipt;
|
|
423
|
+
this.turnReceipt = "";
|
|
290
424
|
const summary =
|
|
291
|
-
|
|
292
|
-
|
|
425
|
+
receipt ||
|
|
426
|
+
event.summary ||
|
|
427
|
+
firstSentence(event.text ?? "").slice(0, 160) ||
|
|
428
|
+
undefined;
|
|
429
|
+
if (summary)
|
|
430
|
+
this.pushCard(summary, {
|
|
431
|
+
tone: "agent",
|
|
432
|
+
hint: receipt ? "ctrl-z undo" : undefined,
|
|
433
|
+
});
|
|
293
434
|
else if (event.applied === 0)
|
|
294
435
|
this.pushCard("agent made no changes", { tone: "info" });
|
|
295
436
|
this.setSpinner(undefined);
|
|
@@ -300,7 +441,7 @@ export class ActivityFeed {
|
|
|
300
441
|
this.setSpinner(undefined);
|
|
301
442
|
if (event.code === "aborted")
|
|
302
443
|
this.pushCard(
|
|
303
|
-
`
|
|
444
|
+
`canceled${event.revision === undefined ? "" : ` · kept rev ${event.revision}`}`,
|
|
304
445
|
{ tone: "warning" },
|
|
305
446
|
);
|
|
306
447
|
else
|
|
@@ -331,11 +472,16 @@ export class ActivityFeed {
|
|
|
331
472
|
* success, `false` a failure, `"warn"` a no-op or conflict. Handlers return
|
|
332
473
|
* these so the strip never infers failure from prose.
|
|
333
474
|
*/
|
|
334
|
-
export type Receipt = Readonly<{
|
|
475
|
+
export type Receipt = Readonly<{
|
|
476
|
+
ok: boolean | "warn" | "info";
|
|
477
|
+
text: string;
|
|
478
|
+
}>;
|
|
335
479
|
|
|
336
480
|
export const ok = (text: string): Receipt => ({ ok: true, text });
|
|
337
481
|
export const fail = (text: string): Receipt => ({ ok: false, text });
|
|
338
482
|
export const warn = (text: string): Receipt => ({ ok: "warn", text });
|
|
483
|
+
/** A status read (`•`): nothing changed (bare `fx`, `tracks`). */
|
|
484
|
+
export const note = (text: string): Receipt => ({ ok: "info", text });
|
|
339
485
|
|
|
340
486
|
/** Tone of a structured receipt; strings fall back to `receiptTone`. */
|
|
341
487
|
export function toneOf(receipt: string | Receipt): CardTone {
|
|
@@ -344,7 +490,9 @@ export function toneOf(receipt: string | Receipt): CardTone {
|
|
|
344
490
|
? "success"
|
|
345
491
|
: receipt.ok === "warn"
|
|
346
492
|
? "warning"
|
|
347
|
-
: "
|
|
493
|
+
: receipt.ok === "info"
|
|
494
|
+
? "info"
|
|
495
|
+
: "error";
|
|
348
496
|
}
|
|
349
497
|
|
|
350
498
|
/**
|
|
@@ -362,3 +510,26 @@ export function receiptTone(message: string): CardTone {
|
|
|
362
510
|
return "warning";
|
|
363
511
|
return "success";
|
|
364
512
|
}
|
|
513
|
+
|
|
514
|
+
/** The first sentence of a reply (up to its first `.`, `!` or `?`). */
|
|
515
|
+
export function firstSentence(text: string): string {
|
|
516
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
517
|
+
const match = /^.*?[.!?](?=\s|$)/.exec(flat);
|
|
518
|
+
return (match ? match[0] : flat).trim();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** The last complete sentence of streaming text, or the one in progress. */
|
|
522
|
+
export function latestSentence(text: string): string {
|
|
523
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
524
|
+
if (!flat) return "";
|
|
525
|
+
const sentences = flat.match(/[^.!?]+[.!?]+(?=\s|$)|[^.!?]+$/g) ?? [flat];
|
|
526
|
+
const last = sentences[sentences.length - 1]!.trim();
|
|
527
|
+
// A fragment of a few words reads as noise; keep the sentence before it.
|
|
528
|
+
if (
|
|
529
|
+
!/[.!?]$/.test(last) &&
|
|
530
|
+
last.split(" ").length < 4 &&
|
|
531
|
+
sentences.length > 1
|
|
532
|
+
)
|
|
533
|
+
return sentences[sentences.length - 2]!.trim();
|
|
534
|
+
return last;
|
|
535
|
+
}
|