@hraness/dawg 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/DAWG.md +142 -0
  3. package/LICENSE +21 -0
  4. package/README.md +164 -2
  5. package/core/drums.ts +102 -0
  6. package/core/loop.ts +78 -0
  7. package/core/score.ts +1065 -0
  8. package/package.json +42 -4
  9. package/src/agent/agent.ts +587 -0
  10. package/src/agent/brief.ts +140 -0
  11. package/src/agent/gateway.ts +267 -0
  12. package/src/agent/ops.ts +172 -0
  13. package/src/agent/planner.ts +221 -0
  14. package/src/agent/provider.ts +326 -0
  15. package/src/agent/sse.ts +90 -0
  16. package/src/agent/tools.ts +976 -0
  17. package/src/agent/xcb-agent.ts +247 -0
  18. package/src/agent/xcb.ts +367 -0
  19. package/src/audio/clock.ts +68 -0
  20. package/src/audio/engine.ts +467 -0
  21. package/src/audio/lock.ts +57 -0
  22. package/src/audio/player.ts +119 -0
  23. package/src/audio/wav.ts +647 -0
  24. package/src/auth/cli.ts +112 -0
  25. package/src/auth/credentials.ts +285 -0
  26. package/src/auth/login.ts +434 -0
  27. package/src/auth/runner.ts +184 -0
  28. package/src/auth/tui.ts +90 -0
  29. package/src/commands/history.ts +56 -0
  30. package/src/commands/music.ts +444 -0
  31. package/src/daemon.ts +31 -0
  32. package/src/main.ts +1332 -0
  33. package/src/render.ts +99 -0
  34. package/src/session/attach.ts +182 -0
  35. package/src/session/client.ts +498 -0
  36. package/src/session/daemon.ts +723 -0
  37. package/src/session/list.ts +180 -0
  38. package/src/session/lock.ts +92 -0
  39. package/src/session/meta.ts +253 -0
  40. package/src/session/naming.ts +464 -0
  41. package/src/session/port.ts +461 -0
  42. package/src/session/presence.ts +159 -0
  43. package/src/session/protocol.ts +618 -0
  44. package/src/session/rebase.ts +138 -0
  45. package/src/session/store.ts +486 -0
  46. package/tui/activity.ts +325 -0
  47. package/tui/app.ts +1129 -0
  48. package/tui/drums.ts +21 -0
  49. package/tui/highway.ts +905 -0
  50. package/tui/input.ts +63 -0
  51. package/tui/keys.ts +102 -0
  52. package/tui/layers.ts +68 -0
  53. package/tui/prompt.ts +609 -0
  54. package/tui/render.ts +124 -0
  55. package/tui/screen.ts +247 -0
  56. package/tui/text.ts +72 -0
  57. package/tui/theme.ts +451 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Activity feed: the strip under the hit line and the transcript overlay.
