agentvibes 5.15.1 → 5.15.2
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/README.md +8 -2
- package/RELEASE_NOTES.md +19 -0
- package/package.json +1 -1
- package/src/console/preview-transport.js +17 -44
- package/src/console/tabs/agents-tab.js +13 -3
- package/src/console/tabs/music-tab.js +18 -0
- package/src/console/tabs/setup-tab.js +1 -1
- package/src/i18n/de.js +1 -1
- package/src/i18n/en.js +1 -1
- package/src/i18n/es.js +1 -1
- package/src/i18n/fr.js +1 -1
- package/src/i18n/hi.js +1 -1
- package/src/i18n/ja.js +1 -1
- package/src/i18n/ko.js +1 -1
- package/src/i18n/pt.js +1 -1
- package/src/i18n/zh-CN.js +1 -1
- package/src/services/agent-voice-store.js +20 -7
package/README.md
CHANGED
|
@@ -58,7 +58,13 @@ New here? The [**Quick Start guide**](docs/quick-start.md) walks you through you
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
-
## 🆕
|
|
61
|
+
## 🆕 Fixes for the preview indicators (v5.15.2)
|
|
62
|
+
|
|
63
|
+
- **Fixed a TUI crash** — filtering/favoriting a track while a preview was playing could rebuild the list under the indicator and crash; it can't now.
|
|
64
|
+
- **Honest per-agent badge** — the voice picker shows **(remotely via SSH)** vs **(locally)** from your real routing, not a partial guess.
|
|
65
|
+
- **BMAD agents show their real names** (Mary, Winston…) even when a skill file lacks the `Name — Title` heading; plus small preview/onboarding polish.
|
|
66
|
+
|
|
67
|
+
### v5.15.1 — Know where your preview plays
|
|
62
68
|
|
|
63
69
|
- **Every preview shows where it plays** — voice and music previews now display **(locally)** or **(remotely via SSH)** right on the row you're auditioning, so you're never guessing (or hearing silence on a headless box).
|
|
64
70
|
- **Preview standardized across the app** — the voice pickers (Kokoro, Piper, ElevenLabs, per-agent BMAD) and the Music page all show the same indicator; music previews now follow a project's remote receiver too.
|
|
@@ -272,7 +278,7 @@ anything (no new network calls, no behavior change) for every current install.
|
|
|
272
278
|
|
|
273
279
|
## About
|
|
274
280
|
|
|
275
|
-
**AgentVibes** · v5.15.
|
|
281
|
+
**AgentVibes** · v5.15.2 · Licensed under [Apache-2.0](LICENSE)
|
|
276
282
|
|
|
277
283
|
Built by **Paul Preibisch** — [@997Fire on X](https://x.com/997Fire) · [agentvibes.org](https://agentvibes.org) · [github.com/paulpreibisch/AgentVibes](https://github.com/paulpreibisch/AgentVibes)
|
|
278
284
|
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## v5.15.2 — Fixes for the preview indicators
|
|
4
|
+
|
|
5
|
+
**Released:** 2026-07-22 · `npm install agentvibes@latest`
|
|
6
|
+
|
|
7
|
+
Follow-up fixes for the 5.15.1 preview work, from an adversarial code review:
|
|
8
|
+
|
|
9
|
+
- **Fixed a crash** — filtering or favoriting a track *while a preview was
|
|
10
|
+
playing* could rebuild the list out from under the indicator and take the whole
|
|
11
|
+
TUI down. It can't anymore.
|
|
12
|
+
- **The badge tells the truth** — the per-agent voice picker now shows
|
|
13
|
+
**(remotely via SSH)** vs **(locally)** based on your actual routing (provider
|
|
14
|
+
file or remote receiver), not a partial guess.
|
|
15
|
+
- **Your BMAD agents show their real names** — the Agents tab reads the persona
|
|
16
|
+
name (Mary, Winston, Amelia…) even on installs whose skill files don't use the
|
|
17
|
+
`Name — Title` heading format.
|
|
18
|
+
- **Smaller polish** — a remote voice-preview error no longer leaves a stuck
|
|
19
|
+
spinner, the onboarding "re-check" hint is translated in all languages, and the
|
|
20
|
+
onboarding screen keeps keyboard focus after a re-check.
|
|
21
|
+
|
|
3
22
|
## v5.15.1 — Know where your preview plays
|
|
4
23
|
|
|
5
24
|
**Released:** 2026-07-21 · `npm install agentvibes@latest`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "agentvibes",
|
|
4
|
-
"version": "5.15.
|
|
4
|
+
"version": "5.15.2",
|
|
5
5
|
"description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
|
|
6
6
|
"homepage": "https://agentvibes.org",
|
|
7
7
|
"keywords": [
|
|
@@ -53,22 +53,6 @@ export function previewRowContent(frameChar, remote, stopHint = 'Space to stop')
|
|
|
53
53
|
return `{cyan-fg}${frameChar} Previewing {/cyan-fg}${transportBadge(remote)}{cyan-fg} (${stopHint}){/cyan-fg}`;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/**
|
|
57
|
-
* Preview indicator APPENDED after a row's existing label (keepLabel mode).
|
|
58
|
-
* Used where replacing the label would be jarring — e.g. the Music tab, whose
|
|
59
|
-
* rows carry emoji: keeping the name in place avoids a perceived "jump", and
|
|
60
|
-
* only the trailing spinner char changes per frame so blessed never re-diffs the
|
|
61
|
-
* (wide) emoji cell.
|
|
62
|
-
* @param {string} baseRow - the row's normal content (with its label/emoji)
|
|
63
|
-
* @param {string} frameChar
|
|
64
|
-
* @param {boolean} remote
|
|
65
|
-
* @param {string} [stopHint]
|
|
66
|
-
* @returns {string}
|
|
67
|
-
*/
|
|
68
|
-
export function previewRowAppend(baseRow, frameChar, remote, stopHint = 'Space to stop') {
|
|
69
|
-
return `${baseRow} {cyan-fg}${frameChar}{/cyan-fg} ${transportBadge(remote)}{cyan-fg} (${stopHint}){/cyan-fg}`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
56
|
/**
|
|
73
57
|
* Attach an animated preview indicator to the SELECTED row of a blessed list.
|
|
74
58
|
* Generalizes the Kokoro picker's row spinner so every picker behaves identically.
|
|
@@ -88,16 +72,7 @@ export function createRowSpinner(list, screen, renderItem, opts = {}) {
|
|
|
88
72
|
const now = opts.now ?? (() => Date.now());
|
|
89
73
|
const intervalMs = opts.intervalMs ?? 80;
|
|
90
74
|
const minVisibleMs = opts.minVisibleMs ?? 1100;
|
|
91
|
-
//
|
|
92
|
-
// Needed for lists containing double-width emoji (the Music tab): blessed's
|
|
93
|
-
// diff render desyncs the terminal cursor around wide chars during rapid
|
|
94
|
-
// in-place updates, corrupting rows — even though the internal buffer is
|
|
95
|
-
// correct. A realloc rewrites the terminal from that correct buffer.
|
|
96
|
-
const fullRedraw = opts.fullRedraw ?? false;
|
|
97
|
-
// keepLabel: append the indicator after the row's existing label instead of
|
|
98
|
-
// replacing it (avoids a perceived "jump" on emoji rows — see previewRowAppend).
|
|
99
|
-
const keepLabel = opts.keepLabel ?? false;
|
|
100
|
-
// isStatic: write the indicator ONCE (no animation). Needed for lists with
|
|
75
|
+
// isStatic: write the indicator ONCE (no animation). Used for lists with
|
|
101
76
|
// double-width emoji (the Music tab): repeated in-place mutation of emoji rows
|
|
102
77
|
// desyncs blessed's terminal output. One write + full-width space padding is
|
|
103
78
|
// the least-fragile option there. A frozen "♪" stands in for the spinner.
|
|
@@ -110,17 +85,18 @@ export function createRowSpinner(list, screen, renderItem, opts = {}) {
|
|
|
110
85
|
let remote = false;
|
|
111
86
|
let startTs = 0;
|
|
112
87
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
88
|
+
// Guard: blessed's List.setItem dereferences this.items[i] unchecked, so a
|
|
89
|
+
// stale idx (e.g. the list was rebuilt shorter by a filter while a preview was
|
|
90
|
+
// active) would throw an uncaught TypeError and kill the whole TUI. Only touch
|
|
91
|
+
// a row that still exists. list.items is an array on a real list; the test stub
|
|
92
|
+
// uses a plain object, so treat "not an array" as always-in-range.
|
|
93
|
+
function _inRange(i) {
|
|
94
|
+
if (i < 0) return false;
|
|
95
|
+
return Array.isArray(list.items) ? i < list.items.length : true;
|
|
120
96
|
}
|
|
121
97
|
|
|
122
|
-
function _paint(
|
|
123
|
-
if (idx
|
|
98
|
+
function _paint() {
|
|
99
|
+
if (!_inRange(idx)) return;
|
|
124
100
|
// Pad to the widest reliable measure so the row fully overwrites the previous
|
|
125
101
|
// (longer) content. list.width may still be a percentage string pre-layout, so
|
|
126
102
|
// fall back to the screen width; over-padding is clipped by the non-wrapping list.
|
|
@@ -128,19 +104,16 @@ export function createRowSpinner(list, screen, renderItem, opts = {}) {
|
|
|
128
104
|
const sw = (screen && typeof screen.width === 'number' && screen.width > 0) ? screen.width : 0;
|
|
129
105
|
const w = Math.max(lw, sw, 80);
|
|
130
106
|
const spin = isStatic ? '♪' : SPIN_FRAMES[frame++ % SPIN_FRAMES.length];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
: previewRowContent(spin, remote);
|
|
134
|
-
list.setItem(idx, padTaggedTo(content, w));
|
|
135
|
-
_render(full);
|
|
107
|
+
list.setItem(idx, padTaggedTo(previewRowContent(spin, remote), w));
|
|
108
|
+
screen.render();
|
|
136
109
|
}
|
|
137
110
|
|
|
138
111
|
function stop() {
|
|
139
112
|
if (floor) { clearTimeout(floor); floor = null; }
|
|
140
113
|
if (timer) { clearInterval(timer); timer = null; }
|
|
141
|
-
if (idx
|
|
114
|
+
if (_inRange(idx) && !isClosed()) {
|
|
142
115
|
list.setItem(idx, renderItem(idx));
|
|
143
|
-
|
|
116
|
+
screen.render();
|
|
144
117
|
}
|
|
145
118
|
idx = -1;
|
|
146
119
|
}
|
|
@@ -152,9 +125,9 @@ export function createRowSpinner(list, screen, renderItem, opts = {}) {
|
|
|
152
125
|
frame = 0;
|
|
153
126
|
remote = !!isRemote;
|
|
154
127
|
startTs = now();
|
|
155
|
-
_paint(
|
|
128
|
+
_paint();
|
|
156
129
|
if (isStatic) return; // static indicator: written once, no animation loop
|
|
157
|
-
timer = setInterval(() => { if (isClosed()) { stop(); return; } _paint(
|
|
130
|
+
timer = setInterval(() => { if (isClosed()) { stop(); return; } _paint(); }, intervalMs);
|
|
158
131
|
// A UI spinner must never keep the process alive (blessed's stdin does that
|
|
159
132
|
// in the real TUI); unref so a leaked spinner can't hang node --test on exit.
|
|
160
133
|
if (timer.unref) timer.unref();
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from './voices-tab.js';
|
|
22
22
|
import { buildAudioEnv, detectWavPlayer, detectRemoteLlm } from '../audio-env.js';
|
|
23
23
|
import { createRowSpinner } from '../preview-transport.js';
|
|
24
|
+
import { resolveMusicProvider } from '../music-preview.js';
|
|
24
25
|
import { voicesForProvider } from '../../services/provider-voice-catalog.js';
|
|
25
26
|
import { destroyList } from '../widgets/destroy-list.js';
|
|
26
27
|
import { BRAND_PINK } from '../brand-colors.js';
|
|
@@ -335,7 +336,11 @@ ${_tl('bmadDesc')}
|
|
|
335
336
|
|
|
336
337
|
// Manual re-check: after installing BMAD from a separate terminal, Enter
|
|
337
338
|
// re-scans without leaving the tab. (Switching tabs also re-scans via onFocus.)
|
|
338
|
-
|
|
339
|
+
// If BMAD now shows up, move focus to the roster (else keys go to the hidden box).
|
|
340
|
+
onboardingBox.key(['enter'], () => {
|
|
341
|
+
refreshDisplay();
|
|
342
|
+
if (_bmadDetected) { agentList.focus(); screen.render(); }
|
|
343
|
+
});
|
|
339
344
|
|
|
340
345
|
// -------------------------------------------------------------------------
|
|
341
346
|
// BMAD state — section header
|
|
@@ -1135,13 +1140,18 @@ ${_tl('bmadDesc')}
|
|
|
1135
1140
|
|
|
1136
1141
|
const phrase = previewPhrase(voiceId);
|
|
1137
1142
|
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1143
|
+
// Badge must match where play-tts actually routes: remote if the provider
|
|
1144
|
+
// file is ssh-remote/agentvibes-receiver OR a transport-config entry is
|
|
1145
|
+
// mode=remote. resolveMusicProvider is the shared transport resolver (both
|
|
1146
|
+
// signals) — don't re-derive routing here (CLAUDE.md invariant #1).
|
|
1147
|
+
const _isRemote = resolveMusicProvider(path.dirname(_pkgClaudeDir)).remote;
|
|
1138
1148
|
|
|
1139
1149
|
if (_isWin) {
|
|
1140
1150
|
// Windows: route through play-tts.ps1 (same pattern as non-Windows bash route)
|
|
1141
1151
|
const playTtsScript = _hookScript('hooks-windows', 'play-tts.ps1');
|
|
1142
1152
|
if (!fs.existsSync(playTtsScript)) return;
|
|
1143
1153
|
_previewVoiceId = voiceId;
|
|
1144
|
-
if (!_vpClosed) { _vpSpin.start(vpList.selected,
|
|
1154
|
+
if (!_vpClosed) { _vpSpin.start(vpList.selected, _isRemote); }
|
|
1145
1155
|
_previewProc = spawn('powershell', [ // NOSONAR
|
|
1146
1156
|
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', playTtsScript, phrase, voiceId,
|
|
1147
1157
|
], { stdio: 'ignore', detached: false, windowsHide: true,
|
|
@@ -1176,7 +1186,7 @@ ${_tl('bmadDesc')}
|
|
|
1176
1186
|
cwd: _projectRoot,
|
|
1177
1187
|
});
|
|
1178
1188
|
_previewVoiceId = voiceId;
|
|
1179
|
-
if (!_vpClosed) { _vpSpin.start(vpList.selected,
|
|
1189
|
+
if (!_vpClosed) { _vpSpin.start(vpList.selected, _isRemote); }
|
|
1180
1190
|
|
|
1181
1191
|
const _clearAfterMinDisplay = () => {
|
|
1182
1192
|
if (_previewVoiceId === voiceId) {
|
|
@@ -765,8 +765,26 @@ export function createMusicTab(screen, services) {
|
|
|
765
765
|
});
|
|
766
766
|
}
|
|
767
767
|
|
|
768
|
+
// Stop any in-flight preview (local player, remote receiver, and the row
|
|
769
|
+
// spinner). Used before a list rebuild so a stale spinner index can't crash
|
|
770
|
+
// blessed's setItem, and so a preview can't be left "playing" invisibly.
|
|
771
|
+
function _stopAnyPreview() {
|
|
772
|
+
if (_remotePlayingTrackId) {
|
|
773
|
+
const mp = resolveMusicProvider(_PKG_ROOT);
|
|
774
|
+
if (mp.remote) _sendMusicRemote(mp, { stop: true });
|
|
775
|
+
_remotePlayingTrackId = null;
|
|
776
|
+
}
|
|
777
|
+
_killPlayingProcess();
|
|
778
|
+
_playingTrackId = null;
|
|
779
|
+
_trackSpin.stop();
|
|
780
|
+
}
|
|
781
|
+
|
|
768
782
|
function refreshDisplay() {
|
|
769
783
|
_refreshing = true;
|
|
784
|
+
// A rebuild (setItems) invalidates the spinner's row index and wipes its
|
|
785
|
+
// indicator — stop any active preview first (guarded, so a normal refresh with
|
|
786
|
+
// no preview is a no-op). Prevents a stale-index crash and a stuck preview.
|
|
787
|
+
if (_trackSpin.isActive()) _stopAnyPreview();
|
|
770
788
|
const savedIdx = trackList.selected ?? 0;
|
|
771
789
|
|
|
772
790
|
_allTracks = _buildAllTracks();
|
|
@@ -4261,7 +4261,7 @@ export function createSetupTab(screen, services) {
|
|
|
4261
4261
|
_vpSpin.stopWithFloor();
|
|
4262
4262
|
}
|
|
4263
4263
|
});
|
|
4264
|
-
rProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
4264
|
+
rProc.on('error', () => { _previewProc = null; _previewVoiceId = null; _vpSpin.stop(); });
|
|
4265
4265
|
return;
|
|
4266
4266
|
}
|
|
4267
4267
|
|
package/src/i18n/de.js
CHANGED
|
@@ -198,6 +198,6 @@ export default {
|
|
|
198
198
|
helpSearchLabel: "Suche:",
|
|
199
199
|
readmeScrollMore: "↓ Scrollen für mehr Inhalt ↓",
|
|
200
200
|
readmeNotFound: "*(Keine README.md im aktuellen Verzeichnis gefunden)*",
|
|
201
|
-
bmadFooterNobmad: "[Tab] Tab wechseln [Q] Beenden",
|
|
201
|
+
bmadFooterNobmad: "[Enter] Erneut prüfen [Tab] Tab wechseln [Q] Beenden",
|
|
202
202
|
bmadFooterBmad: "[↑↓/jk] Navigieren [Space] Vorschau [Enter] Konfigurieren [A] Auto-zuweisen [B] Stapel [Del] Zurücksetzen [Q] Beenden",
|
|
203
203
|
};
|
package/src/i18n/en.js
CHANGED
|
@@ -200,4 +200,4 @@ export default {
|
|
|
200
200
|
readmeNotFound: "*(No README.md found in current directory)*",
|
|
201
201
|
bmadFooterNobmad: "[Enter] Re-check [Tab] Switch Tab [Q] Quit",
|
|
202
202
|
bmadFooterBmad: "[↑↓/jk] Navigate [Space] Preview [Enter] Configure [A] Auto-assign [B] Bulk [Del] Reset [Q] Quit",
|
|
203
|
-
};
|
|
203
|
+
};
|
package/src/i18n/es.js
CHANGED
|
@@ -198,6 +198,6 @@ export default {
|
|
|
198
198
|
helpSearchLabel: "Buscar:",
|
|
199
199
|
readmeScrollMore: "↓ Desplázate para ver más ↓",
|
|
200
200
|
readmeNotFound: "*(No se encontró README.md en el directorio actual)*",
|
|
201
|
-
bmadFooterNobmad: "[Tab] Cambiar Pestaña [Q] Salir",
|
|
201
|
+
bmadFooterNobmad: "[Enter] Volver a comprobar [Tab] Cambiar Pestaña [Q] Salir",
|
|
202
202
|
bmadFooterBmad: "[↑↓/jk] Navegar [Space] Previsualizar [Enter] Configurar [A] Auto-asignar [B] Masivo [Del] Restablecer [Q] Salir",
|
|
203
203
|
};
|
package/src/i18n/fr.js
CHANGED
|
@@ -198,6 +198,6 @@ export default {
|
|
|
198
198
|
helpSearchLabel: "Recherche:",
|
|
199
199
|
readmeScrollMore: "↓ Défiler pour plus de contenu ↓",
|
|
200
200
|
readmeNotFound: "*(Aucun README.md trouvé dans le répertoire actuel)*",
|
|
201
|
-
bmadFooterNobmad: "[Tab] Changer d'onglet [Q] Quitter",
|
|
201
|
+
bmadFooterNobmad: "[Enter] Revérifier [Tab] Changer d'onglet [Q] Quitter",
|
|
202
202
|
bmadFooterBmad: "[↑↓/jk] Naviguer [Space] Aperçu [Enter] Configurer [A] Auto-assigner [B] Lot [Del] Réinitialiser [Q] Quitter",
|
|
203
203
|
};
|
package/src/i18n/hi.js
CHANGED
|
@@ -198,6 +198,6 @@ export default {
|
|
|
198
198
|
helpSearchLabel: "खोजें:",
|
|
199
199
|
readmeScrollMore: "↓ अधिक सामग्री के लिए स्क्रॉल करें ↓",
|
|
200
200
|
readmeNotFound: "*(वर्तमान निर्देशिका में README.md नहीं मिला)*",
|
|
201
|
-
bmadFooterNobmad: "[Tab] टैब बदलें [Q] बाहर",
|
|
201
|
+
bmadFooterNobmad: "[Enter] फिर से जाँचें [Tab] टैब बदलें [Q] बाहर",
|
|
202
202
|
bmadFooterBmad: "[↑↓/jk] नेविगेट [Space] प्रीव्यू [Enter] कॉन्फ़िगर [A] ऑटो-असाइन [B] बल्क [Del] रीसेट [Q] बाहर",
|
|
203
203
|
};
|
package/src/i18n/ja.js
CHANGED
|
@@ -198,6 +198,6 @@ export default {
|
|
|
198
198
|
helpSearchLabel: "検索:",
|
|
199
199
|
readmeScrollMore: "↓ スクロールして続きを見る ↓",
|
|
200
200
|
readmeNotFound: "*(現在のディレクトリにREADME.mdが見つかりません)*",
|
|
201
|
-
bmadFooterNobmad: "[Tab] タブ切替 [Q] 終了",
|
|
201
|
+
bmadFooterNobmad: "[Enter] 再確認 [Tab] タブ切替 [Q] 終了",
|
|
202
202
|
bmadFooterBmad: "[↑↓/jk] 移動 [Space] プレビュー [Enter] 設定 [A] 自動割当 [B] 一括 [Del] リセット [Q] 終了",
|
|
203
203
|
};
|
package/src/i18n/ko.js
CHANGED
|
@@ -198,6 +198,6 @@ export default {
|
|
|
198
198
|
helpSearchLabel: "검색:",
|
|
199
199
|
readmeScrollMore: "↓ 아래로 스크롤하여 더 보기 ↓",
|
|
200
200
|
readmeNotFound: "*(현재 디렉토리에서 README.md를 찾을 수 없습니다)*",
|
|
201
|
-
bmadFooterNobmad: "[Tab] 탭 전환 [Q] 나가기",
|
|
201
|
+
bmadFooterNobmad: "[Enter] 다시 확인 [Tab] 탭 전환 [Q] 나가기",
|
|
202
202
|
bmadFooterBmad: "[↑↓/jk] 탐색 [Space] 미리보기 [Enter] 설정 [A] 자동배정 [B] 일괄 [Del] 초기화 [Q] 나가기",
|
|
203
203
|
};
|
package/src/i18n/pt.js
CHANGED
|
@@ -198,6 +198,6 @@ export default {
|
|
|
198
198
|
helpSearchLabel: "Buscar:",
|
|
199
199
|
readmeScrollMore: "↓ Role para mais conteúdo ↓",
|
|
200
200
|
readmeNotFound: "*(Nenhum README.md encontrado no diretório atual)*",
|
|
201
|
-
bmadFooterNobmad: "[Tab] Mudar Aba [Q] Sair",
|
|
201
|
+
bmadFooterNobmad: "[Enter] Verificar novamente [Tab] Mudar Aba [Q] Sair",
|
|
202
202
|
bmadFooterBmad: "[↑↓/jk] Navegar [Space] Visualizar [Enter] Configurar [A] Auto-atribuir [B] Lote [Del] Redefinir [Q] Sair",
|
|
203
203
|
};
|
package/src/i18n/zh-CN.js
CHANGED
|
@@ -198,6 +198,6 @@ export default {
|
|
|
198
198
|
helpSearchLabel: "搜索:",
|
|
199
199
|
readmeScrollMore: "↓ 向下滚动查看更多 ↓",
|
|
200
200
|
readmeNotFound: "*(当前目录中未找到README.md)*",
|
|
201
|
-
bmadFooterNobmad: "[Tab] 切换标签 [Q] 退出",
|
|
201
|
+
bmadFooterNobmad: "[Enter] 重新检查 [Tab] 切换标签 [Q] 退出",
|
|
202
202
|
bmadFooterBmad: "[↑↓/jk] 导航 [Space] 预览 [Enter] 配置 [A] 自动分配 [B] 批量 [Del] 重置 [Q] 退出",
|
|
203
203
|
};
|
|
@@ -228,18 +228,31 @@ function _findBmadAgentSkills(root) {
|
|
|
228
228
|
* heading ("# Mary — Business Analyst") for the persona name + title. Falls back
|
|
229
229
|
* to a title-cased id when the heading is missing/unreadable.
|
|
230
230
|
*/
|
|
231
|
+
// Generic section words that a bare "# Word" heading might be (not a persona).
|
|
232
|
+
const _GENERIC_HEADING = /^(overview|about|usage|conventions|activation|role|purpose|description|instructions)$/i;
|
|
233
|
+
|
|
231
234
|
function _agentFromSkill(id, skillDir) {
|
|
232
235
|
let displayName = _titleCaseId(id);
|
|
233
236
|
let title = '';
|
|
234
237
|
try {
|
|
235
238
|
const md = fs.readFileSync(path.resolve(skillDir, 'SKILL.md'), 'utf8');
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
// 1) Preferred: "# Mary — Business Analyst" (name <dash> title).
|
|
240
|
+
const dash = md.match(/^#\s+([^\n]+?)\s+[—–-]\s+([^\n]+?)\s*$/m); // em / en / hyphen
|
|
241
|
+
if (dash) {
|
|
242
|
+
displayName = dash[1].trim() || displayName;
|
|
243
|
+
title = dash[2].trim();
|
|
244
|
+
} else {
|
|
245
|
+
// 2) Frontmatter description often names the persona: "...talk to Mary...".
|
|
246
|
+
const desc = md.match(/^description:\s*(.+)$/mi);
|
|
247
|
+
const talk = desc && desc[1].match(/talk to ([A-Z][\w'’-]+)/);
|
|
248
|
+
if (talk) {
|
|
249
|
+
displayName = talk[1];
|
|
250
|
+
} else {
|
|
251
|
+
// 3) A bare "# Mary" heading — but skip generic section headings so a
|
|
252
|
+
// skill without a persona heading (e.g. "# Overview") falls back to the id.
|
|
253
|
+
const h1 = md.match(/^#\s+([A-Z][\w'’-]*)\s*$/m);
|
|
254
|
+
if (h1 && !_GENERIC_HEADING.test(h1[1])) displayName = h1[1];
|
|
255
|
+
}
|
|
243
256
|
}
|
|
244
257
|
} catch { /* use derived displayName */ }
|
|
245
258
|
return { id, displayName, title, icon: '', module: 'bmm' };
|