privateboard 0.1.11 → 0.1.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateboard",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "PrivateBoard · your private board meeting, on call. Local-first, multi-agent thinking amplifier.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,6 +45,7 @@
45
45
  "commander": "^12.1.0",
46
46
  "hono": "^4.6.14",
47
47
  "open": "^10.1.0",
48
+ "tiny-pinyin": "^1.3.2",
48
49
  "yaml": "^2.8.4"
49
50
  },
50
51
  "devDependencies": {
@@ -29,8 +29,11 @@
29
29
  .adjourn-modal {
30
30
  position: relative;
31
31
  z-index: 1;
32
- width: 100%;
33
- max-width: 880px;
32
+ /* Width matches the convene-follow-up overlay (.supplement-modal)
33
+ so the room's two terminal-action overlays read as the same
34
+ family. Was max-width: 880px which felt oversized for a
35
+ short-form action confirm. */
36
+ width: min(560px, calc(100vw - 32px));
34
37
  max-height: calc(100vh - 48px);
35
38
  background: var(--panel);
36
39
  border: 0.5px solid var(--line-strong);
@@ -441,8 +444,9 @@
441
444
  pointer-events: none;
442
445
  }
443
446
 
444
- /* Narrow modal */
447
+ /* Narrow viewport · summary rows tighten. The modal width itself
448
+ already collapses gracefully below ~592px via the
449
+ `min(560px, calc(100vw - 32px))` rule on .adjourn-modal. */
445
450
  @media (max-width: 640px) {
446
- .adjourn-modal { max-width: 100%; }
447
451
  .adjourn-summary-row { grid-template-columns: 78px 1fr; gap: 10px; padding: 9px 12px; }
448
452
  }
@@ -4046,3 +4046,348 @@
4046
4046
  .ap-voice-advanced[open] summary::after { content: "−"; }
4047
4047
  .ap-voice-advanced summary:hover { color: var(--text, #eee); }
4048
4048
  .ap-voice-advanced summary:hover::after { color: var(--text); }
4049
+
4050
+ /* ────────────────────────────────────────────────────────────
4051
+ Persona dossier · Full-mode agents only
4052
+ ────────────────────────────────────────────────────────────
4053
+ Reads as a gamified character-sheet entry below the rules
4054
+ section. The card is the entire button — keyboard + click
4055
+ land on the same element. Stat values use the mono register
4056
+ so digits hold a fixed grid; labels are upper-mono kickers
4057
+ for the dossier flavour. */
4058
+ .ap-persona-block .ap-block-h-tag {
4059
+ font-family: var(--mono);
4060
+ font-size: 9px;
4061
+ letter-spacing: 0.16em;
4062
+ text-transform: uppercase;
4063
+ color: var(--lime);
4064
+ padding: 2px 8px;
4065
+ border: 0.5px solid var(--lime-dim, var(--line));
4066
+ border-radius: 999px;
4067
+ }
4068
+ .ap-persona-card {
4069
+ display: flex;
4070
+ flex-direction: column;
4071
+ gap: 14px;
4072
+ width: 100%;
4073
+ padding: 16px 18px 14px;
4074
+ background: var(--panel-2);
4075
+ border: 0.5px solid var(--line);
4076
+ text-align: left;
4077
+ cursor: pointer;
4078
+ font-family: inherit;
4079
+ color: inherit;
4080
+ transition: border-color 0.12s, background 0.12s, transform 0.12s;
4081
+ position: relative;
4082
+ overflow: hidden;
4083
+ }
4084
+ .ap-persona-card::before {
4085
+ /* Faint diagonal hatch behind the card · evokes a stamped
4086
+ dossier surface without competing with content. Pure CSS,
4087
+ pinned to the corner so it doesn't follow the cta. */
4088
+ content: "";
4089
+ position: absolute;
4090
+ top: 0; right: 0;
4091
+ width: 110px; height: 110px;
4092
+ background-image: repeating-linear-gradient(
4093
+ 45deg,
4094
+ transparent 0,
4095
+ transparent 6px,
4096
+ var(--line) 6px,
4097
+ var(--line) 7px
4098
+ );
4099
+ opacity: 0.35;
4100
+ pointer-events: none;
4101
+ }
4102
+ .ap-persona-card:hover {
4103
+ border-color: var(--lime);
4104
+ background: var(--panel-3, var(--panel-2));
4105
+ }
4106
+ .ap-persona-card:active { transform: translateY(1px); }
4107
+ .ap-persona-card:focus-visible {
4108
+ outline: 1px solid var(--lime);
4109
+ outline-offset: 2px;
4110
+ }
4111
+ .ap-persona-card-head {
4112
+ display: grid;
4113
+ grid-template-columns: auto 1fr auto;
4114
+ align-items: center;
4115
+ gap: 14px;
4116
+ }
4117
+ .ap-persona-card-glyph {
4118
+ width: 44px; height: 44px;
4119
+ display: flex;
4120
+ align-items: center;
4121
+ justify-content: center;
4122
+ color: var(--lime);
4123
+ border: 0.5px solid var(--lime-dim, var(--line));
4124
+ background: var(--panel);
4125
+ }
4126
+ .ap-persona-card-id { min-width: 0; }
4127
+ .ap-persona-card-kicker {
4128
+ font-family: var(--mono);
4129
+ font-size: 9.5px;
4130
+ letter-spacing: 0.18em;
4131
+ text-transform: uppercase;
4132
+ color: var(--lime);
4133
+ margin-bottom: 4px;
4134
+ }
4135
+ .ap-persona-card-title {
4136
+ font-family: var(--font-human);
4137
+ font-size: 17px;
4138
+ font-weight: 700;
4139
+ color: var(--text);
4140
+ letter-spacing: -0.01em;
4141
+ line-height: 1.2;
4142
+ }
4143
+ .ap-persona-card-meta {
4144
+ font-family: var(--mono);
4145
+ font-size: 10px;
4146
+ letter-spacing: 0.06em;
4147
+ color: var(--text-faint);
4148
+ margin-top: 4px;
4149
+ text-transform: uppercase;
4150
+ }
4151
+ .ap-persona-card-score {
4152
+ text-align: right;
4153
+ padding-left: 12px;
4154
+ border-left: 0.5px solid var(--line);
4155
+ }
4156
+ .ap-persona-card-score-v {
4157
+ font-family: var(--mono);
4158
+ font-size: 26px;
4159
+ font-weight: 600;
4160
+ color: var(--lime);
4161
+ letter-spacing: 0.02em;
4162
+ line-height: 1;
4163
+ }
4164
+ .ap-persona-card-score-l {
4165
+ font-family: var(--mono);
4166
+ font-size: 8.5px;
4167
+ letter-spacing: 0.18em;
4168
+ text-transform: uppercase;
4169
+ color: var(--text-faint);
4170
+ margin-top: 5px;
4171
+ }
4172
+ .ap-persona-card-grid {
4173
+ display: grid;
4174
+ grid-template-columns: repeat(6, minmax(0, 1fr));
4175
+ gap: 0;
4176
+ border-top: 0.5px solid var(--line);
4177
+ border-bottom: 0.5px solid var(--line);
4178
+ }
4179
+ .ap-persona-stat {
4180
+ padding: 10px 8px;
4181
+ text-align: center;
4182
+ border-right: 0.5px solid var(--line);
4183
+ }
4184
+ .ap-persona-stat:last-child { border-right: none; }
4185
+ .ap-persona-stat-v {
4186
+ font-family: var(--mono);
4187
+ font-size: 16px;
4188
+ font-weight: 600;
4189
+ color: var(--text);
4190
+ line-height: 1;
4191
+ }
4192
+ .ap-persona-stat-l {
4193
+ font-family: var(--mono);
4194
+ font-size: 8px;
4195
+ letter-spacing: 0.16em;
4196
+ text-transform: uppercase;
4197
+ color: var(--text-faint);
4198
+ margin-top: 5px;
4199
+ }
4200
+ .ap-persona-card-cta {
4201
+ display: flex;
4202
+ justify-content: space-between;
4203
+ align-items: baseline;
4204
+ }
4205
+ .ap-persona-card-cta-label {
4206
+ font-family: var(--mono);
4207
+ font-size: 11px;
4208
+ letter-spacing: 0.18em;
4209
+ text-transform: uppercase;
4210
+ color: var(--lime);
4211
+ font-weight: 600;
4212
+ }
4213
+ .ap-persona-card-cta-hint {
4214
+ font-family: var(--mono);
4215
+ font-size: 9px;
4216
+ letter-spacing: 0.12em;
4217
+ text-transform: uppercase;
4218
+ color: var(--text-faint);
4219
+ }
4220
+ @media (max-width: 720px) {
4221
+ .ap-persona-card-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
4222
+ .ap-persona-stat:nth-child(3) { border-right: none; }
4223
+ .ap-persona-stat:nth-child(n+4) { border-top: 0.5px solid var(--line); }
4224
+ }
4225
+
4226
+ /* ────────────────────────────────────────────────────────────
4227
+ Persona dossier overlay · markdown preview + download
4228
+ ──────────────────────────────────────────────────────────── */
4229
+ .ap-persona-overlay {
4230
+ position: fixed;
4231
+ inset: 0;
4232
+ z-index: 9500;
4233
+ display: flex;
4234
+ align-items: center;
4235
+ justify-content: center;
4236
+ }
4237
+ .ap-persona-overlay-backdrop {
4238
+ position: absolute;
4239
+ inset: 0;
4240
+ background: rgba(0, 0, 0, 0.6);
4241
+ backdrop-filter: blur(3px);
4242
+ cursor: pointer;
4243
+ }
4244
+ .ap-persona-overlay-modal {
4245
+ position: relative;
4246
+ width: min(820px, calc(100vw - 32px));
4247
+ max-height: calc(100vh - 48px);
4248
+ display: flex;
4249
+ flex-direction: column;
4250
+ background: var(--panel);
4251
+ border: 0.5px solid var(--line-strong, var(--line));
4252
+ box-shadow: 0 24px 56px rgba(0, 0, 0, 0.5);
4253
+ }
4254
+ .ap-persona-overlay-classification {
4255
+ display: flex;
4256
+ justify-content: space-between;
4257
+ align-items: center;
4258
+ padding: 8px 18px;
4259
+ background: var(--panel-2);
4260
+ border-bottom: 0.5px solid var(--line);
4261
+ font-family: var(--mono);
4262
+ font-size: 9.5px;
4263
+ letter-spacing: 0.18em;
4264
+ text-transform: uppercase;
4265
+ color: var(--text-faint);
4266
+ }
4267
+ .ap-persona-overlay-classification .dot { color: var(--lime); margin-right: 4px; }
4268
+ .ap-persona-overlay-classification .right { color: var(--text-soft); }
4269
+ .ap-persona-overlay-head {
4270
+ display: flex;
4271
+ justify-content: space-between;
4272
+ align-items: center;
4273
+ gap: 14px;
4274
+ padding: 14px 22px;
4275
+ border-bottom: 0.5px solid var(--line);
4276
+ }
4277
+ .ap-persona-overlay-title {
4278
+ font-family: var(--font-human);
4279
+ font-size: 16px;
4280
+ font-weight: 700;
4281
+ color: var(--text);
4282
+ letter-spacing: -0.005em;
4283
+ }
4284
+ .ap-persona-overlay-actions {
4285
+ display: flex;
4286
+ align-items: center;
4287
+ gap: 8px;
4288
+ }
4289
+ .ap-persona-overlay-dl {
4290
+ font-family: var(--mono);
4291
+ font-size: 10px;
4292
+ letter-spacing: 0.12em;
4293
+ text-transform: uppercase;
4294
+ color: var(--lime);
4295
+ text-decoration: none;
4296
+ padding: 6px 12px;
4297
+ border: 0.5px solid var(--lime-dim, var(--line));
4298
+ background: var(--panel-2);
4299
+ transition: background 0.12s, color 0.12s;
4300
+ }
4301
+ .ap-persona-overlay-dl:hover {
4302
+ background: var(--lime);
4303
+ color: var(--bg, #0c0c0c);
4304
+ }
4305
+ .ap-persona-overlay-close {
4306
+ width: 30px;
4307
+ height: 30px;
4308
+ background: transparent;
4309
+ border: 0.5px solid var(--line);
4310
+ color: var(--text-soft);
4311
+ font-size: 14px;
4312
+ cursor: pointer;
4313
+ transition: color 0.12s, border-color 0.12s;
4314
+ }
4315
+ .ap-persona-overlay-close:hover { color: var(--text); border-color: var(--line-strong, var(--line)); }
4316
+ .ap-persona-overlay-body {
4317
+ flex: 1;
4318
+ min-height: 0;
4319
+ overflow-y: auto;
4320
+ padding: 22px 28px 28px;
4321
+ }
4322
+ .ap-persona-overlay-loading,
4323
+ .ap-persona-overlay-error {
4324
+ font-family: var(--mono);
4325
+ font-size: 11px;
4326
+ letter-spacing: 0.06em;
4327
+ color: var(--text-faint);
4328
+ text-align: center;
4329
+ padding: 60px 0;
4330
+ }
4331
+ .ap-persona-overlay-error { color: var(--ink, #c97373); }
4332
+ .ap-persona-overlay-md {
4333
+ font-family: var(--font-human);
4334
+ font-size: 13.5px;
4335
+ line-height: 1.65;
4336
+ color: var(--text);
4337
+ }
4338
+ .ap-persona-overlay-md h2 {
4339
+ font-family: var(--font-human);
4340
+ font-size: 16px;
4341
+ font-weight: 700;
4342
+ color: var(--text);
4343
+ margin: 24px 0 8px;
4344
+ padding-bottom: 6px;
4345
+ border-bottom: 0.5px solid var(--line);
4346
+ }
4347
+ .ap-persona-overlay-md h3 {
4348
+ font-family: var(--font-human);
4349
+ font-size: 13px;
4350
+ font-weight: 700;
4351
+ color: var(--text);
4352
+ margin: 18px 0 6px;
4353
+ }
4354
+ .ap-persona-overlay-md h4 {
4355
+ font-family: var(--mono);
4356
+ font-size: 10.5px;
4357
+ letter-spacing: 0.14em;
4358
+ text-transform: uppercase;
4359
+ color: var(--lime);
4360
+ margin: 14px 0 4px;
4361
+ }
4362
+ .ap-persona-overlay-md p { margin: 8px 0; }
4363
+ .ap-persona-overlay-md ul, .ap-persona-overlay-md ol {
4364
+ margin: 8px 0 8px 22px;
4365
+ padding: 0;
4366
+ }
4367
+ .ap-persona-overlay-md li { margin: 3px 0; }
4368
+ .ap-persona-overlay-md blockquote {
4369
+ margin: 10px 0;
4370
+ padding: 6px 12px;
4371
+ border-top: 0.5px solid var(--line);
4372
+ border-bottom: 0.5px solid var(--line);
4373
+ font-style: italic;
4374
+ color: var(--text-soft);
4375
+ }
4376
+ .ap-persona-overlay-md code {
4377
+ font-family: var(--mono);
4378
+ font-size: 12px;
4379
+ background: var(--panel-2);
4380
+ padding: 1px 5px;
4381
+ color: var(--text);
4382
+ }
4383
+ .ap-persona-overlay-md pre {
4384
+ background: var(--panel-2);
4385
+ padding: 10px 12px;
4386
+ overflow-x: auto;
4387
+ margin: 10px 0;
4388
+ border: 0.5px solid var(--line);
4389
+ }
4390
+ .ap-persona-overlay-md pre code { background: transparent; padding: 0; font-size: 11.5px; }
4391
+ .ap-persona-overlay-md strong { color: var(--text); font-weight: 700; }
4392
+ .ap-persona-overlay-md em { color: var(--lime); font-style: italic; }
4393
+ body.ap-persona-overlay-open { overflow: hidden; }
@@ -1079,6 +1079,100 @@
1079
1079
  function renderRulesBlock(slug) {
1080
1080
  return `<div class="ap-rules-block" data-ap-rules-block data-slug="${escape(slug)}">${renderRulesInner(slug)}</div>`;
1081
1081
  }
1082
+
1083
+ /** Persona dossier card · only mounted for Full-mode agents (those
1084
+ * with `personaSpec` populated by the deep-build pipeline). The
1085
+ * card reads as a gamified character-sheet · mono kicker, big
1086
+ * divergence stat, secondary stat grid, ▸ CTA. Clicking opens an
1087
+ * overlay that previews the persona.md content with a Download
1088
+ * affordance. Hidden entirely for Signal-mode agents and seeded
1089
+ * directors so the panel doesn't render an empty section.
1090
+ *
1091
+ * Stats source from `live.personaSpec` (resolved from
1092
+ * window.app.agentsById at render time · the `p` object passed in
1093
+ * doesn't carry the spec, so we reach across). */
1094
+ function renderPersonaDossierSection(slug, p) {
1095
+ const live = window.app && window.app.agentsById ? window.app.agentsById[slug] : null;
1096
+ const spec = live && live.personaSpec ? live.personaSpec : null;
1097
+ if (!spec) return "";
1098
+ // Stats. Differentiation score is the headline — null when the
1099
+ // build skipped the eval pass (rare). The rest are counts of the
1100
+ // structured artifacts so the user can eyeball depth at a glance.
1101
+ const score = typeof spec.differentiationScore === "number" ? spec.differentiationScore : null;
1102
+ const scorePct = score == null ? "—" : `${Math.round(score * 100)}%`;
1103
+ const knowledge = spec.knowledge || {};
1104
+ const sourceCount = (knowledge.keyThinkers || []).length
1105
+ + (knowledge.foundationalWorks || []).length
1106
+ + (knowledge.recentDevelopments || []).length
1107
+ + (knowledge.contestedClaims || []).length;
1108
+ const searchCount = (knowledge.searchQueries || []).length;
1109
+ const fewShotCount = (spec.fewShot || []).length;
1110
+ const rulesCount = (spec.rules || []).length;
1111
+ const checklistCount = (spec.reflectionChecklist || []).length;
1112
+ const evalCount = (spec.evalSet || []).length;
1113
+ const builtIso = spec.generatedAt || "";
1114
+ const builtLabel = builtIso ? new Date(builtIso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }) : "";
1115
+ return `
1116
+ <section class="ap-block ap-persona-block">
1117
+ <header class="ap-block-h">
1118
+ <span class="ap-block-h-title">Persona dossier</span>
1119
+ <span class="ap-block-h-tag">Full-mode build</span>
1120
+ </header>
1121
+ <button type="button" class="ap-persona-card" data-ap-persona-open data-slug="${escape(slug)}" aria-label="Open persona dossier">
1122
+ <div class="ap-persona-card-head">
1123
+ <div class="ap-persona-card-glyph" aria-hidden="true">
1124
+ <svg viewBox="0 0 32 32" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.4">
1125
+ <circle cx="16" cy="16" r="12" />
1126
+ <circle cx="16" cy="16" r="5" />
1127
+ <line x1="16" y1="2" x2="16" y2="8" />
1128
+ <line x1="16" y1="24" x2="16" y2="30" />
1129
+ <line x1="2" y1="16" x2="8" y2="16" />
1130
+ <line x1="24" y1="16" x2="30" y2="16" />
1131
+ </svg>
1132
+ </div>
1133
+ <div class="ap-persona-card-id">
1134
+ <div class="ap-persona-card-kicker">— PERSONA · 7-PHASE DOSSIER</div>
1135
+ <div class="ap-persona-card-title">${escape(p && p.name ? p.name : "Director")}</div>
1136
+ ${builtLabel ? `<div class="ap-persona-card-meta">Built · ${escape(builtLabel)}</div>` : ""}
1137
+ </div>
1138
+ <div class="ap-persona-card-score" title="Differentiation vs generic-AI baseline">
1139
+ <div class="ap-persona-card-score-v">${escape(scorePct)}</div>
1140
+ <div class="ap-persona-card-score-l">DIVERGENCE</div>
1141
+ </div>
1142
+ </div>
1143
+ <div class="ap-persona-card-grid">
1144
+ <div class="ap-persona-stat">
1145
+ <div class="ap-persona-stat-v">${sourceCount}</div>
1146
+ <div class="ap-persona-stat-l">SOURCES</div>
1147
+ </div>
1148
+ <div class="ap-persona-stat">
1149
+ <div class="ap-persona-stat-v">${searchCount}</div>
1150
+ <div class="ap-persona-stat-l">SEARCHES</div>
1151
+ </div>
1152
+ <div class="ap-persona-stat">
1153
+ <div class="ap-persona-stat-v">${fewShotCount}</div>
1154
+ <div class="ap-persona-stat-l">VOICE EX.</div>
1155
+ </div>
1156
+ <div class="ap-persona-stat">
1157
+ <div class="ap-persona-stat-v">${rulesCount}</div>
1158
+ <div class="ap-persona-stat-l">RULES</div>
1159
+ </div>
1160
+ <div class="ap-persona-stat">
1161
+ <div class="ap-persona-stat-v">${checklistCount}</div>
1162
+ <div class="ap-persona-stat-l">CHECKS</div>
1163
+ </div>
1164
+ <div class="ap-persona-stat">
1165
+ <div class="ap-persona-stat-v">${evalCount}</div>
1166
+ <div class="ap-persona-stat-l">EVALS</div>
1167
+ </div>
1168
+ </div>
1169
+ <div class="ap-persona-card-cta">
1170
+ <span class="ap-persona-card-cta-label">▸ OPEN DOSSIER</span>
1171
+ <span class="ap-persona-card-cta-hint">preview · download .md</span>
1172
+ </div>
1173
+ </button>
1174
+ </section>`;
1175
+ }
1082
1176
  function renderRulesInner(slug) {
1083
1177
  const rules = rulesForAgent(slug);
1084
1178
  const list = rules.length === 0
@@ -1772,6 +1866,79 @@
1772
1866
  `;
1773
1867
  }
1774
1868
 
1869
+ /* ─── Persona dossier overlay ──────────────────────────
1870
+ Full-screen modal that previews the persona.md content. Opens
1871
+ from the dossier card in the main column. Fetches the route
1872
+ once per open (no caching · the file is small and downloads
1873
+ are cheap), renders via the in-file renderMarkdown helper, and
1874
+ surfaces a Download button that hits the same endpoint with
1875
+ the browser's native download path. Closed on backdrop click
1876
+ or Escape. */
1877
+ let _personaOverlayEsc = null;
1878
+ function openPersonaOverlay(slug, agentName) {
1879
+ closePersonaOverlay();
1880
+ const overlay = document.createElement("div");
1881
+ overlay.id = "ap-persona-overlay";
1882
+ overlay.className = "ap-persona-overlay";
1883
+ overlay.innerHTML = `
1884
+ <div class="ap-persona-overlay-backdrop" data-ap-persona-close></div>
1885
+ <div class="ap-persona-overlay-modal" role="dialog" aria-modal="true" aria-label="Persona dossier">
1886
+ <div class="ap-persona-overlay-classification">
1887
+ <span><span class="dot">●</span> CLASSIFIED · DIRECTOR DOSSIER</span>
1888
+ <span class="right">${escape(agentName || "")}</span>
1889
+ </div>
1890
+ <div class="ap-persona-overlay-head">
1891
+ <div class="ap-persona-overlay-title">Persona dossier</div>
1892
+ <div class="ap-persona-overlay-actions">
1893
+ <a class="ap-persona-overlay-dl" href="/api/agents/${encodeURIComponent(slug)}/persona.md" download>
1894
+ <span aria-hidden="true">↓</span> Download .md
1895
+ </a>
1896
+ <button type="button" class="ap-persona-overlay-close" data-ap-persona-close aria-label="Close">✕</button>
1897
+ </div>
1898
+ </div>
1899
+ <div class="ap-persona-overlay-body" data-ap-persona-body>
1900
+ <div class="ap-persona-overlay-loading">Decrypting dossier…</div>
1901
+ </div>
1902
+ </div>
1903
+ `;
1904
+ document.body.appendChild(overlay);
1905
+ document.body.classList.add("ap-persona-overlay-open");
1906
+ _personaOverlayEsc = (ev) => {
1907
+ if (ev.key === "Escape") {
1908
+ ev.stopImmediatePropagation();
1909
+ closePersonaOverlay();
1910
+ }
1911
+ };
1912
+ document.addEventListener("keydown", _personaOverlayEsc, true);
1913
+ // Fetch + render. Same-origin so credentials default. The
1914
+ // fetch path returns text/markdown; we feed it straight into
1915
+ // the existing renderMarkdown helper.
1916
+ fetch(`/api/agents/${encodeURIComponent(slug)}/persona.md`, { credentials: "same-origin" })
1917
+ .then((r) => {
1918
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
1919
+ return r.text();
1920
+ })
1921
+ .then((md) => {
1922
+ const body = overlay.querySelector("[data-ap-persona-body]");
1923
+ if (!body) return;
1924
+ body.innerHTML = `<div class="ap-persona-overlay-md">${renderMarkdown(md)}</div>`;
1925
+ })
1926
+ .catch((err) => {
1927
+ const body = overlay.querySelector("[data-ap-persona-body]");
1928
+ if (!body) return;
1929
+ body.innerHTML = `<div class="ap-persona-overlay-error">Could not load dossier · ${escape(String(err && err.message ? err.message : err))}</div>`;
1930
+ });
1931
+ }
1932
+ function closePersonaOverlay() {
1933
+ const el = document.getElementById("ap-persona-overlay");
1934
+ if (el) el.remove();
1935
+ document.body.classList.remove("ap-persona-overlay-open");
1936
+ if (_personaOverlayEsc) {
1937
+ document.removeEventListener("keydown", _personaOverlayEsc, true);
1938
+ _personaOverlayEsc = null;
1939
+ }
1940
+ }
1941
+
1775
1942
  /* ─── Profile · ⋯ menu (top-right of the cover) ─────
1776
1943
  Small popover anchored to the menu button with one or more
1777
1944
  actions. v1 ships a single "regenerate 8-bit avatar" item. */
@@ -1807,6 +1974,20 @@
1807
1974
  <span>Regenerate 8-bit avatar</span>
1808
1975
  </button>`);
1809
1976
  }
1977
+ // Persona MD download · only present for Full-mode agents (those
1978
+ // built via the deep persona-builder pipeline). Their `personaSpec`
1979
+ // field carries the 7-phase artifact; the route renders it as
1980
+ // Markdown. Hidden for Signal-mode agents and seeded directors —
1981
+ // they have no spec to export.
1982
+ const hasPersonaSpec = !!(live && live.personaSpec);
1983
+ if (hasPersonaSpec) {
1984
+ parts.push(`<div class="ap-id-menu-divider" aria-hidden="true"></div>`);
1985
+ parts.push(`
1986
+ <a class="ap-id-menu-item" href="/api/agents/${encodeURIComponent(slug)}/persona.md" target="_blank" rel="noopener" data-ap-menu-action="persona-md">
1987
+ <span class="ap-id-menu-mark">↓</span>
1988
+ <span>Download persona.md</span>
1989
+ </a>`);
1990
+ }
1810
1991
  if (isCustom) {
1811
1992
  parts.push(`<div class="ap-id-menu-divider" aria-hidden="true"></div>`);
1812
1993
  parts.push(`
@@ -2408,7 +2589,6 @@
2408
2589
  ensureVoiceOptions();
2409
2590
  const v = voiceForAgent(slug);
2410
2591
  const label = v ? `${v.provider} · ${v.voiceId}` : uiT("ap_voice_browser_default");
2411
- const deck = v ? v.model : uiT("ap_voice_engine_browser");
2412
2592
  const speed = v?.speed ?? 1;
2413
2593
  const pitch = v?.pitch ?? 0;
2414
2594
  const emotion = v?.emotion || "";
@@ -2424,7 +2604,6 @@
2424
2604
  <button type="button" class="ap-model-trigger" data-ap-voice-trigger>
2425
2605
  <span class="ap-model-trigger-text">
2426
2606
  <span class="ap-model-trigger-name" data-ap-voice-name>${escape(label)}</span>
2427
- <span class="ap-model-trigger-provider" data-ap-voice-provider>${escape(deck)}</span>
2428
2607
  </span>
2429
2608
  <span class="ap-model-trigger-caret">▾</span>
2430
2609
  </button>
@@ -2709,6 +2888,8 @@
2709
2888
  </div>
2710
2889
  </section>
2711
2890
 
2891
+ ${renderPersonaDossierSection(slug, p)}
2892
+
2712
2893
  <section class="ap-block">
2713
2894
  <header class="ap-block-h">
2714
2895
  <span class="ap-block-h-title">${escape(uiT("ap_instruction"))}</span>
@@ -3158,6 +3339,34 @@
3158
3339
  return;
3159
3340
  }
3160
3341
 
3342
+ // Persona dossier card · open the preview overlay. Card is a
3343
+ // <button>, so the click lands on the button or any of its
3344
+ // children — closest() catches both. The overlay reads the
3345
+ // slug from the data attribute and fetches /persona.md.
3346
+ const personaOpen = e.target.closest("[data-ap-persona-open]");
3347
+ if (personaOpen) {
3348
+ e.preventDefault();
3349
+ e.stopPropagation();
3350
+ const slug = personaOpen.getAttribute("data-slug");
3351
+ if (!slug) return;
3352
+ const live = window.app && window.app.agentsById ? window.app.agentsById[slug] : null;
3353
+ const agentName = live && live.name ? live.name : "";
3354
+ openPersonaOverlay(slug, agentName);
3355
+ return;
3356
+ }
3357
+ // Persona dossier overlay · backdrop / close-button click. The
3358
+ // download anchor inside the overlay is NOT marked with the
3359
+ // close attr, so it doesn't fire here — its native href
3360
+ // navigation handles the download and the user can dismiss
3361
+ // the overlay manually if they wish.
3362
+ const personaClose = e.target.closest("[data-ap-persona-close]");
3363
+ if (personaClose) {
3364
+ e.preventDefault();
3365
+ e.stopPropagation();
3366
+ closePersonaOverlay();
3367
+ return;
3368
+ }
3369
+
3161
3370
  // ⋯ menu · open the popover (anchored to the button).
3162
3371
  const idMenuBtn = e.target.closest("[data-ap-id-menu]");
3163
3372
  if (idMenuBtn) {
@@ -3173,10 +3382,20 @@
3173
3382
  // ⋯ menu · action click.
3174
3383
  const menuAction = e.target.closest("[data-ap-menu-action]");
3175
3384
  if (menuAction) {
3176
- e.preventDefault();
3177
3385
  const action = menuAction.getAttribute("data-ap-menu-action");
3178
3386
  const pop = document.getElementById("ap-id-menu-pop");
3179
3387
  const slug = pop?.dataset.slug;
3388
+ // persona-md is rendered as an <a href=…> · let the browser
3389
+ // navigate natively (the route returns the file with a
3390
+ // download Content-Disposition). preventDefault here would
3391
+ // kill the download. For the button-based actions below we
3392
+ // DO want preventDefault (otherwise the button-as-form-
3393
+ // submitter behaviour and click bubbling can both fire).
3394
+ if (action === "persona-md") {
3395
+ closeProfileIdMenu();
3396
+ return;
3397
+ }
3398
+ e.preventDefault();
3180
3399
  closeProfileIdMenu();
3181
3400
  if (action === "regen-avatar" && slug) regenerateProfileAvatar(slug);
3182
3401
  if (action === "delete" && slug && window.app && typeof window.app.deleteAgent === "function") {