3
+ *
4
+ * Other subsystems describe what happened; the TUI decides how to show it.
5
+ * - Command handlers push receipts (`pushCard`) and errors (`pushError`).
6
+ * - The agent lane forwards streaming events through `applyAgentEvent`.
7
+ * - A spinner (`setSpinner`) shows that work is in flight.
8
+ *
9
+ * The feed is plain data with an injectable clock, so tests can assert exact
10
+ * strip contents without timers.
11
+ */
12
+
13
+ export type CardTone = "info" | "success" | "warning" | "error" | "agent";
14
+
15
+ export interface ActivityCard {
16
+ id: number;
17
+ /** Short operation summary, e.g. "+8 bass notes". */
18
+ text: string;
19
+ tone: CardTone;
20
+ atMs: number;
21
+ baseRevision?: number | undefined;
22
+ resultRevision?: number | undefined;
23
+ trackId?: string | undefined;
24
+ /** Key hint shown after the card, e.g. "u undo". */
25
+ hint?: string | undefined;
26
+ }
27
+
28
+ export type TranscriptKind =
29
+ "request" | "op" | "revision" | "error" | "agent" | "note";
30
+
31
+ export interface TranscriptEntry {
32
+ id: number;
33
+ kind: TranscriptKind;
34
+ text: string;
35
+ atMs: number;
36
+ }
37
+
38
+ /**
39
+ * Streaming agent events. Structurally compatible with `AgentEvent` from
40
+ * src/agent/agent.ts (`delta`, `diagnostic`, `text`, `code`), so the agent lane
41
+ * forwards events unchanged; the shorter aliases (`text`, `reason`, `summary`)
42
+ * are accepted for other producers.
43
+ */
44
+ export type AgentActivityEvent =
45
+ | { type: "start"; prompt?: string | undefined; model?: string | undefined }
46
+ | { type: "step"; step: number }
47
+ | { type: "text-delta"; delta?: string; text?: string }
48
+ | { type: "tool-start"; name: string; callId?: string; step?: number }
49
+ | {
50
+ type: "tool-applied";
51
+ summary: string;
52
+ name?: string;
53
+ callId?: string;
54
+ baseRevision?: number | undefined;
55
+ resultRevision?: number | undefined;
56
+ trackId?: string | undefined;
57
+ }
58
+ | {
59
+ type: "tool-rejected";
60
+ name?: string;
61
+ callId?: string;
62
+ diagnostic?: string;
63
+ summary?: string | undefined;
64
+ reason?: string;
65
+ }
66
+ | {
67
+ type: "done";
68
+ summary?: string | undefined;
69
+ text?: string;
70
+ applied?: number;
71
+ rejected?: number;
72
+ revision?: number;
73
+ }
74
+ | {
75
+ type: "error";
76
+ message: string;
77
+ code?: string;
78
+ applied?: number;
79
+ revision?: number;
80
+ };
81
+
82
+ export interface ActivityOptions {
83
+ clock?: () => number;
84
+ /** Cards kept for the strip. */
85
+ maxCards?: number;
86
+ /** Entries kept for the transcript overlay. */
87
+ maxTranscript?: number;
88
+ }
89
+
90
+ export const BRAILLE_SPINNER = [
91
+ "⠋",
92
+ "⠙",
93
+ "⠹",
94
+ "⠸",
95
+ "⠼",
96
+ "⠴",
97
+ "⠦",
98
+ "⠧",
99
+ "⠇",
100
+ "⠏",
101
+ ] as const;
102
+ export const ASCII_SPINNER = ["-", "\\", "|", "/"] as const;
103
+
104
+ export function spinnerFrame(
105
+ nowMs: number,
106
+ unicode: boolean,
107
+ reducedMotion = false,
108
+ ): string {
109
+ const frames = unicode ? BRAILLE_SPINNER : ASCII_SPINNER;
110
+ if (reducedMotion) return unicode ? "…" : "~";
111
+ return frames[Math.floor(nowMs / 80) % frames.length]!;
112
+ }
113
+
114
+ /** Format a revision transition, e.g. "rev 41→42". */
115
+ export function revisionLabel(
116
+ base: number | undefined,
117
+ result: number | undefined,
118
+ unicode = true,
119
+ ): string | undefined {
120
+ if (result === undefined) return undefined;
121
+ if (base === undefined || base === result) return `rev ${result}`;
122
+ return `rev ${base}${unicode ? "→" : "->"}${result}`;
123
+ }
124
+
125
+ export class ActivityFeed {
126
+ private nextId = 1;
127
+ private readonly clock: () => number;
128
+ private readonly maxCards: number;
129
+ private readonly maxTranscript: number;
130
+ private cardList: ActivityCard[] = [];
131
+ private log: TranscriptEntry[] = [];
132
+ private spinnerLabel: string | undefined;
133
+ private spinnerSince = 0;
134
+ private streamText = "";
135
+ private queue = 0;
136
+ private listeners = new Set<() => void>();
137
+ /** Increments on every change; renderers can skip work when unchanged. */
138
+ version = 0;
139
+
140
+ constructor(options: ActivityOptions = {}) {
141
+ this.clock = options.clock ?? Date.now;
142
+ this.maxCards = options.maxCards ?? 6;
143
+ this.maxTranscript = options.maxTranscript ?? 200;
144
+ }
145
+
146
+ get cards(): readonly ActivityCard[] {
147
+ return this.cardList;
148
+ }
149
+ get transcript(): readonly TranscriptEntry[] {
150
+ return this.log;
151
+ }
152
+ get spinner(): { label: string; sinceMs: number } | undefined {
153
+ return this.spinnerLabel === undefined
154
+ ? undefined
155
+ : { label: this.spinnerLabel, sinceMs: this.spinnerSince };
156
+ }
157
+ /** Latest streamed agent text (single line, newest tail kept). */
158
+ get streaming(): string {
159
+ return this.streamText;
160
+ }
161
+ get queueDepth(): number {
162
+ return this.queue;
163
+ }
164
+ get latest(): ActivityCard | undefined {
165
+ return this.cardList[this.cardList.length - 1];
166
+ }
167
+
168
+ subscribe(listener: () => void): () => void {
169
+ this.listeners.add(listener);
170
+ return () => this.listeners.delete(listener);
171
+ }
172
+
173
+ pushCard(
174
+ text: string,
175
+ details: Omit<Partial<ActivityCard>, "id" | "atMs" | "text"> = {},
176
+ ): ActivityCard {
177
+ const card: ActivityCard = {
178
+ id: this.nextId++,
179
+ text,
180
+ tone: details.tone ?? "info",
181
+ atMs: this.clock(),
182
+ baseRevision: details.baseRevision,
183
+ resultRevision: details.resultRevision,
184
+ trackId: details.trackId,
185
+ hint: details.hint,
186
+ };
187
+ this.cardList = [...this.cardList, card].slice(-this.maxCards);
188
+ const revision = revisionLabel(card.baseRevision, card.resultRevision);
189
+ this.record(
190
+ card.tone === "error" ? "error" : "op",
191
+ revision ? `${text} · ${revision}` : text,
192
+ );
193
+ this.changed();
194
+ return card;
195
+ }
196
+
197
+ pushError(message: string): ActivityCard {
198
+ return this.pushCard(message, { tone: "error" });
199
+ }
200
+
201
+ /** Record a user request in the transcript without adding a strip card. */
202
+ pushRequest(text: string): void {
203
+ this.record("request", text);
204
+ this.changed();
205
+ }
206
+
207
+ pushNote(text: string, kind: TranscriptKind = "note"): void {
208
+ this.record(kind, text);
209
+ this.changed();
210
+ }
211
+
212
+ setSpinner(label: string | undefined): void {
213
+ if (label === this.spinnerLabel) return;
214
+ if (label !== undefined && this.spinnerLabel === undefined)
215
+ this.spinnerSince = this.clock();
216
+ this.spinnerLabel = label;
217
+ if (label === undefined) this.streamText = "";
218
+ this.changed();
219
+ }
220
+
221
+ setQueueDepth(depth: number): void {
222
+ const next = Math.max(0, Math.floor(depth));
223
+ if (next === this.queue) return;
224
+ this.queue = next;
225
+ this.changed();
226
+ }
227
+
228
+ /** Adapter for the agent lane's streaming events. */
229
+ applyAgentEvent(event: AgentActivityEvent): void {
230
+ switch (event.type) {
231
+ case "start":
232
+ this.streamText = "";
233
+ if (event.prompt) this.record("request", event.prompt);
234
+ this.setSpinner(event.model ? `thinking · ${event.model}` : "thinking");
235
+ return;
236
+ case "step":
237
+ this.setSpinner(
238
+ event.step <= 1 ? "thinking" : `thinking · step ${event.step}`,
239
+ );
240
+ return;
241
+ case "text-delta": {
242
+ const chunk = event.delta ?? event.text ?? "";
243
+ const joined = `${this.streamText}${chunk}`.replace(/\s+/g, " ");
244
+ this.streamText = joined.slice(-240);
245
+ if (this.spinnerLabel === undefined) this.setSpinner("thinking");
246
+ this.changed();
247
+ return;
248
+ }
249
+ case "tool-start":
250
+ this.setSpinner(`${event.name.replace(/_/g, " ")}…`);
251
+ return;
252
+ case "tool-applied":
253
+ this.pushCard(event.summary, {
254
+ tone: "success",
255
+ baseRevision: event.baseRevision,
256
+ resultRevision: event.resultRevision,
257
+ trackId: event.trackId,
258
+ hint: "^z undo",
259
+ });
260
+ return;
261
+ case "tool-rejected": {
262
+ const what = event.summary ?? event.name?.replace(/_/g, " ");
263
+ const why = event.reason ?? event.diagnostic;
264
+ this.pushCard(
265
+ `rejected${what ? ` · ${what}` : ""}${why ? ` · ${why}` : ""}`,
266
+ { tone: "warning" },
267
+ );
268
+ return;
269
+ }
270
+ case "done": {
271
+ if (this.streamText) this.record("agent", this.streamText);
272
+ this.streamText = "";
273
+ const summary =
274
+ event.summary ?? event.text?.split("\n")[0]?.slice(0, 160);
275
+ if (summary) this.pushCard(summary, { tone: "agent" });
276
+ else if (event.applied === 0)
277
+ this.pushCard("agent made no changes", { tone: "info" });
278
+ this.setSpinner(undefined);
279
+ return;
280
+ }
281
+ case "error":
282
+ this.streamText = "";
283
+ this.setSpinner(undefined);
284
+ if (event.code === "aborted")
285
+ this.pushCard(
286
+ `cancelled${event.revision === undefined ? "" : ` · kept rev ${event.revision}`}`,
287
+ { tone: "warning" },
288
+ );
289
+ else
290
+ this.pushError(
291
+ event.code
292
+ ? `agent ${event.code}: ${event.message}`
293
+ : event.message,
294
+ );
295
+ return;
296
+ }
297
+ }
298
+
299
+ private record(kind: TranscriptKind, text: string): void {
300
+ this.log = [
301
+ ...this.log,
302
+ { id: this.nextId++, kind, text, atMs: this.clock() },
303
+ ].slice(-this.maxTranscript);
304
+ }
305
+
306
+ private changed(): void {
307
+ this.version += 1;
308
+ for (const listener of this.listeners) listener();
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Classify a legacy command receipt string (e.g. "undid · rev 41",
314
+ * "tempo error · …") into a card tone so existing handlers need no rewrite.
315
+ */
316
+ export function receiptTone(message: string): CardTone {
317
+ const lower = message.toLowerCase();
318
+ if (
319
+ /\berror\b|unrecognized|failed|invalid|cannot|can't|not found/.test(lower)
320
+ )
321
+ return "error";
322
+ if (/nothing to|conflict|changed; retry|exists|another window/.test(lower))
323
+ return "warning";
324
+ return "success";
325
+ }