@hraness/dawg 0.6.1 → 0.8.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 +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- 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 +31 -0
- package/guides/audio.md +31 -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 +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -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/tape.md +32 -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 +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -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 +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -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/range-tools.ts +216 -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 +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- 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/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -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 +360 -40
- 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 +82 -32
- 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 +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -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 +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -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 +617 -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/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- 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/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- 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 +987 -179
- 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 +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -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 +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
package/src/web/fetch.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
9
9
|
import { isIP } from "node:net";
|
|
10
|
+
import { checkServerIdentity, type PeerCertificate } from "node:tls";
|
|
10
11
|
import {
|
|
11
12
|
decodeEntities,
|
|
12
13
|
describeFetchError,
|
|
@@ -81,26 +82,64 @@ function isPrivateV4(address: string): boolean {
|
|
|
81
82
|
);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
/** Expand an IPv6 address (zone stripped, embedded dotted IPv4 allowed) to 8 groups. */
|
|
86
|
+
function expandV6(address: string): number[] | undefined {
|
|
87
|
+
let text = address.toLowerCase().split("%")[0]!;
|
|
88
|
+
const dotted = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(text);
|
|
89
|
+
if (dotted) {
|
|
90
|
+
const [a, b, c, d] = dotted.slice(1).map(Number) as [
|
|
91
|
+
number,
|
|
92
|
+
number,
|
|
93
|
+
number,
|
|
94
|
+
number,
|
|
95
|
+
];
|
|
96
|
+
text = `${text.slice(0, dotted.index)}${((a << 8) | b).toString(16)}:${((c << 8) | d).toString(16)}`;
|
|
96
97
|
}
|
|
97
|
-
const
|
|
98
|
+
const halves = text.split("::");
|
|
99
|
+
if (halves.length > 2) return undefined;
|
|
100
|
+
const parse = (part: string) =>
|
|
101
|
+
part === ""
|
|
102
|
+
? []
|
|
103
|
+
: part.split(":").map((group) => Number.parseInt(group, 16));
|
|
104
|
+
const head = parse(halves[0]!);
|
|
105
|
+
const tail = halves.length === 2 ? parse(halves[1]!) : [];
|
|
106
|
+
const fill = 8 - head.length - tail.length;
|
|
107
|
+
if (halves.length === 1 ? fill !== 0 : fill < 1) return undefined;
|
|
108
|
+
const groups = [
|
|
109
|
+
...head,
|
|
110
|
+
...new Array<number>(halves.length === 2 ? fill : 0).fill(0),
|
|
111
|
+
...tail,
|
|
112
|
+
];
|
|
113
|
+
return groups.every((g) => Number.isInteger(g) && g >= 0 && g <= 0xffff)
|
|
114
|
+
? groups
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function v4From(high: number, low: number): string {
|
|
119
|
+
return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isPrivateV6(address: string): boolean {
|
|
123
|
+
const g = expandV6(address);
|
|
124
|
+
if (!g) return true;
|
|
125
|
+
const zero = (from: number, to: number) =>
|
|
126
|
+
g.slice(from, to).every((x) => x === 0);
|
|
127
|
+
// ::/96 covers :: and ::1 and the deprecated IPv4-compatible form (::a.b.c.d).
|
|
128
|
+
if (zero(0, 6)) return true;
|
|
129
|
+
// ::ffff:0:0/96 IPv4-mapped: as private as the address it carries.
|
|
130
|
+
if (zero(0, 5) && g[5] === 0xffff) return isPrivateV4(v4From(g[6]!, g[7]!));
|
|
131
|
+
// 2002::/16 6to4 embeds an IPv4 address in bits 16..47.
|
|
132
|
+
if (g[0] === 0x2002) return isPrivateV4(v4From(g[1]!, g[2]!));
|
|
133
|
+
const first = g[0]!;
|
|
98
134
|
return (
|
|
135
|
+
(first === 0x64 && g[1] === 0xff9b) || // 64:ff9b::/96 and 64:ff9b:1::/48 NAT64
|
|
136
|
+
(first === 0x100 && zero(1, 4)) || // 100::/64 discard
|
|
137
|
+
(first === 0x2001 && g[1] === 0) || // 2001::/32 Teredo (obfuscated IPv4)
|
|
138
|
+
(first === 0x2001 && g[1] === 0xdb8) || // 2001:db8::/32 documentation
|
|
99
139
|
(first & 0xfe00) === 0xfc00 || // fc00::/7 unique local
|
|
100
140
|
(first & 0xffc0) === 0xfe80 || // fe80::/10 link-local
|
|
101
|
-
(first &
|
|
102
|
-
|
|
103
|
-
lower.startsWith("2001:db8:") // documentation
|
|
141
|
+
(first & 0xffc0) === 0xfec0 || // fec0::/10 deprecated site-local
|
|
142
|
+
(first & 0xff00) === 0xff00 // ff00::/8 multicast
|
|
104
143
|
);
|
|
105
144
|
}
|
|
106
145
|
|
|
@@ -136,12 +175,19 @@ export function admitUrl(raw: unknown): URL {
|
|
|
136
175
|
return url;
|
|
137
176
|
}
|
|
138
177
|
|
|
139
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Check that a URL's host is public and return the address to connect to.
|
|
180
|
+
* Every resolved address must be public; the first one is returned so the
|
|
181
|
+
* request connects to exactly the address that was checked, never to a
|
|
182
|
+
* second, independent DNS answer (which a rebinding domain could point at
|
|
183
|
+
* loopback or a metadata service).
|
|
184
|
+
*/
|
|
185
|
+
async function assertPublicHost(url: URL, lookup: Lookup): Promise<string> {
|
|
140
186
|
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
141
187
|
if (isIP(host)) {
|
|
142
188
|
if (isPrivateAddress(host))
|
|
143
189
|
throw new WebError(`${host} is a private or local address`);
|
|
144
|
-
return;
|
|
190
|
+
return host;
|
|
145
191
|
}
|
|
146
192
|
let addresses: readonly string[];
|
|
147
193
|
try {
|
|
@@ -153,6 +199,42 @@ async function assertPublicHost(url: URL, lookup: Lookup): Promise<void> {
|
|
|
153
199
|
for (const address of addresses)
|
|
154
200
|
if (isPrivateAddress(address))
|
|
155
201
|
throw new WebError(`${host} resolves to a private or local address`);
|
|
202
|
+
return addresses[0]!;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Bun's fetch accepts TLS options beyond the standard RequestInit. */
|
|
206
|
+
type PinnedInit = RequestInit & {
|
|
207
|
+
tls?: {
|
|
208
|
+
serverName?: string;
|
|
209
|
+
checkServerIdentity?: (
|
|
210
|
+
hostname: string,
|
|
211
|
+
cert: PeerCertificate,
|
|
212
|
+
) => Error | undefined;
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Address a request to a checked IP while keeping the original name for the
|
|
218
|
+
* Host header and for TLS (SNI and certificate verification), so the
|
|
219
|
+
* connection cannot be redirected by a later DNS answer.
|
|
220
|
+
*/
|
|
221
|
+
export function pinRequest(
|
|
222
|
+
url: URL,
|
|
223
|
+
address: string,
|
|
224
|
+
): { target: string; host: string; tls?: PinnedInit["tls"] } {
|
|
225
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
226
|
+
const pinned = new URL(url.toString());
|
|
227
|
+
pinned.hostname = isIP(address) === 6 ? `[${address}]` : address;
|
|
228
|
+
const result: { target: string; host: string; tls?: PinnedInit["tls"] } = {
|
|
229
|
+
target: pinned.toString(),
|
|
230
|
+
host: url.host,
|
|
231
|
+
};
|
|
232
|
+
if (url.protocol === "https:" && !isIP(hostname))
|
|
233
|
+
result.tls = {
|
|
234
|
+
serverName: hostname,
|
|
235
|
+
checkServerIdentity: (_name, cert) => checkServerIdentity(hostname, cert),
|
|
236
|
+
};
|
|
237
|
+
return result;
|
|
156
238
|
}
|
|
157
239
|
|
|
158
240
|
export async function fetchUrl(
|
|
@@ -168,18 +250,22 @@ export async function fetchUrl(
|
|
|
168
250
|
let url = admitUrl(raw);
|
|
169
251
|
let response: Response | undefined;
|
|
170
252
|
for (let hop = 0; hop <= FETCH_LIMITS.maxRedirects; hop += 1) {
|
|
171
|
-
await assertPublicHost(url, lookup);
|
|
253
|
+
const address = await assertPublicHost(url, lookup);
|
|
254
|
+
const pin = pinRequest(url, address);
|
|
255
|
+
const init: PinnedInit = {
|
|
256
|
+
method: "GET",
|
|
257
|
+
redirect: "manual",
|
|
258
|
+
headers: {
|
|
259
|
+
host: pin.host,
|
|
260
|
+
accept:
|
|
261
|
+
"text/html,application/xhtml+xml,text/plain;q=0.9,application/json;q=0.8,*/*;q=0.5",
|
|
262
|
+
"user-agent": "Mozilla/5.0 (compatible; dawg-agent)",
|
|
263
|
+
},
|
|
264
|
+
signal: timer.signal,
|
|
265
|
+
};
|
|
266
|
+
if (pin.tls) init.tls = pin.tls;
|
|
172
267
|
try {
|
|
173
|
-
response = await fetcher(
|
|
174
|
-
method: "GET",
|
|
175
|
-
redirect: "manual",
|
|
176
|
-
headers: {
|
|
177
|
-
accept:
|
|
178
|
-
"text/html,application/xhtml+xml,text/plain;q=0.9,application/json;q=0.8,*/*;q=0.5",
|
|
179
|
-
"user-agent": "Mozilla/5.0 (compatible; dawg-agent)",
|
|
180
|
-
},
|
|
181
|
-
signal: timer.signal,
|
|
182
|
-
});
|
|
268
|
+
response = await fetcher(pin.target, init);
|
|
183
269
|
} catch (error) {
|
|
184
270
|
throw new WebError(
|
|
185
271
|
`fetch failed: ${timer.timedOut() ? `timed out after ${timeoutMs / 1000}s` : describeFetchError(error)}`,
|
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
|
+
}
|