@kolisachint/hoocode-agent 0.5.17 → 0.5.19
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 +247 -0
- package/dist/core/learn/audit.d.ts +136 -0
- package/dist/core/learn/audit.d.ts.map +1 -0
- package/dist/core/learn/audit.js +316 -0
- package/dist/core/learn/audit.js.map +1 -0
- package/dist/core/learn/cache.d.ts +58 -0
- package/dist/core/learn/cache.d.ts.map +1 -0
- package/dist/core/learn/cache.js +132 -0
- package/dist/core/learn/cache.js.map +1 -0
- package/dist/core/learn/cluster.d.ts +78 -0
- package/dist/core/learn/cluster.d.ts.map +1 -0
- package/dist/core/learn/cluster.js +184 -0
- package/dist/core/learn/cluster.js.map +1 -0
- package/dist/core/learn/coverage.d.ts +58 -0
- package/dist/core/learn/coverage.d.ts.map +1 -0
- package/dist/core/learn/coverage.js +144 -0
- package/dist/core/learn/coverage.js.map +1 -0
- package/dist/core/learn/digest.d.ts +13 -0
- package/dist/core/learn/digest.d.ts.map +1 -1
- package/dist/core/learn/digest.js +113 -14
- package/dist/core/learn/digest.js.map +1 -1
- package/dist/core/learn/extract.d.ts +108 -105
- package/dist/core/learn/extract.d.ts.map +1 -1
- package/dist/core/learn/extract.js +308 -447
- package/dist/core/learn/extract.js.map +1 -1
- package/dist/core/learn/mine.d.ts +178 -0
- package/dist/core/learn/mine.d.ts.map +1 -0
- package/dist/core/learn/mine.js +390 -0
- package/dist/core/learn/mine.js.map +1 -0
- package/dist/core/learn/reduce.d.ts +89 -0
- package/dist/core/learn/reduce.d.ts.map +1 -0
- package/dist/core/learn/reduce.js +179 -0
- package/dist/core/learn/reduce.js.map +1 -0
- package/dist/core/learn/state.d.ts +19 -18
- package/dist/core/learn/state.d.ts.map +1 -1
- package/dist/core/learn/state.js +35 -31
- package/dist/core/learn/state.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -1
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -1
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +5 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/startup-progress.d.ts +12 -7
- package/dist/core/startup-progress.d.ts.map +1 -1
- package/dist/core/startup-progress.js +12 -7
- package/dist/core/startup-progress.js.map +1 -1
- package/dist/extensions/core/learn.d.ts +8 -4
- package/dist/extensions/core/learn.d.ts.map +1 -1
- package/dist/extensions/core/learn.js +292 -56
- package/dist/extensions/core/learn.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +7 -25
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/progress-bar.d.ts +50 -0
- package/dist/modes/interactive/components/progress-bar.d.ts.map +1 -0
- package/dist/modes/interactive/components/progress-bar.js +77 -0
- package/dist/modes/interactive/components/progress-bar.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts +6 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.js +18 -14
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -1
- package/docs/settings.md +9 -6
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/dist/core/learn/normalize.d.ts +0 -65
- package/dist/core/learn/normalize.d.ts.map +0 -1
- package/dist/core/learn/normalize.js +0 -245
- package/dist/core/learn/normalize.js.map +0 -1
|
@@ -68,7 +68,12 @@ export declare class VoicePanel implements Component {
|
|
|
68
68
|
private renderWaveform;
|
|
69
69
|
private renderPartial;
|
|
70
70
|
private elapsedLabel;
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* The model download's progress, rendered by the shared progress-bar
|
|
73
|
+
* component so this panel and the footer always agree. The panel is wider
|
|
74
|
+
* than a footer line, so it asks for a finer track — width is the only thing
|
|
75
|
+
* a surface gets to choose.
|
|
76
|
+
*/
|
|
72
77
|
private renderDownloadBar;
|
|
73
78
|
render(width: number): string[];
|
|
74
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-panel.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAI/D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,cAAc,CAAC;AAsDnF,qBAAa,UAAW,YAAW,SAAS;IAe1C,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAf5B,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,KAAK,CAA6C;IAE1D,YACkB,EAAE,EAAE,GAAG,GAAG,SAAS,EACnB,UAAU,EAAE,MAAM,EAChC;IAIJ,OAAO,CAAC,eAAe;IAWvB,kFAAkF;IAClF,OAAO,IAAI,IAAI,CAKd;IAED,oFAAoF;IACpF,UAAU,IAAI,IAAI,CAAG;IAIrB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAMjD;IAED;;;OAGG;IACH,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAK1E;IAED,cAAc,IAAI,IAAI,CAQrB;IAED,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKlC;IAED,mEAAmE;IACnE,UAAU,IAAI,IAAI,CAKjB;IAED,eAAe,IAAI,IAAI,CAItB;IAID,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAO3B;IAED,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI7B;IAID;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,YAAY;IAOpB,iFAAgF;IAChF,OAAO,CAAC,iBAAiB;IAazB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA4C9B;CACD","sourcesContent":["/**\n * Multi-line voice-input status panel.\n *\n * Renders the live state of a `voicetools serve` capture across its phases so\n * the user always sees what's happening:\n *\n * ```text\n * ● Listening · 0:03 esc cancel\n * ▁▂▃▅▇▆▄▂▁▂▄▆▇▅▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ← level history, left-anchored on a track\n * › and so my fellow ameri▏ ← live partial transcript (PARTIAL)\n * ```\n *\n * Phases:\n * - `warming` spinner while the binary resolves / model loads; shows a\n * real progress bar when a download reports byte counts\n * - `listening` recording dot + elapsed timer + waveform + partial text\n * - `silence` trailing-silence countdown; waveform dims\n * - `transcribing` spinner while the final decode runs\n *\n * The component self-animates (spinner, timer, countdown) on an internal timer\n * and calls `ui.requestRender()`; the owner feeds it protocol events via\n * `pushLevel` / `setPartial` / `beginSilence` / `setDownloadProgress` etc. and\n * disposes it on collapse.\n */\n\nimport type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport { truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { theme } from \"../theme/theme.js\";\n\nexport type VoicePanelPhase = \"warming\" | \"listening\" | \"silence\" | \"transcribing\";\n\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n// 8 filled levels; index 0 renders as a baseline dot so an empty history still\n// reads as \"a track\", not a blank line.\nconst WAVE_LEVELS = [\"▁\", \"▁\", \"▂\", \"▃\", \"▄\", \"▅\", \"▆\", \"▇\", \"█\"];\nconst TICK_MS = 100;\nconst HISTORY_CAP = 256;\n// RMS from voicetools' vad::rms sits well below 1.0 for speech; this gain maps a\n// normal speaking level to roughly the top of the meter without clipping constantly.\nconst LEVEL_GAIN = 6;\n// During trailing silence the binary keeps emitting low LEVELs; only an RMS above\n// this counts as the speaker resuming, so we don't cancel the countdown on noise.\nconst RESUME_RMS = 0.02;\n/** Cells in the download progress bar shown while the voicetools binary fetches. */\nconst PROGRESS_CELLS = 20;\n\n/** Map an RMS energy to a waveform block glyph. */\nfunction levelGlyph(rms: number): string {\n\tconst norm = Math.max(0, Math.min(1, rms * LEVEL_GAIN));\n\tconst idx = Math.round(norm * (WAVE_LEVELS.length - 1));\n\treturn WAVE_LEVELS[idx] ?? WAVE_LEVELS[0]!;\n}\n\n/** Keep the tail (most recent chars) of `text` within `max` visible columns. */\nfunction clampTail(text: string, max: number): string {\n\tif (max <= 0) return \"\";\n\tif (visibleWidth(text) <= max) return text;\n\t// Partial transcripts are effectively plain text; slice by chars from the end\n\t// and prefix an ellipsis so the newest words stay visible as it grows.\n\tconst tail = text.slice(-(max - 1));\n\treturn `…${tail}`;\n}\n\n/** `left` flush left, `right` flush right when it fits; otherwise just `left`. */\nfunction lineWithHint(\n\twidth: number,\n\tleftPlain: string,\n\tleftStyled: string,\n\thintPlain: string,\n\thintStyled: string,\n): string {\n\tconst lw = visibleWidth(leftPlain);\n\tif (hintPlain && lw + 2 + visibleWidth(hintPlain) <= width) {\n\t\treturn leftStyled + \" \".repeat(width - lw - visibleWidth(hintPlain)) + hintStyled;\n\t}\n\tif (lw <= width) return leftStyled;\n\treturn truncateToWidth(leftStyled, width, theme.fg(\"dim\", \"…\"));\n}\n\nfunction formatMb(bytes: number): string {\n\treturn `${Math.round(bytes / (1024 * 1024))} MB`;\n}\n\nexport class VoicePanel implements Component {\n\tprivate phase: VoicePanelPhase = \"warming\";\n\tprivate frame = 0;\n\tprivate levels: number[] = [];\n\tprivate partial = \"\";\n\tprivate listeningStartMs = 0;\n\tprivate silenceRemainingMs = 0;\n\tprivate warmingMessage = \"Warming up voice input…\";\n\tprivate warmingDetail: string | undefined;\n\tprivate downloadReceived = 0;\n\tprivate downloadTotal: number | null = null;\n\tprivate downloading = false;\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\n\tconstructor(\n\t\tprivate readonly ui: TUI | undefined,\n\t\tprivate readonly cancelHint: string,\n\t) {}\n\n\t// ---- lifecycle -------------------------------------------------------\n\n\tprivate ensureAnimating(): void {\n\t\tif (this.timer) return;\n\t\tthis.timer = setInterval(() => {\n\t\t\tthis.frame = (this.frame + 1) % SPINNER_FRAMES.length;\n\t\t\tif (this.phase === \"silence\" && this.silenceRemainingMs > 0) {\n\t\t\t\tthis.silenceRemainingMs = Math.max(0, this.silenceRemainingMs - TICK_MS);\n\t\t\t}\n\t\t\tthis.ui?.requestRender();\n\t\t}, TICK_MS);\n\t}\n\n\t/** Stop timers. The owner removes the panel from its container to collapse it. */\n\tdispose(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\t/** No cached render state to clear; every `render()` recomputes from live state. */\n\tinvalidate(): void {}\n\n\t// ---- phase transitions ----------------------------------------------\n\n\tsetWarming(message: string, detail?: string): void {\n\t\tthis.phase = \"warming\";\n\t\tthis.warmingMessage = message;\n\t\tthis.warmingDetail = detail;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/**\n\t * Live byte counts while the voicetools binary downloads (first run only).\n\t * Turns the warming spinner into a determinate progress bar.\n\t */\n\tsetDownloadProgress(receivedBytes: number, totalBytes: number | null): void {\n\t\tthis.downloading = true;\n\t\tthis.downloadReceived = receivedBytes;\n\t\tthis.downloadTotal = totalBytes;\n\t\tthis.ui?.requestRender();\n\t}\n\n\tstartListening(): void {\n\t\tthis.phase = \"listening\";\n\t\tthis.downloading = false;\n\t\tthis.levels = [];\n\t\tthis.partial = \"\";\n\t\tthis.listeningStartMs = Date.now();\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tbeginSilence(totalMs: number): void {\n\t\tthis.phase = \"silence\";\n\t\tthis.silenceRemainingMs = totalMs;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/** Speech resumed before cutoff: return to the listening phase. */\n\tendSilence(): void {\n\t\tif (this.phase === \"silence\") {\n\t\t\tthis.phase = \"listening\";\n\t\t\tthis.ui?.requestRender();\n\t\t}\n\t}\n\n\tsetTranscribing(): void {\n\t\tthis.phase = \"transcribing\";\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- data feed -------------------------------------------------------\n\n\tpushLevel(rms: number): void {\n\t\tthis.levels.push(rms);\n\t\tif (this.levels.length > HISTORY_CAP) this.levels.shift();\n\t\t// Low levels keep flowing during the silence window; only a loud one means\n\t\t// the speaker resumed and the countdown should be abandoned.\n\t\tif (this.phase === \"silence\" && rms > RESUME_RMS) this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tsetPartial(text: string): void {\n\t\tthis.partial = text;\n\t\tif (this.phase === \"silence\") this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- rendering -------------------------------------------------------\n\n\t/**\n\t * Level history, LEFT-anchored: the meter grows from the left edge and the\n\t * unfilled remainder renders as a dim baseline track, so the first seconds of\n\t * a capture read as a meter filling — not a cluster of glyphs floating at the\n\t * far right edge over a void (the old right-anchored layout). Once the\n\t * history exceeds the width it scrolls, keeping the newest samples visible.\n\t */\n\tprivate renderWaveform(width: number): string {\n\t\tconst cells = Math.max(1, width - 2);\n\t\tconst recent = this.levels.slice(-cells);\n\t\tconst wave = recent.map(levelGlyph).join(\"\");\n\t\tconst track = \"▁\".repeat(Math.max(0, cells - recent.length));\n\t\tconst waveStyled = this.phase === \"silence\" ? theme.fg(\"dim\", wave) : theme.fg(\"accent\", wave);\n\t\treturn waveStyled + theme.fg(\"dim\", track);\n\t}\n\n\tprivate renderPartial(width: number): string | undefined {\n\t\tif (!this.partial) return undefined;\n\t\tconst body = clampTail(this.partial, Math.max(1, width - 4));\n\t\tconst cursor = this.phase === \"transcribing\" ? \"\" : theme.blink(theme.fg(\"accent\", \"▏\"));\n\t\treturn ` ${theme.fg(\"muted\", \"›\")} ${theme.fg(\"dim\", body)}${cursor}`;\n\t}\n\n\tprivate elapsedLabel(): string {\n\t\tconst secs = Math.floor((Date.now() - this.listeningStartMs) / 1000);\n\t\tconst m = Math.floor(secs / 60);\n\t\tconst s = secs % 60;\n\t\treturn `${m}:${s.toString().padStart(2, \"0\")}`;\n\t}\n\n\t/** Determinate download bar: `·` fill over a dim track with % and MB counts. */\n\tprivate renderDownloadBar(): string {\n\t\tconst total = this.downloadTotal;\n\t\tif (total === null || total <= 0) {\n\t\t\treturn ` ${theme.fg(\"dim\", `downloaded ${formatMb(this.downloadReceived)}…`)}`;\n\t\t}\n\t\tconst ratio = Math.max(0, Math.min(1, this.downloadReceived / total));\n\t\tconst filled = Math.round(ratio * PROGRESS_CELLS);\n\t\tconst bar = theme.fg(\"accent\", \"·\".repeat(filled)) + theme.fg(\"dim\", \"·\".repeat(PROGRESS_CELLS - filled));\n\t\tconst pct = `${Math.round(ratio * 100)}%`;\n\t\tconst sizes = `${formatMb(this.downloadReceived)} / ${formatMb(total)}`;\n\t\treturn ` ${bar} ${theme.fg(\"muted\", pct)} ${theme.fg(\"dim\", `· ${sizes}`)}`;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst spinner = SPINNER_FRAMES[this.frame] ?? SPINNER_FRAMES[0]!;\n\t\tconst hintPlain = this.cancelHint;\n\t\tconst hintStyled = theme.fg(\"dim\", this.cancelHint);\n\t\tconst lines: string[] = [\"\"];\n\n\t\tif (this.phase === \"warming\") {\n\t\t\tconst headPlain = ` ${spinner} ${this.warmingMessage}`;\n\t\t\tconst headStyled = ` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"text\", this.warmingMessage)}`;\n\t\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\t\tif (this.downloading) {\n\t\t\t\tlines.push(this.renderDownloadBar());\n\t\t\t}\n\t\t\tif (this.warmingDetail) {\n\t\t\t\tlines.push(` ${theme.fg(\"dim\", this.warmingDetail)}`);\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.phase === \"transcribing\") {\n\t\t\tconst headPlain = ` ${spinner} Transcribing…`;\n\t\t\tconst headStyled = ` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"accent\", \"Transcribing…\")}`;\n\t\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\t\tconst partial = this.renderPartial(width);\n\t\t\tif (partial) lines.push(partial);\n\t\t\treturn lines;\n\t\t}\n\n\t\t// listening / silence. A single-cell recording dot (●, error red = \"live\")\n\t\t// replaces the old emoji mic, whose emoji-presentation width drift was the\n\t\t// reason for a VS16 workaround; ● needs no such care.\n\t\tconst dot = theme.fg(\"error\", \"●\");\n\t\tlet headPlain = ` ● Listening · ${this.elapsedLabel()}`;\n\t\tlet headStyled = ` ${dot} ${theme.fg(\"text\", \"Listening\")} ${theme.fg(\"dim\", `· ${this.elapsedLabel()}`)}`;\n\t\tif (this.phase === \"silence\") {\n\t\t\tconst remaining = (this.silenceRemainingMs / 1000).toFixed(1);\n\t\t\theadPlain += ` · cutting off in ${remaining}s`;\n\t\t\theadStyled += ` ${theme.fg(\"warning\", `· cutting off in ${remaining}s`)}`;\n\t\t}\n\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\tlines.push(` ${this.renderWaveform(width)}`);\n\t\tconst partial = this.renderPartial(width);\n\t\tif (partial) lines.push(partial);\n\t\treturn lines;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"voice-panel.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAK/D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,cAAc,CAAC;AA4DnF,qBAAa,UAAW,YAAW,SAAS;IAe1C,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAf5B,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,KAAK,CAA6C;IAE1D,YACkB,EAAE,EAAE,GAAG,GAAG,SAAS,EACnB,UAAU,EAAE,MAAM,EAChC;IAIJ,OAAO,CAAC,eAAe;IAWvB,kFAAkF;IAClF,OAAO,IAAI,IAAI,CAKd;IAED,oFAAoF;IACpF,UAAU,IAAI,IAAI,CAAG;IAIrB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAMjD;IAED;;;OAGG;IACH,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAK1E;IAED,cAAc,IAAI,IAAI,CAQrB;IAED,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKlC;IAED,mEAAmE;IACnE,UAAU,IAAI,IAAI,CAKjB;IAED,eAAe,IAAI,IAAI,CAItB;IAID,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAO3B;IAED,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI7B;IAID;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,YAAY;IAOpB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAIzB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA4C9B;CACD","sourcesContent":["/**\n * Multi-line voice-input status panel.\n *\n * Renders the live state of a `voicetools serve` capture across its phases so\n * the user always sees what's happening:\n *\n * ```text\n * ● Listening · 0:03 esc cancel\n * ▁▂▃▅▇▆▄▂▁▂▄▆▇▅▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ← level history, left-anchored on a track\n * › and so my fellow ameri▏ ← live partial transcript (PARTIAL)\n * ```\n *\n * Phases:\n * - `warming` spinner while the binary resolves / model loads; shows a\n * real progress bar when a download reports byte counts\n * - `listening` recording dot + elapsed timer + waveform + partial text\n * - `silence` trailing-silence countdown; waveform dims\n * - `transcribing` spinner while the final decode runs\n *\n * The component self-animates (spinner, timer, countdown) on an internal timer\n * and calls `ui.requestRender()`; the owner feeds it protocol events via\n * `pushLevel` / `setPartial` / `beginSilence` / `setDownloadProgress` etc. and\n * disposes it on collapse.\n */\n\nimport type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport { truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { renderDownloadProgress } from \"../components/progress-bar.js\";\nimport { theme } from \"../theme/theme.js\";\n\nexport type VoicePanelPhase = \"warming\" | \"listening\" | \"silence\" | \"transcribing\";\n\n/**\n * A rotation, deliberately not the two-frame `○`/`●` pulse the transcript\n * Loader uses.\n *\n * Frame count is functional here, not cosmetic. Two frames read as the line\n * switching on and off, so the Loader beats them slowly (640ms) to avoid a\n * strobe; enough frames to read as motion can run fast without flickering. This\n * panel animates at 100ms because it also drives the silence countdown, and a\n * two-frame pulse at that cadence is exactly the strobe the Loader avoids.\n */\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n// 8 filled levels; index 0 renders as a baseline dot so an empty history still\n// reads as \"a track\", not a blank line.\nconst WAVE_LEVELS = [\"▁\", \"▁\", \"▂\", \"▃\", \"▄\", \"▅\", \"▆\", \"▇\", \"█\"];\nconst TICK_MS = 100;\nconst HISTORY_CAP = 256;\n// RMS from voicetools' vad::rms sits well below 1.0 for speech; this gain maps a\n// normal speaking level to roughly the top of the meter without clipping constantly.\nconst LEVEL_GAIN = 6;\n// During trailing silence the binary keeps emitting low LEVELs; only an RMS above\n// this counts as the speaker resuming, so we don't cancel the countdown on noise.\nconst RESUME_RMS = 0.02;\n/** Cells in the download progress bar shown while the voicetools binary fetches. */\nconst PROGRESS_CELLS = 20;\n\n/** Map an RMS energy to a waveform block glyph. */\nfunction levelGlyph(rms: number): string {\n\tconst norm = Math.max(0, Math.min(1, rms * LEVEL_GAIN));\n\tconst idx = Math.round(norm * (WAVE_LEVELS.length - 1));\n\treturn WAVE_LEVELS[idx] ?? WAVE_LEVELS[0]!;\n}\n\n/** Keep the tail (most recent chars) of `text` within `max` visible columns. */\nfunction clampTail(text: string, max: number): string {\n\tif (max <= 0) return \"\";\n\tif (visibleWidth(text) <= max) return text;\n\t// Partial transcripts are effectively plain text; slice by chars from the end\n\t// and prefix an ellipsis so the newest words stay visible as it grows.\n\tconst tail = text.slice(-(max - 1));\n\treturn `…${tail}`;\n}\n\n/** `left` flush left, `right` flush right when it fits; otherwise just `left`. */\nfunction lineWithHint(\n\twidth: number,\n\tleftPlain: string,\n\tleftStyled: string,\n\thintPlain: string,\n\thintStyled: string,\n): string {\n\tconst lw = visibleWidth(leftPlain);\n\tif (hintPlain && lw + 2 + visibleWidth(hintPlain) <= width) {\n\t\treturn leftStyled + \" \".repeat(width - lw - visibleWidth(hintPlain)) + hintStyled;\n\t}\n\tif (lw <= width) return leftStyled;\n\treturn truncateToWidth(leftStyled, width, theme.fg(\"dim\", \"…\"));\n}\n\nexport class VoicePanel implements Component {\n\tprivate phase: VoicePanelPhase = \"warming\";\n\tprivate frame = 0;\n\tprivate levels: number[] = [];\n\tprivate partial = \"\";\n\tprivate listeningStartMs = 0;\n\tprivate silenceRemainingMs = 0;\n\tprivate warmingMessage = \"Warming up voice input…\";\n\tprivate warmingDetail: string | undefined;\n\tprivate downloadReceived = 0;\n\tprivate downloadTotal: number | null = null;\n\tprivate downloading = false;\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\n\tconstructor(\n\t\tprivate readonly ui: TUI | undefined,\n\t\tprivate readonly cancelHint: string,\n\t) {}\n\n\t// ---- lifecycle -------------------------------------------------------\n\n\tprivate ensureAnimating(): void {\n\t\tif (this.timer) return;\n\t\tthis.timer = setInterval(() => {\n\t\t\tthis.frame = (this.frame + 1) % SPINNER_FRAMES.length;\n\t\t\tif (this.phase === \"silence\" && this.silenceRemainingMs > 0) {\n\t\t\t\tthis.silenceRemainingMs = Math.max(0, this.silenceRemainingMs - TICK_MS);\n\t\t\t}\n\t\t\tthis.ui?.requestRender();\n\t\t}, TICK_MS);\n\t}\n\n\t/** Stop timers. The owner removes the panel from its container to collapse it. */\n\tdispose(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\t/** No cached render state to clear; every `render()` recomputes from live state. */\n\tinvalidate(): void {}\n\n\t// ---- phase transitions ----------------------------------------------\n\n\tsetWarming(message: string, detail?: string): void {\n\t\tthis.phase = \"warming\";\n\t\tthis.warmingMessage = message;\n\t\tthis.warmingDetail = detail;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/**\n\t * Live byte counts while the voicetools binary downloads (first run only).\n\t * Turns the warming spinner into a determinate progress bar.\n\t */\n\tsetDownloadProgress(receivedBytes: number, totalBytes: number | null): void {\n\t\tthis.downloading = true;\n\t\tthis.downloadReceived = receivedBytes;\n\t\tthis.downloadTotal = totalBytes;\n\t\tthis.ui?.requestRender();\n\t}\n\n\tstartListening(): void {\n\t\tthis.phase = \"listening\";\n\t\tthis.downloading = false;\n\t\tthis.levels = [];\n\t\tthis.partial = \"\";\n\t\tthis.listeningStartMs = Date.now();\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tbeginSilence(totalMs: number): void {\n\t\tthis.phase = \"silence\";\n\t\tthis.silenceRemainingMs = totalMs;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/** Speech resumed before cutoff: return to the listening phase. */\n\tendSilence(): void {\n\t\tif (this.phase === \"silence\") {\n\t\t\tthis.phase = \"listening\";\n\t\t\tthis.ui?.requestRender();\n\t\t}\n\t}\n\n\tsetTranscribing(): void {\n\t\tthis.phase = \"transcribing\";\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- data feed -------------------------------------------------------\n\n\tpushLevel(rms: number): void {\n\t\tthis.levels.push(rms);\n\t\tif (this.levels.length > HISTORY_CAP) this.levels.shift();\n\t\t// Low levels keep flowing during the silence window; only a loud one means\n\t\t// the speaker resumed and the countdown should be abandoned.\n\t\tif (this.phase === \"silence\" && rms > RESUME_RMS) this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tsetPartial(text: string): void {\n\t\tthis.partial = text;\n\t\tif (this.phase === \"silence\") this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- rendering -------------------------------------------------------\n\n\t/**\n\t * Level history, LEFT-anchored: the meter grows from the left edge and the\n\t * unfilled remainder renders as a dim baseline track, so the first seconds of\n\t * a capture read as a meter filling — not a cluster of glyphs floating at the\n\t * far right edge over a void (the old right-anchored layout). Once the\n\t * history exceeds the width it scrolls, keeping the newest samples visible.\n\t */\n\tprivate renderWaveform(width: number): string {\n\t\tconst cells = Math.max(1, width - 2);\n\t\tconst recent = this.levels.slice(-cells);\n\t\tconst wave = recent.map(levelGlyph).join(\"\");\n\t\tconst track = \"▁\".repeat(Math.max(0, cells - recent.length));\n\t\tconst waveStyled = this.phase === \"silence\" ? theme.fg(\"dim\", wave) : theme.fg(\"accent\", wave);\n\t\treturn waveStyled + theme.fg(\"dim\", track);\n\t}\n\n\tprivate renderPartial(width: number): string | undefined {\n\t\tif (!this.partial) return undefined;\n\t\tconst body = clampTail(this.partial, Math.max(1, width - 4));\n\t\tconst cursor = this.phase === \"transcribing\" ? \"\" : theme.blink(theme.fg(\"accent\", \"▏\"));\n\t\treturn ` ${theme.fg(\"muted\", \"›\")} ${theme.fg(\"dim\", body)}${cursor}`;\n\t}\n\n\tprivate elapsedLabel(): string {\n\t\tconst secs = Math.floor((Date.now() - this.listeningStartMs) / 1000);\n\t\tconst m = Math.floor(secs / 60);\n\t\tconst s = secs % 60;\n\t\treturn `${m}:${s.toString().padStart(2, \"0\")}`;\n\t}\n\n\t/**\n\t * The model download's progress, rendered by the shared progress-bar\n\t * component so this panel and the footer always agree. The panel is wider\n\t * than a footer line, so it asks for a finer track — width is the only thing\n\t * a surface gets to choose.\n\t */\n\tprivate renderDownloadBar(): string {\n\t\treturn ` ${renderDownloadProgress(this.downloadReceived, this.downloadTotal, { cells: PROGRESS_CELLS })}`;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst spinner = SPINNER_FRAMES[this.frame] ?? SPINNER_FRAMES[0]!;\n\t\tconst hintPlain = this.cancelHint;\n\t\tconst hintStyled = theme.fg(\"dim\", this.cancelHint);\n\t\tconst lines: string[] = [\"\"];\n\n\t\tif (this.phase === \"warming\") {\n\t\t\tconst headPlain = ` ${spinner} ${this.warmingMessage}`;\n\t\t\tconst headStyled = ` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"text\", this.warmingMessage)}`;\n\t\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\t\tif (this.downloading) {\n\t\t\t\tlines.push(this.renderDownloadBar());\n\t\t\t}\n\t\t\tif (this.warmingDetail) {\n\t\t\t\tlines.push(` ${theme.fg(\"dim\", this.warmingDetail)}`);\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.phase === \"transcribing\") {\n\t\t\tconst headPlain = ` ${spinner} Transcribing…`;\n\t\t\tconst headStyled = ` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"accent\", \"Transcribing…\")}`;\n\t\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\t\tconst partial = this.renderPartial(width);\n\t\t\tif (partial) lines.push(partial);\n\t\t\treturn lines;\n\t\t}\n\n\t\t// listening / silence. A single-cell recording dot (●, error red = \"live\")\n\t\t// replaces the old emoji mic, whose emoji-presentation width drift was the\n\t\t// reason for a VS16 workaround; ● needs no such care.\n\t\tconst dot = theme.fg(\"error\", \"●\");\n\t\tlet headPlain = ` ● Listening · ${this.elapsedLabel()}`;\n\t\tlet headStyled = ` ${dot} ${theme.fg(\"text\", \"Listening\")} ${theme.fg(\"dim\", `· ${this.elapsedLabel()}`)}`;\n\t\tif (this.phase === \"silence\") {\n\t\t\tconst remaining = (this.silenceRemainingMs / 1000).toFixed(1);\n\t\t\theadPlain += ` · cutting off in ${remaining}s`;\n\t\t\theadStyled += ` ${theme.fg(\"warning\", `· cutting off in ${remaining}s`)}`;\n\t\t}\n\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\tlines.push(` ${this.renderWaveform(width)}`);\n\t\tconst partial = this.renderPartial(width);\n\t\tif (partial) lines.push(partial);\n\t\treturn lines;\n\t}\n}\n"]}
|
|
@@ -23,7 +23,18 @@
|
|
|
23
23
|
* disposes it on collapse.
|
|
24
24
|
*/
|
|
25
25
|
import { truncateToWidth, visibleWidth } from "@kolisachint/hoocode-tui";
|
|
26
|
+
import { renderDownloadProgress } from "../components/progress-bar.js";
|
|
26
27
|
import { theme } from "../theme/theme.js";
|
|
28
|
+
/**
|
|
29
|
+
* A rotation, deliberately not the two-frame `○`/`●` pulse the transcript
|
|
30
|
+
* Loader uses.
|
|
31
|
+
*
|
|
32
|
+
* Frame count is functional here, not cosmetic. Two frames read as the line
|
|
33
|
+
* switching on and off, so the Loader beats them slowly (640ms) to avoid a
|
|
34
|
+
* strobe; enough frames to read as motion can run fast without flickering. This
|
|
35
|
+
* panel animates at 100ms because it also drives the silence countdown, and a
|
|
36
|
+
* two-frame pulse at that cadence is exactly the strobe the Loader avoids.
|
|
37
|
+
*/
|
|
27
38
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
28
39
|
// 8 filled levels; index 0 renders as a baseline dot so an empty history still
|
|
29
40
|
// reads as "a track", not a blank line.
|
|
@@ -65,9 +76,6 @@ function lineWithHint(width, leftPlain, leftStyled, hintPlain, hintStyled) {
|
|
|
65
76
|
return leftStyled;
|
|
66
77
|
return truncateToWidth(leftStyled, width, theme.fg("dim", "…"));
|
|
67
78
|
}
|
|
68
|
-
function formatMb(bytes) {
|
|
69
|
-
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
70
|
-
}
|
|
71
79
|
export class VoicePanel {
|
|
72
80
|
ui;
|
|
73
81
|
cancelHint;
|
|
@@ -199,18 +207,14 @@ export class VoicePanel {
|
|
|
199
207
|
const s = secs % 60;
|
|
200
208
|
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
201
209
|
}
|
|
202
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* The model download's progress, rendered by the shared progress-bar
|
|
212
|
+
* component so this panel and the footer always agree. The panel is wider
|
|
213
|
+
* than a footer line, so it asks for a finer track — width is the only thing
|
|
214
|
+
* a surface gets to choose.
|
|
215
|
+
*/
|
|
203
216
|
renderDownloadBar() {
|
|
204
|
-
|
|
205
|
-
if (total === null || total <= 0) {
|
|
206
|
-
return ` ${theme.fg("dim", `downloaded ${formatMb(this.downloadReceived)}…`)}`;
|
|
207
|
-
}
|
|
208
|
-
const ratio = Math.max(0, Math.min(1, this.downloadReceived / total));
|
|
209
|
-
const filled = Math.round(ratio * PROGRESS_CELLS);
|
|
210
|
-
const bar = theme.fg("accent", "·".repeat(filled)) + theme.fg("dim", "·".repeat(PROGRESS_CELLS - filled));
|
|
211
|
-
const pct = `${Math.round(ratio * 100)}%`;
|
|
212
|
-
const sizes = `${formatMb(this.downloadReceived)} / ${formatMb(total)}`;
|
|
213
|
-
return ` ${bar} ${theme.fg("muted", pct)} ${theme.fg("dim", `· ${sizes}`)}`;
|
|
217
|
+
return ` ${renderDownloadProgress(this.downloadReceived, this.downloadTotal, { cells: PROGRESS_CELLS })}`;
|
|
214
218
|
}
|
|
215
219
|
render(width) {
|
|
216
220
|
const spinner = SPINNER_FRAMES[this.frame] ?? SPINNER_FRAMES[0];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-panel.js","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-panel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Multi-line voice-input status panel.\n *\n * Renders the live state of a `voicetools serve` capture across its phases so\n * the user always sees what's happening:\n *\n * ```text\n * ● Listening · 0:03 esc cancel\n * ▁▂▃▅▇▆▄▂▁▂▄▆▇▅▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ← level history, left-anchored on a track\n * › and so my fellow ameri▏ ← live partial transcript (PARTIAL)\n * ```\n *\n * Phases:\n * - `warming` spinner while the binary resolves / model loads; shows a\n * real progress bar when a download reports byte counts\n * - `listening` recording dot + elapsed timer + waveform + partial text\n * - `silence` trailing-silence countdown; waveform dims\n * - `transcribing` spinner while the final decode runs\n *\n * The component self-animates (spinner, timer, countdown) on an internal timer\n * and calls `ui.requestRender()`; the owner feeds it protocol events via\n * `pushLevel` / `setPartial` / `beginSilence` / `setDownloadProgress` etc. and\n * disposes it on collapse.\n */\n\nimport type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport { truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { theme } from \"../theme/theme.js\";\n\nexport type VoicePanelPhase = \"warming\" | \"listening\" | \"silence\" | \"transcribing\";\n\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n// 8 filled levels; index 0 renders as a baseline dot so an empty history still\n// reads as \"a track\", not a blank line.\nconst WAVE_LEVELS = [\"▁\", \"▁\", \"▂\", \"▃\", \"▄\", \"▅\", \"▆\", \"▇\", \"█\"];\nconst TICK_MS = 100;\nconst HISTORY_CAP = 256;\n// RMS from voicetools' vad::rms sits well below 1.0 for speech; this gain maps a\n// normal speaking level to roughly the top of the meter without clipping constantly.\nconst LEVEL_GAIN = 6;\n// During trailing silence the binary keeps emitting low LEVELs; only an RMS above\n// this counts as the speaker resuming, so we don't cancel the countdown on noise.\nconst RESUME_RMS = 0.02;\n/** Cells in the download progress bar shown while the voicetools binary fetches. */\nconst PROGRESS_CELLS = 20;\n\n/** Map an RMS energy to a waveform block glyph. */\nfunction levelGlyph(rms: number): string {\n\tconst norm = Math.max(0, Math.min(1, rms * LEVEL_GAIN));\n\tconst idx = Math.round(norm * (WAVE_LEVELS.length - 1));\n\treturn WAVE_LEVELS[idx] ?? WAVE_LEVELS[0]!;\n}\n\n/** Keep the tail (most recent chars) of `text` within `max` visible columns. */\nfunction clampTail(text: string, max: number): string {\n\tif (max <= 0) return \"\";\n\tif (visibleWidth(text) <= max) return text;\n\t// Partial transcripts are effectively plain text; slice by chars from the end\n\t// and prefix an ellipsis so the newest words stay visible as it grows.\n\tconst tail = text.slice(-(max - 1));\n\treturn `…${tail}`;\n}\n\n/** `left` flush left, `right` flush right when it fits; otherwise just `left`. */\nfunction lineWithHint(\n\twidth: number,\n\tleftPlain: string,\n\tleftStyled: string,\n\thintPlain: string,\n\thintStyled: string,\n): string {\n\tconst lw = visibleWidth(leftPlain);\n\tif (hintPlain && lw + 2 + visibleWidth(hintPlain) <= width) {\n\t\treturn leftStyled + \" \".repeat(width - lw - visibleWidth(hintPlain)) + hintStyled;\n\t}\n\tif (lw <= width) return leftStyled;\n\treturn truncateToWidth(leftStyled, width, theme.fg(\"dim\", \"…\"));\n}\n\nfunction formatMb(bytes: number): string {\n\treturn `${Math.round(bytes / (1024 * 1024))} MB`;\n}\n\nexport class VoicePanel implements Component {\n\tprivate phase: VoicePanelPhase = \"warming\";\n\tprivate frame = 0;\n\tprivate levels: number[] = [];\n\tprivate partial = \"\";\n\tprivate listeningStartMs = 0;\n\tprivate silenceRemainingMs = 0;\n\tprivate warmingMessage = \"Warming up voice input…\";\n\tprivate warmingDetail: string | undefined;\n\tprivate downloadReceived = 0;\n\tprivate downloadTotal: number | null = null;\n\tprivate downloading = false;\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\n\tconstructor(\n\t\tprivate readonly ui: TUI | undefined,\n\t\tprivate readonly cancelHint: string,\n\t) {}\n\n\t// ---- lifecycle -------------------------------------------------------\n\n\tprivate ensureAnimating(): void {\n\t\tif (this.timer) return;\n\t\tthis.timer = setInterval(() => {\n\t\t\tthis.frame = (this.frame + 1) % SPINNER_FRAMES.length;\n\t\t\tif (this.phase === \"silence\" && this.silenceRemainingMs > 0) {\n\t\t\t\tthis.silenceRemainingMs = Math.max(0, this.silenceRemainingMs - TICK_MS);\n\t\t\t}\n\t\t\tthis.ui?.requestRender();\n\t\t}, TICK_MS);\n\t}\n\n\t/** Stop timers. The owner removes the panel from its container to collapse it. */\n\tdispose(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\t/** No cached render state to clear; every `render()` recomputes from live state. */\n\tinvalidate(): void {}\n\n\t// ---- phase transitions ----------------------------------------------\n\n\tsetWarming(message: string, detail?: string): void {\n\t\tthis.phase = \"warming\";\n\t\tthis.warmingMessage = message;\n\t\tthis.warmingDetail = detail;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/**\n\t * Live byte counts while the voicetools binary downloads (first run only).\n\t * Turns the warming spinner into a determinate progress bar.\n\t */\n\tsetDownloadProgress(receivedBytes: number, totalBytes: number | null): void {\n\t\tthis.downloading = true;\n\t\tthis.downloadReceived = receivedBytes;\n\t\tthis.downloadTotal = totalBytes;\n\t\tthis.ui?.requestRender();\n\t}\n\n\tstartListening(): void {\n\t\tthis.phase = \"listening\";\n\t\tthis.downloading = false;\n\t\tthis.levels = [];\n\t\tthis.partial = \"\";\n\t\tthis.listeningStartMs = Date.now();\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tbeginSilence(totalMs: number): void {\n\t\tthis.phase = \"silence\";\n\t\tthis.silenceRemainingMs = totalMs;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/** Speech resumed before cutoff: return to the listening phase. */\n\tendSilence(): void {\n\t\tif (this.phase === \"silence\") {\n\t\t\tthis.phase = \"listening\";\n\t\t\tthis.ui?.requestRender();\n\t\t}\n\t}\n\n\tsetTranscribing(): void {\n\t\tthis.phase = \"transcribing\";\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- data feed -------------------------------------------------------\n\n\tpushLevel(rms: number): void {\n\t\tthis.levels.push(rms);\n\t\tif (this.levels.length > HISTORY_CAP) this.levels.shift();\n\t\t// Low levels keep flowing during the silence window; only a loud one means\n\t\t// the speaker resumed and the countdown should be abandoned.\n\t\tif (this.phase === \"silence\" && rms > RESUME_RMS) this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tsetPartial(text: string): void {\n\t\tthis.partial = text;\n\t\tif (this.phase === \"silence\") this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- rendering -------------------------------------------------------\n\n\t/**\n\t * Level history, LEFT-anchored: the meter grows from the left edge and the\n\t * unfilled remainder renders as a dim baseline track, so the first seconds of\n\t * a capture read as a meter filling — not a cluster of glyphs floating at the\n\t * far right edge over a void (the old right-anchored layout). Once the\n\t * history exceeds the width it scrolls, keeping the newest samples visible.\n\t */\n\tprivate renderWaveform(width: number): string {\n\t\tconst cells = Math.max(1, width - 2);\n\t\tconst recent = this.levels.slice(-cells);\n\t\tconst wave = recent.map(levelGlyph).join(\"\");\n\t\tconst track = \"▁\".repeat(Math.max(0, cells - recent.length));\n\t\tconst waveStyled = this.phase === \"silence\" ? theme.fg(\"dim\", wave) : theme.fg(\"accent\", wave);\n\t\treturn waveStyled + theme.fg(\"dim\", track);\n\t}\n\n\tprivate renderPartial(width: number): string | undefined {\n\t\tif (!this.partial) return undefined;\n\t\tconst body = clampTail(this.partial, Math.max(1, width - 4));\n\t\tconst cursor = this.phase === \"transcribing\" ? \"\" : theme.blink(theme.fg(\"accent\", \"▏\"));\n\t\treturn ` ${theme.fg(\"muted\", \"›\")} ${theme.fg(\"dim\", body)}${cursor}`;\n\t}\n\n\tprivate elapsedLabel(): string {\n\t\tconst secs = Math.floor((Date.now() - this.listeningStartMs) / 1000);\n\t\tconst m = Math.floor(secs / 60);\n\t\tconst s = secs % 60;\n\t\treturn `${m}:${s.toString().padStart(2, \"0\")}`;\n\t}\n\n\t/** Determinate download bar: `·` fill over a dim track with % and MB counts. */\n\tprivate renderDownloadBar(): string {\n\t\tconst total = this.downloadTotal;\n\t\tif (total === null || total <= 0) {\n\t\t\treturn ` ${theme.fg(\"dim\", `downloaded ${formatMb(this.downloadReceived)}…`)}`;\n\t\t}\n\t\tconst ratio = Math.max(0, Math.min(1, this.downloadReceived / total));\n\t\tconst filled = Math.round(ratio * PROGRESS_CELLS);\n\t\tconst bar = theme.fg(\"accent\", \"·\".repeat(filled)) + theme.fg(\"dim\", \"·\".repeat(PROGRESS_CELLS - filled));\n\t\tconst pct = `${Math.round(ratio * 100)}%`;\n\t\tconst sizes = `${formatMb(this.downloadReceived)} / ${formatMb(total)}`;\n\t\treturn ` ${bar} ${theme.fg(\"muted\", pct)} ${theme.fg(\"dim\", `· ${sizes}`)}`;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst spinner = SPINNER_FRAMES[this.frame] ?? SPINNER_FRAMES[0]!;\n\t\tconst hintPlain = this.cancelHint;\n\t\tconst hintStyled = theme.fg(\"dim\", this.cancelHint);\n\t\tconst lines: string[] = [\"\"];\n\n\t\tif (this.phase === \"warming\") {\n\t\t\tconst headPlain = ` ${spinner} ${this.warmingMessage}`;\n\t\t\tconst headStyled = ` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"text\", this.warmingMessage)}`;\n\t\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\t\tif (this.downloading) {\n\t\t\t\tlines.push(this.renderDownloadBar());\n\t\t\t}\n\t\t\tif (this.warmingDetail) {\n\t\t\t\tlines.push(` ${theme.fg(\"dim\", this.warmingDetail)}`);\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.phase === \"transcribing\") {\n\t\t\tconst headPlain = ` ${spinner} Transcribing…`;\n\t\t\tconst headStyled = ` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"accent\", \"Transcribing…\")}`;\n\t\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\t\tconst partial = this.renderPartial(width);\n\t\t\tif (partial) lines.push(partial);\n\t\t\treturn lines;\n\t\t}\n\n\t\t// listening / silence. A single-cell recording dot (●, error red = \"live\")\n\t\t// replaces the old emoji mic, whose emoji-presentation width drift was the\n\t\t// reason for a VS16 workaround; ● needs no such care.\n\t\tconst dot = theme.fg(\"error\", \"●\");\n\t\tlet headPlain = ` ● Listening · ${this.elapsedLabel()}`;\n\t\tlet headStyled = ` ${dot} ${theme.fg(\"text\", \"Listening\")} ${theme.fg(\"dim\", `· ${this.elapsedLabel()}`)}`;\n\t\tif (this.phase === \"silence\") {\n\t\t\tconst remaining = (this.silenceRemainingMs / 1000).toFixed(1);\n\t\t\theadPlain += ` · cutting off in ${remaining}s`;\n\t\t\theadStyled += ` ${theme.fg(\"warning\", `· cutting off in ${remaining}s`)}`;\n\t\t}\n\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\tlines.push(` ${this.renderWaveform(width)}`);\n\t\tconst partial = this.renderPartial(width);\n\t\tif (partial) lines.push(partial);\n\t\treturn lines;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"voice-panel.js","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-panel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Multi-line voice-input status panel.\n *\n * Renders the live state of a `voicetools serve` capture across its phases so\n * the user always sees what's happening:\n *\n * ```text\n * ● Listening · 0:03 esc cancel\n * ▁▂▃▅▇▆▄▂▁▂▄▆▇▅▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ← level history, left-anchored on a track\n * › and so my fellow ameri▏ ← live partial transcript (PARTIAL)\n * ```\n *\n * Phases:\n * - `warming` spinner while the binary resolves / model loads; shows a\n * real progress bar when a download reports byte counts\n * - `listening` recording dot + elapsed timer + waveform + partial text\n * - `silence` trailing-silence countdown; waveform dims\n * - `transcribing` spinner while the final decode runs\n *\n * The component self-animates (spinner, timer, countdown) on an internal timer\n * and calls `ui.requestRender()`; the owner feeds it protocol events via\n * `pushLevel` / `setPartial` / `beginSilence` / `setDownloadProgress` etc. and\n * disposes it on collapse.\n */\n\nimport type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport { truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { renderDownloadProgress } from \"../components/progress-bar.js\";\nimport { theme } from \"../theme/theme.js\";\n\nexport type VoicePanelPhase = \"warming\" | \"listening\" | \"silence\" | \"transcribing\";\n\n/**\n * A rotation, deliberately not the two-frame `○`/`●` pulse the transcript\n * Loader uses.\n *\n * Frame count is functional here, not cosmetic. Two frames read as the line\n * switching on and off, so the Loader beats them slowly (640ms) to avoid a\n * strobe; enough frames to read as motion can run fast without flickering. This\n * panel animates at 100ms because it also drives the silence countdown, and a\n * two-frame pulse at that cadence is exactly the strobe the Loader avoids.\n */\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n// 8 filled levels; index 0 renders as a baseline dot so an empty history still\n// reads as \"a track\", not a blank line.\nconst WAVE_LEVELS = [\"▁\", \"▁\", \"▂\", \"▃\", \"▄\", \"▅\", \"▆\", \"▇\", \"█\"];\nconst TICK_MS = 100;\nconst HISTORY_CAP = 256;\n// RMS from voicetools' vad::rms sits well below 1.0 for speech; this gain maps a\n// normal speaking level to roughly the top of the meter without clipping constantly.\nconst LEVEL_GAIN = 6;\n// During trailing silence the binary keeps emitting low LEVELs; only an RMS above\n// this counts as the speaker resuming, so we don't cancel the countdown on noise.\nconst RESUME_RMS = 0.02;\n/** Cells in the download progress bar shown while the voicetools binary fetches. */\nconst PROGRESS_CELLS = 20;\n\n/** Map an RMS energy to a waveform block glyph. */\nfunction levelGlyph(rms: number): string {\n\tconst norm = Math.max(0, Math.min(1, rms * LEVEL_GAIN));\n\tconst idx = Math.round(norm * (WAVE_LEVELS.length - 1));\n\treturn WAVE_LEVELS[idx] ?? WAVE_LEVELS[0]!;\n}\n\n/** Keep the tail (most recent chars) of `text` within `max` visible columns. */\nfunction clampTail(text: string, max: number): string {\n\tif (max <= 0) return \"\";\n\tif (visibleWidth(text) <= max) return text;\n\t// Partial transcripts are effectively plain text; slice by chars from the end\n\t// and prefix an ellipsis so the newest words stay visible as it grows.\n\tconst tail = text.slice(-(max - 1));\n\treturn `…${tail}`;\n}\n\n/** `left` flush left, `right` flush right when it fits; otherwise just `left`. */\nfunction lineWithHint(\n\twidth: number,\n\tleftPlain: string,\n\tleftStyled: string,\n\thintPlain: string,\n\thintStyled: string,\n): string {\n\tconst lw = visibleWidth(leftPlain);\n\tif (hintPlain && lw + 2 + visibleWidth(hintPlain) <= width) {\n\t\treturn leftStyled + \" \".repeat(width - lw - visibleWidth(hintPlain)) + hintStyled;\n\t}\n\tif (lw <= width) return leftStyled;\n\treturn truncateToWidth(leftStyled, width, theme.fg(\"dim\", \"…\"));\n}\n\nexport class VoicePanel implements Component {\n\tprivate phase: VoicePanelPhase = \"warming\";\n\tprivate frame = 0;\n\tprivate levels: number[] = [];\n\tprivate partial = \"\";\n\tprivate listeningStartMs = 0;\n\tprivate silenceRemainingMs = 0;\n\tprivate warmingMessage = \"Warming up voice input…\";\n\tprivate warmingDetail: string | undefined;\n\tprivate downloadReceived = 0;\n\tprivate downloadTotal: number | null = null;\n\tprivate downloading = false;\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\n\tconstructor(\n\t\tprivate readonly ui: TUI | undefined,\n\t\tprivate readonly cancelHint: string,\n\t) {}\n\n\t// ---- lifecycle -------------------------------------------------------\n\n\tprivate ensureAnimating(): void {\n\t\tif (this.timer) return;\n\t\tthis.timer = setInterval(() => {\n\t\t\tthis.frame = (this.frame + 1) % SPINNER_FRAMES.length;\n\t\t\tif (this.phase === \"silence\" && this.silenceRemainingMs > 0) {\n\t\t\t\tthis.silenceRemainingMs = Math.max(0, this.silenceRemainingMs - TICK_MS);\n\t\t\t}\n\t\t\tthis.ui?.requestRender();\n\t\t}, TICK_MS);\n\t}\n\n\t/** Stop timers. The owner removes the panel from its container to collapse it. */\n\tdispose(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\t/** No cached render state to clear; every `render()` recomputes from live state. */\n\tinvalidate(): void {}\n\n\t// ---- phase transitions ----------------------------------------------\n\n\tsetWarming(message: string, detail?: string): void {\n\t\tthis.phase = \"warming\";\n\t\tthis.warmingMessage = message;\n\t\tthis.warmingDetail = detail;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/**\n\t * Live byte counts while the voicetools binary downloads (first run only).\n\t * Turns the warming spinner into a determinate progress bar.\n\t */\n\tsetDownloadProgress(receivedBytes: number, totalBytes: number | null): void {\n\t\tthis.downloading = true;\n\t\tthis.downloadReceived = receivedBytes;\n\t\tthis.downloadTotal = totalBytes;\n\t\tthis.ui?.requestRender();\n\t}\n\n\tstartListening(): void {\n\t\tthis.phase = \"listening\";\n\t\tthis.downloading = false;\n\t\tthis.levels = [];\n\t\tthis.partial = \"\";\n\t\tthis.listeningStartMs = Date.now();\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tbeginSilence(totalMs: number): void {\n\t\tthis.phase = \"silence\";\n\t\tthis.silenceRemainingMs = totalMs;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/** Speech resumed before cutoff: return to the listening phase. */\n\tendSilence(): void {\n\t\tif (this.phase === \"silence\") {\n\t\t\tthis.phase = \"listening\";\n\t\t\tthis.ui?.requestRender();\n\t\t}\n\t}\n\n\tsetTranscribing(): void {\n\t\tthis.phase = \"transcribing\";\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- data feed -------------------------------------------------------\n\n\tpushLevel(rms: number): void {\n\t\tthis.levels.push(rms);\n\t\tif (this.levels.length > HISTORY_CAP) this.levels.shift();\n\t\t// Low levels keep flowing during the silence window; only a loud one means\n\t\t// the speaker resumed and the countdown should be abandoned.\n\t\tif (this.phase === \"silence\" && rms > RESUME_RMS) this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tsetPartial(text: string): void {\n\t\tthis.partial = text;\n\t\tif (this.phase === \"silence\") this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- rendering -------------------------------------------------------\n\n\t/**\n\t * Level history, LEFT-anchored: the meter grows from the left edge and the\n\t * unfilled remainder renders as a dim baseline track, so the first seconds of\n\t * a capture read as a meter filling — not a cluster of glyphs floating at the\n\t * far right edge over a void (the old right-anchored layout). Once the\n\t * history exceeds the width it scrolls, keeping the newest samples visible.\n\t */\n\tprivate renderWaveform(width: number): string {\n\t\tconst cells = Math.max(1, width - 2);\n\t\tconst recent = this.levels.slice(-cells);\n\t\tconst wave = recent.map(levelGlyph).join(\"\");\n\t\tconst track = \"▁\".repeat(Math.max(0, cells - recent.length));\n\t\tconst waveStyled = this.phase === \"silence\" ? theme.fg(\"dim\", wave) : theme.fg(\"accent\", wave);\n\t\treturn waveStyled + theme.fg(\"dim\", track);\n\t}\n\n\tprivate renderPartial(width: number): string | undefined {\n\t\tif (!this.partial) return undefined;\n\t\tconst body = clampTail(this.partial, Math.max(1, width - 4));\n\t\tconst cursor = this.phase === \"transcribing\" ? \"\" : theme.blink(theme.fg(\"accent\", \"▏\"));\n\t\treturn ` ${theme.fg(\"muted\", \"›\")} ${theme.fg(\"dim\", body)}${cursor}`;\n\t}\n\n\tprivate elapsedLabel(): string {\n\t\tconst secs = Math.floor((Date.now() - this.listeningStartMs) / 1000);\n\t\tconst m = Math.floor(secs / 60);\n\t\tconst s = secs % 60;\n\t\treturn `${m}:${s.toString().padStart(2, \"0\")}`;\n\t}\n\n\t/**\n\t * The model download's progress, rendered by the shared progress-bar\n\t * component so this panel and the footer always agree. The panel is wider\n\t * than a footer line, so it asks for a finer track — width is the only thing\n\t * a surface gets to choose.\n\t */\n\tprivate renderDownloadBar(): string {\n\t\treturn ` ${renderDownloadProgress(this.downloadReceived, this.downloadTotal, { cells: PROGRESS_CELLS })}`;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst spinner = SPINNER_FRAMES[this.frame] ?? SPINNER_FRAMES[0]!;\n\t\tconst hintPlain = this.cancelHint;\n\t\tconst hintStyled = theme.fg(\"dim\", this.cancelHint);\n\t\tconst lines: string[] = [\"\"];\n\n\t\tif (this.phase === \"warming\") {\n\t\t\tconst headPlain = ` ${spinner} ${this.warmingMessage}`;\n\t\t\tconst headStyled = ` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"text\", this.warmingMessage)}`;\n\t\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\t\tif (this.downloading) {\n\t\t\t\tlines.push(this.renderDownloadBar());\n\t\t\t}\n\t\t\tif (this.warmingDetail) {\n\t\t\t\tlines.push(` ${theme.fg(\"dim\", this.warmingDetail)}`);\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.phase === \"transcribing\") {\n\t\t\tconst headPlain = ` ${spinner} Transcribing…`;\n\t\t\tconst headStyled = ` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"accent\", \"Transcribing…\")}`;\n\t\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\t\tconst partial = this.renderPartial(width);\n\t\t\tif (partial) lines.push(partial);\n\t\t\treturn lines;\n\t\t}\n\n\t\t// listening / silence. A single-cell recording dot (●, error red = \"live\")\n\t\t// replaces the old emoji mic, whose emoji-presentation width drift was the\n\t\t// reason for a VS16 workaround; ● needs no such care.\n\t\tconst dot = theme.fg(\"error\", \"●\");\n\t\tlet headPlain = ` ● Listening · ${this.elapsedLabel()}`;\n\t\tlet headStyled = ` ${dot} ${theme.fg(\"text\", \"Listening\")} ${theme.fg(\"dim\", `· ${this.elapsedLabel()}`)}`;\n\t\tif (this.phase === \"silence\") {\n\t\t\tconst remaining = (this.silenceRemainingMs / 1000).toFixed(1);\n\t\t\theadPlain += ` · cutting off in ${remaining}s`;\n\t\t\theadStyled += ` ${theme.fg(\"warning\", `· cutting off in ${remaining}s`)}`;\n\t\t}\n\t\tlines.push(lineWithHint(width, headPlain, headStyled, hintPlain, hintStyled));\n\t\tlines.push(` ${this.renderWaveform(width)}`);\n\t\tconst partial = this.renderPartial(width);\n\t\tif (partial) lines.push(partial);\n\t\treturn lines;\n\t}\n}\n"]}
|
package/docs/settings.md
CHANGED
|
@@ -196,8 +196,8 @@ the session directory being read.
|
|
|
196
196
|
|---------|------|---------|-------------|
|
|
197
197
|
| `learnMaxSessions` | number | `20` | Recent sessions in this directory to scan |
|
|
198
198
|
| `learnMaxAgeDays` | number | `30` | Ignore sessions older than this |
|
|
199
|
-
| `learnMinRepeats` | number | `2` |
|
|
200
|
-
| `
|
|
199
|
+
| `learnMinRepeats` | number | `2` | Separate sessions a directive must recur in before it is proposed |
|
|
200
|
+
| `learnMinRequestRepeats` | number | `3` | Separate sessions a piece of work must be asked for before it is proposed as a slash command |
|
|
201
201
|
| `learnMaxProposals` | number | `8` | Cap on each list in the digest |
|
|
202
202
|
|
|
203
203
|
```json
|
|
@@ -209,10 +209,13 @@ reaches the repeat threshold. Narrow it on one you work in daily, where the last
|
|
|
209
209
|
few weeks are the only relevant history. `learnMinRepeats` is the signal/noise
|
|
210
210
|
dial: raise it for fewer, better-evidenced proposals.
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
Both thresholds count *distinct sessions*, not occurrences: saying a thing twice
|
|
213
|
+
in one sitting usually means it was ignored the first time, which is evidence
|
|
214
|
+
about that afternoon rather than about how you work.
|
|
215
|
+
`learnMinRequestRepeats` is higher than `learnMinRepeats` on purpose — a rule
|
|
216
|
+
stated twice is a rule, but a job asked for twice may just be a job that came up
|
|
217
|
+
twice. `learnMaxProposals` bounds what a single run can ask you to review; every
|
|
218
|
+
proposal costs the model context.
|
|
216
219
|
|
|
217
220
|
Non-numeric or non-positive values fall back to the default rather than
|
|
218
221
|
narrowing the window to nothing. As with all settings, a project
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolisachint/hoocode-agent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.19",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"hoocodeConfig": {
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"prepublishOnly": "npm run clean && npm run build"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@kolisachint/hoocode-agent-core": "^0.5.
|
|
53
|
-
"@kolisachint/hoocode-ai": "^0.5.
|
|
54
|
-
"@kolisachint/hoocode-tui": "^0.5.
|
|
52
|
+
"@kolisachint/hoocode-agent-core": "^0.5.19",
|
|
53
|
+
"@kolisachint/hoocode-ai": "^0.5.19",
|
|
54
|
+
"@kolisachint/hoocode-tui": "^0.5.19",
|
|
55
55
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
|
56
56
|
"chalk": "^5.5.0",
|
|
57
57
|
"cli-highlight": "^2.1.11",
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Normalization and noise filters for the `/learn` extractor.
|
|
3
|
-
*
|
|
4
|
-
* Everything downstream — clustering, dedupe, cross-session counting — is only
|
|
5
|
-
* as good as what happens here. Two runs of the same failing command differ by
|
|
6
|
-
* absolute paths, line numbers, PIDs and timings; two statements of the same
|
|
7
|
-
* rule differ by politeness and word order. Collapsing that variance is what
|
|
8
|
-
* turns a pile of transcripts into counts, and the counts are the product.
|
|
9
|
-
*
|
|
10
|
-
* These are deliberately lexical and cheap. Semantic grouping is left to the
|
|
11
|
-
* model, which sees the deduped candidates and can tell that "run tests with
|
|
12
|
-
* coverage" and "use --coverage on the suite" are one rule. Trying to do that
|
|
13
|
-
* here would cost a dependency and still be worse.
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Collapse a raw error or command output into a comparable signature.
|
|
17
|
-
*
|
|
18
|
-
* Case is preserved: `TS2307` and `ERR_MODULE_NOT_FOUND` carry meaning that
|
|
19
|
-
* lowercasing would blur into surrounding prose, and they are exactly the
|
|
20
|
-
* tokens worth matching on.
|
|
21
|
-
*/
|
|
22
|
-
export declare function normalizeErrorSignature(text: string): string;
|
|
23
|
-
/**
|
|
24
|
-
* Normalize a shell command so two invocations of "the same" command match.
|
|
25
|
-
*
|
|
26
|
-
* Paths are collapsed but flags are kept — `npm test` and `npm test --coverage`
|
|
27
|
-
* are genuinely different commands, and flags are frequently the fix itself.
|
|
28
|
-
*/
|
|
29
|
-
export declare function normalizeCommand(command: string): string;
|
|
30
|
-
/**
|
|
31
|
-
* The first `tokens` words of what a command actually does, with the
|
|
32
|
-
* where-and-how prefixes stripped. `cd <path> && npm run check` becomes
|
|
33
|
-
* `npm run`, which is the part worth comparing across sessions.
|
|
34
|
-
*/
|
|
35
|
-
export declare function commandHead(command: string, tokens?: number): string;
|
|
36
|
-
export declare function extractErrorRegion(output: string, maxChars?: number): string;
|
|
37
|
-
/**
|
|
38
|
-
* Output that reports no failure worth learning from: an abort is the user
|
|
39
|
-
* changing their mind, and an empty result says nothing at all.
|
|
40
|
-
*/
|
|
41
|
-
export declare function isUninformativeFailure(output: string): boolean;
|
|
42
|
-
/**
|
|
43
|
-
* Normalize a user directive for clustering: lowercase, strip filler and
|
|
44
|
-
* punctuation, collapse whitespace. Unlike error signatures these are compared
|
|
45
|
-
* loosely, so case and symbols are noise.
|
|
46
|
-
*/
|
|
47
|
-
export declare function normalizeDirective(text: string): string;
|
|
48
|
-
/** Content words of a directive, for overlap comparisons against existing rules. */
|
|
49
|
-
export declare function contentWords(text: string): string[];
|
|
50
|
-
/**
|
|
51
|
-
* Fraction of `words` that appear in `haystack`, 0 when there is nothing to
|
|
52
|
-
* compare. Used to decide whether a proposed rule is already written down.
|
|
53
|
-
*/
|
|
54
|
-
export declare function wordOverlap(words: string[], haystack: string): number;
|
|
55
|
-
/**
|
|
56
|
-
* Whether a user message is worth mining as a potential rule.
|
|
57
|
-
*
|
|
58
|
-
* Deliberately strict. The extractor's output is read by a model with a limited
|
|
59
|
-
* budget, and recall costs precision: every task statement that slips through
|
|
60
|
-
* displaces a real recurring directive in the ranking.
|
|
61
|
-
*/
|
|
62
|
-
export declare function isRuleShapedDirective(text: string): boolean;
|
|
63
|
-
/** Whether a non-zero exit from this command should be ignored by the fix extractor. */
|
|
64
|
-
export declare function isBenignFailure(command: string): boolean;
|
|
65
|
-
//# sourceMappingURL=normalize.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../src/core/learn/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAwCH;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM5D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMxD;AAYD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAI,GAAG,MAAM,CAO/D;AAgBD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAM,GAAG,MAAM,CAOzE;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAI9D;AAuCD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWvD;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEnD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKrE;AAmBD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAU3D;AAYD,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGxD","sourcesContent":["/**\n * Normalization and noise filters for the `/learn` extractor.\n *\n * Everything downstream — clustering, dedupe, cross-session counting — is only\n * as good as what happens here. Two runs of the same failing command differ by\n * absolute paths, line numbers, PIDs and timings; two statements of the same\n * rule differ by politeness and word order. Collapsing that variance is what\n * turns a pile of transcripts into counts, and the counts are the product.\n *\n * These are deliberately lexical and cheap. Semantic grouping is left to the\n * model, which sees the deduped candidates and can tell that \"run tests with\n * coverage\" and \"use --coverage on the suite\" are one rule. Trying to do that\n * here would cost a dependency and still be worse.\n */\n\n/** Longest text we normalize; error dumps can run to megabytes. */\nconst MAX_SIGNATURE_CHARS = 400;\n\n/**\n * Volatile fragments that make two runs of the same failure look different.\n *\n * Order is load-bearing, because these patterns overlap and the first one to\n * match wins the characters. Longest-and-most-specific therefore runs first:\n * paths before line numbers so `foo.ts:12:3` does not leave a bare `:12:3`, and\n * timestamps before line numbers so the `:00:00` inside `10:00:00` is not\n * mistaken for a line/column pair. Adding a pattern means placing it by\n * specificity, not appending it.\n */\nconst VOLATILE_PATTERNS: Array<[RegExp, string]> = [\n\t// Absolute POSIX and Windows paths, including the temp dirs that change per run.\n\t[/\\/(?:[\\w.@+-]+\\/)+[\\w.@+-]+/g, \"<path>\"],\n\t[/[A-Za-z]:\\\\(?:[\\w.@+-]+\\\\)+[\\w.@+-]+/g, \"<path>\"],\n\t// UUIDs, before the shorter hex rule can eat their first group.\n\t[/\\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\b/gi, \"<uuid>\"],\n\t// ISO timestamps and clock times, before the line/column rule.\n\t[/\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z?/g, \"<time>\"],\n\t[/\\b\\d{1,2}:\\d{2}:\\d{2}\\b/g, \"<time>\"],\n\t// Ports, before the line/column rule can claim the colon.\n\t[/\\b(?:127\\.0\\.0\\.1|localhost):\\d+/gi, \"localhost:<port>\"],\n\t// file:line:col and bare line references.\n\t[/:\\d+:\\d+/g, \":<line>\"],\n\t[/\\bline \\d+/gi, \"line <line>\"],\n\t// Hex-ish identifiers: git shas, content hashes, addresses.\n\t[/\\b0x[0-9a-f]+\\b/gi, \"<addr>\"],\n\t[/\\b[0-9a-f]{7,40}\\b/gi, \"<hash>\"],\n\t// Durations and byte counts reported by build tools.\n\t[/\\b\\d+(?:\\.\\d+)?\\s?(?:ms|s|sec|secs|seconds|m|min|mins|kb|mb|gb)\\b/gi, \"<qty>\"],\n\t// PIDs, which vary per machine and per run.\n\t[/\\bpid[= ]\\d+/gi, \"pid <pid>\"],\n\t// Anything left that is a bare multi-digit run.\n\t[/\\b\\d{3,}\\b/g, \"<n>\"],\n];\n\n/**\n * Collapse a raw error or command output into a comparable signature.\n *\n * Case is preserved: `TS2307` and `ERR_MODULE_NOT_FOUND` carry meaning that\n * lowercasing would blur into surrounding prose, and they are exactly the\n * tokens worth matching on.\n */\nexport function normalizeErrorSignature(text: string): string {\n\tlet out = text.slice(0, MAX_SIGNATURE_CHARS * 4);\n\tfor (const [pattern, replacement] of VOLATILE_PATTERNS) {\n\t\tout = out.replace(pattern, replacement);\n\t}\n\treturn out.replace(/\\s+/g, \" \").trim().slice(0, MAX_SIGNATURE_CHARS);\n}\n\n/**\n * Normalize a shell command so two invocations of \"the same\" command match.\n *\n * Paths are collapsed but flags are kept — `npm test` and `npm test --coverage`\n * are genuinely different commands, and flags are frequently the fix itself.\n */\nexport function normalizeCommand(command: string): string {\n\tconst collapsed = command\n\t\t.replace(/\\s+/g, \" \")\n\t\t.replace(/(?:^|\\s)\\/(?:[\\w.@+-]+\\/)+[\\w.@+-]+/g, \" <path>\")\n\t\t.trim();\n\treturn collapsed.slice(0, MAX_SIGNATURE_CHARS);\n}\n\n/**\n * Prefixes that say where or how a command runs rather than what it does.\n *\n * Nearly every command an agent runs is wrapped in `cd <somewhere> && …`, and\n * once paths are collapsed that prefix is identical everywhere. Taking the head\n * of the raw command therefore yields `cd <path>` for the whole session, which\n * makes every bash step look like every other one.\n */\nconst COMMAND_PREFIX = /^(?:cd\\s+\\S+\\s*&&\\s*|(?:sudo|env|time|nice|exec)\\s+)/;\n\n/**\n * The first `tokens` words of what a command actually does, with the\n * where-and-how prefixes stripped. `cd <path> && npm run check` becomes\n * `npm run`, which is the part worth comparing across sessions.\n */\nexport function commandHead(command: string, tokens = 2): string {\n\tlet out = normalizeCommand(command);\n\t// Prefixes stack: `cd x && sudo env FOO=1 cmd`.\n\tfor (let i = 0; i < 4 && COMMAND_PREFIX.test(out); i++) {\n\t\tout = out.replace(COMMAND_PREFIX, \"\");\n\t}\n\treturn out.split(\" \").slice(0, tokens).join(\" \").trim();\n}\n\n/**\n * The part of a command's output that is actually the error.\n *\n * Build tools lead with a banner — npm's `> pkg@1.0 check > biome check …`, a\n * compiler's version line — that is byte-identical across every run. Signing a\n * failure by its whole output therefore signs mostly the banner, and two\n * unrelated failures of the same command look like the same problem. Scanning\n * for the first error-shaped line fixes both the signature and what the reader\n * is shown; failing that, the tail beats the head, because tools print the\n * banner first and the verdict last.\n */\nconst ERROR_LINE =\n\t/\\b(?:error|err!|errors?:|failed|failure|cannot|can't|unable to|not found|no such|unexpected|expected|invalid|denied|refused|timed out|traceback|panic|exception|fatal)\\b|^\\s*(?:×|✖|✗)/i;\n\nexport function extractErrorRegion(output: string, maxChars = 400): string {\n\tconst lines = output.split(\"\\n\");\n\tconst start = lines.findIndex((line) => ERROR_LINE.test(line));\n\tif (start >= 0) {\n\t\treturn lines.slice(start).join(\"\\n\").slice(0, maxChars).trim();\n\t}\n\treturn output.slice(-maxChars).trim();\n}\n\n/**\n * Output that reports no failure worth learning from: an abort is the user\n * changing their mind, and an empty result says nothing at all.\n */\nexport function isUninformativeFailure(output: string): boolean {\n\tconst trimmed = output.trim();\n\tif (trimmed.length === 0) return true;\n\treturn /^(?:command aborted|aborted|cancelled|canceled|interrupted|killed|sigint|sigterm)\\b/i.test(trimmed);\n}\n\n/** Filler that carries no rule content, stripped before directives are compared. */\nconst DIRECTIVE_FILLER =\n\t/^(?:please|pls|hey|ok|okay|now|also|and|so|just|quickly|can you|could you|would you|i want you to|i'd like you to|let's|lets)\\b[\\s,]*/i;\n\n/** Words too common to distinguish one directive from another. */\nconst STOPWORDS = new Set([\n\t\"a\",\n\t\"an\",\n\t\"and\",\n\t\"are\",\n\t\"as\",\n\t\"at\",\n\t\"be\",\n\t\"but\",\n\t\"by\",\n\t\"do\",\n\t\"for\",\n\t\"from\",\n\t\"in\",\n\t\"is\",\n\t\"it\",\n\t\"its\",\n\t\"of\",\n\t\"on\",\n\t\"or\",\n\t\"that\",\n\t\"the\",\n\t\"then\",\n\t\"this\",\n\t\"to\",\n\t\"we\",\n\t\"when\",\n\t\"with\",\n\t\"you\",\n\t\"your\",\n]);\n\n/**\n * Normalize a user directive for clustering: lowercase, strip filler and\n * punctuation, collapse whitespace. Unlike error signatures these are compared\n * loosely, so case and symbols are noise.\n */\nexport function normalizeDirective(text: string): string {\n\tlet out = text.trim();\n\t// Filler can stack (\"ok so please also ...\"), so strip until it stops matching.\n\tfor (let i = 0; i < 4 && DIRECTIVE_FILLER.test(out); i++) {\n\t\tout = out.replace(DIRECTIVE_FILLER, \"\");\n\t}\n\treturn out\n\t\t.toLowerCase()\n\t\t.replace(/[^\\w\\s@./+-]/g, \" \")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.trim();\n}\n\n/** Content words of a directive, for overlap comparisons against existing rules. */\nexport function contentWords(text: string): string[] {\n\treturn [...new Set(normalizeDirective(text).split(\" \"))].filter((w) => w.length > 2 && !STOPWORDS.has(w));\n}\n\n/**\n * Fraction of `words` that appear in `haystack`, 0 when there is nothing to\n * compare. Used to decide whether a proposed rule is already written down.\n */\nexport function wordOverlap(words: string[], haystack: string): number {\n\tif (words.length === 0) return 0;\n\tconst hay = ` ${normalizeDirective(haystack)} `;\n\tconst hits = words.filter((w) => hay.includes(` ${w} `)).length;\n\treturn hits / words.length;\n}\n\n/** Approvals and acknowledgements. These are the bulk of user turns and none are rules. */\nconst ACKNOWLEDGEMENT =\n\t/^(?:y|n|ok|okay|yes|no|yep|yeah|nope|sure|thanks|thank you|ty|go|go ahead|continue|proceed|do it|next|done|good|great|nice|perfect|lgtm|cool|k|fine|stop|wait|hmm|\\?+|\\.+)$/i;\n\n/**\n * Imperative or corrective phrasing — the shape a rule takes when it is spoken\n * aloud. A directive that matches none of these is almost always a task\n * (\"add a button here\"), which is worth doing once and never worth writing down.\n */\nconst RULE_SHAPED =\n\t/\\b(?:always|never|don'?t|do not|avoid|make sure|ensure|must|should|prefer|instead|remember|from now on|going forward|every time|each time|whenever|stop|use|run|keep|only|no need to)\\b/i;\n\n/** Minimum length before a directive is worth considering at all. */\nconst MIN_DIRECTIVE_CHARS = 12;\n/** Above this a \"directive\" is a task description or a pasted spec, not a rule. */\nconst MAX_DIRECTIVE_CHARS = 400;\n\n/**\n * Whether a user message is worth mining as a potential rule.\n *\n * Deliberately strict. The extractor's output is read by a model with a limited\n * budget, and recall costs precision: every task statement that slips through\n * displaces a real recurring directive in the ranking.\n */\nexport function isRuleShapedDirective(text: string): boolean {\n\tconst trimmed = text.trim();\n\tif (trimmed.length < MIN_DIRECTIVE_CHARS || trimmed.length > MAX_DIRECTIVE_CHARS) return false;\n\tif (ACKNOWLEDGEMENT.test(trimmed)) return false;\n\t// Slash commands are invocations, not instructions.\n\tif (trimmed.startsWith(\"/\")) return false;\n\t// Pasted output, diffs and stack traces are not directives.\n\tif (/^(?:```|diff --git|\\s*at\\s|\\{|\\[)/.test(trimmed)) return false;\n\tif (trimmed.split(\"\\n\").length > 6) return false;\n\treturn RULE_SHAPED.test(trimmed);\n}\n\n/**\n * Commands whose non-zero exit is a normal answer rather than a failure.\n *\n * `rg`/`grep` exit 1 on \"no match\", `test`/`diff`/`cmp` exit non-zero to report\n * their result, and a TDD loop is *supposed* to go red. Without this filter the\n * fail→fix extractor is mostly noise, since these are among the most-run\n * commands in any session.\n */\nconst BENIGN_EXIT_COMMANDS = /^(?:rg|grep|egrep|fgrep|ag|ack|test|\\[|diff|cmp|which|command|type|hash|find|fd)\\b/;\n\n/** Whether a non-zero exit from this command should be ignored by the fix extractor. */\nexport function isBenignFailure(command: string): boolean {\n\tconst first = command.trim().replace(/^(?:sudo|env|time|nice)\\s+/, \"\");\n\treturn BENIGN_EXIT_COMMANDS.test(first);\n}\n"]}
|