@jay-framework/aiditor 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as _jay_framework_dev_server from '@jay-framework/dev-server';
2
2
  import * as _jay_framework_fullstack_component from '@jay-framework/fullstack-component';
3
3
  import { JayFile } from '@jay-framework/fullstack-component';
4
+ import { AddMenuCatalogLintWarning, AddMenuItem, AddMenuPresentation, AddMenuBrowseSize } from '@jay-framework/plugin-validator';
5
+ export { AddMenuItem, AddMenuPresentation } from '@jay-framework/plugin-validator';
4
6
  import { PluginSetupContext, PluginSetupResult } from '@jay-framework/stack-server-runtime';
5
7
  export { aiditorPage, aiditorShell } from './index.client.js';
6
8
  import '@jay-framework/component';
@@ -121,26 +123,13 @@ declare const cancelAgentTaskAction: _jay_framework_fullstack_component.JayActio
121
123
  cancelled: boolean;
122
124
  }, []>;
123
125
 
124
- /** Design Log #19 — Add Menu agreement types (M19.1). */
125
- type AddMenuInteraction = {
126
- mode: "reference" | "stage-place";
127
- persistOnPage?: boolean;
128
- stagePromptTemplate?: string;
129
- };
130
- type AddMenuItem = {
131
- /** Stable unique id, convention: `<pluginName>:<slug>` */
132
- id: string;
133
- title: string;
134
- /** Menu navigation label — not a type system */
135
- category: string;
136
- /** Injected verbatim into agent prompt when attached */
137
- prompt: string;
138
- pluginName?: string;
139
- packageName?: string;
140
- subCategory?: string;
141
- thumbnail?: string;
142
- interaction?: AddMenuInteraction;
143
- };
126
+ /**
127
+ * AIditor Add Menu runtime types.
128
+ * Catalog item schema types come from @jay-framework/plugin-validator (source of truth).
129
+ */
130
+
131
+ /** Runtime catalog warning — same shape as plugin-validator lint findings. */
132
+ type AddMenuCatalogWarning = AddMenuCatalogLintWarning;
144
133
  type AddMenuAttachment = {
145
134
  itemId: string;
146
135
  /** Change Request: set when chip is on a marker; omit = request-level */
@@ -163,7 +152,7 @@ type AddMenuSubCategoryGroup = {
163
152
  type ListAddMenuItemsResult = {
164
153
  items: AddMenuItem[];
165
154
  categories: AddMenuCategoryGroup[];
166
- warnings: string[];
155
+ warnings: AddMenuCatalogWarning[];
167
156
  };
168
157
 
169
158
  type PageBriefAttachment = {
@@ -765,7 +754,6 @@ type PageAssetEntry = {
765
754
  source: PageAssetSource;
766
755
  itemId?: string;
767
756
  thumbnail?: string;
768
- persistOnPage: boolean;
769
757
  addedAt: string;
770
758
  /** Stable key for detected entries — used for sync + suppression */
771
759
  detectedKey?: string;
@@ -863,4 +851,14 @@ declare const runAiditorPublishAction: _jay_framework_fullstack_component.JayStr
863
851
 
864
852
  declare function setupAiditor(ctx: PluginSetupContext): Promise<PluginSetupResult>;
865
853
 
866
- export { cancelAgentTaskAction, checkAddPageRouteAction, checkAiditorPublishAction, checkPageAddPageBriefAction, checkPluginUpdateAction, ensureProjectPluginAction, generateAddPageBriefFromImageAction, getAiditorBootstrap, getContractInspectorTagsAction, getPageAssetsAction, getPageMetaAction, getPageParamsAction, getPluginSetupStatusAction, getProjectInfoAction, listAddMenuItemsAction, listFreezesAction, listJayPluginsAction, listPluginContractsAction, openAddPageFromBriefAction, readFileAction, reclassifyAddPageAssetAction, rerunPluginSetupAction, resolveAddMenuContractParamsAction, runAiditorPublishAction, saveAddPageDraftAction, savePageAssetsAction, savePageMetaAction, setupAiditor, startAddPageRequestAction, submitAddPageAction, submitTaskAction, syncAddMenuAttachmentsAction, syncAddPagePluginManifestAction, syncDetectedPageAssetsAction, uploadAddPageAssetAction, writePluginSourceAction };
854
+ declare function listAddMenuItems(projectRoot: string): Promise<ListAddMenuItemsResult>;
855
+
856
+ declare function sanitizeAddMenuHtmlFragment(html: string): string;
857
+
858
+ /** Normalize legacy `thumbnail` and explicit `presentation` to one shape (Design Log #30). */
859
+ declare function normalizeAddMenuPresentation(item: Pick<AddMenuItem, "thumbnail" | "presentation">): AddMenuPresentation | undefined;
860
+
861
+ /** Default browse footprint — Design Log #32. */
862
+ declare function normalizeAddMenuBrowseSize(item: Pick<AddMenuItem, "browse">): AddMenuBrowseSize;
863
+
864
+ export { type AddMenuCatalogWarning, type ListAddMenuItemsResult, cancelAgentTaskAction, checkAddPageRouteAction, checkAiditorPublishAction, checkPageAddPageBriefAction, checkPluginUpdateAction, ensureProjectPluginAction, generateAddPageBriefFromImageAction, getAiditorBootstrap, getContractInspectorTagsAction, getPageAssetsAction, getPageMetaAction, getPageParamsAction, getPluginSetupStatusAction, getProjectInfoAction, listAddMenuItems, listAddMenuItemsAction, listFreezesAction, listJayPluginsAction, listPluginContractsAction, normalizeAddMenuBrowseSize, normalizeAddMenuPresentation, openAddPageFromBriefAction, readFileAction, reclassifyAddPageAssetAction, rerunPluginSetupAction, resolveAddMenuContractParamsAction, runAiditorPublishAction, sanitizeAddMenuHtmlFragment, saveAddPageDraftAction, savePageAssetsAction, savePageMetaAction, setupAiditor, startAddPageRequestAction, submitAddPageAction, submitTaskAction, syncAddMenuAttachmentsAction, syncAddPagePluginManifestAction, syncDetectedPageAssetsAction, uploadAddPageAssetAction, writePluginSourceAction };
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
9
9
  import fs$1 from "fs";
10
10
  import crypto from "crypto";
11
11
  import yaml, { load } from "js-yaml";
12
+ import { validateAddMenuCatalogFile, lintAddMenuCatalog } from "@jay-framework/plugin-validator";
12
13
  import { getLogger } from "@jay-framework/logger";
13
14
  import { fileURLToPath } from "url";
14
15
  import { spawn } from "child_process";
@@ -341,131 +342,10 @@ function groupAddMenuItems(items) {
341
342
  return sa.localeCompare(sb);
342
343
  }).map(([subCategory, subItems]) => ({
343
344
  subCategory,
344
- items: subItems.sort((a, b) => a.title.localeCompare(b.title))
345
+ items: subItems
345
346
  }))
346
347
  }));
347
348
  }
348
- const REJECTED_ITEM_FIELDS = [
349
- "kind",
350
- "parameters",
351
- "component",
352
- "allowedScopes"
353
- ];
354
- function isRecord(value) {
355
- return typeof value === "object" && value !== null && !Array.isArray(value);
356
- }
357
- function requiredString(obj, field, path2, errors) {
358
- const value = obj[field];
359
- if (typeof value !== "string" || value.trim().length === 0) {
360
- errors.push({
361
- path: `${path2}.${field}`,
362
- message: "required non-empty string"
363
- });
364
- return null;
365
- }
366
- return value.trim();
367
- }
368
- function optionalString(obj, field) {
369
- const value = obj[field];
370
- if (value === void 0) return void 0;
371
- if (typeof value !== "string") return void 0;
372
- const trimmed = value.trim();
373
- return trimmed.length > 0 ? trimmed : void 0;
374
- }
375
- function optionalBoolean(obj, field) {
376
- const value = obj[field];
377
- return typeof value === "boolean" ? value : void 0;
378
- }
379
- function validateInteraction(raw, path2, errors) {
380
- if (raw === void 0) return void 0;
381
- if (!isRecord(raw)) {
382
- errors.push({ path: path2, message: "interaction must be an object" });
383
- return void 0;
384
- }
385
- const mode = raw.mode;
386
- if (mode !== "reference" && mode !== "stage-place") {
387
- errors.push({
388
- path: `${path2}.mode`,
389
- message: 'mode must be "reference" or "stage-place"'
390
- });
391
- return void 0;
392
- }
393
- return {
394
- mode,
395
- persistOnPage: optionalBoolean(raw, "persistOnPage"),
396
- stagePromptTemplate: optionalString(raw, "stagePromptTemplate")
397
- };
398
- }
399
- function validateAddMenuItem(raw, path2) {
400
- const errors = [];
401
- if (!isRecord(raw)) {
402
- return {
403
- item: null,
404
- errors: [{ path: path2, message: "item must be an object" }]
405
- };
406
- }
407
- for (const field of REJECTED_ITEM_FIELDS) {
408
- if (field in raw) {
409
- errors.push({
410
- path: `${path2}.${field}`,
411
- message: `field "${field}" is not allowed in Add Menu catalog items`
412
- });
413
- }
414
- }
415
- const id = requiredString(raw, "id", path2, errors);
416
- const title = requiredString(raw, "title", path2, errors);
417
- const category = requiredString(raw, "category", path2, errors);
418
- const prompt = requiredString(raw, "prompt", path2, errors);
419
- if (errors.length > 0 || !id || !title || !category || !prompt) {
420
- return { item: null, errors };
421
- }
422
- const interaction = validateInteraction(
423
- raw.interaction,
424
- `${path2}.interaction`,
425
- errors
426
- );
427
- return {
428
- item: {
429
- id,
430
- title,
431
- category,
432
- prompt,
433
- pluginName: optionalString(raw, "pluginName"),
434
- packageName: optionalString(raw, "packageName"),
435
- subCategory: optionalString(raw, "subCategory"),
436
- thumbnail: optionalString(raw, "thumbnail"),
437
- ...interaction ? { interaction } : {}
438
- },
439
- errors
440
- };
441
- }
442
- function validateAddMenuCatalogFile(raw, sourcePath) {
443
- const errors = [];
444
- if (!isRecord(raw)) {
445
- return {
446
- file: null,
447
- errors: [{ path: sourcePath, message: "catalog file must be an object" }]
448
- };
449
- }
450
- if (!Array.isArray(raw.items)) {
451
- return {
452
- file: null,
453
- errors: [
454
- { path: `${sourcePath}.items`, message: "items must be an array" }
455
- ]
456
- };
457
- }
458
- const items = [];
459
- raw.items.forEach((entry, index) => {
460
- const result = validateAddMenuItem(entry, `${sourcePath}.items[${index}]`);
461
- errors.push(...result.errors);
462
- if (result.item) items.push(result.item);
463
- });
464
- if (items.length === 0 && errors.length > 0) {
465
- return { file: null, errors };
466
- }
467
- return { file: { items }, errors };
468
- }
469
349
  function parsePluginsIndexYaml(yaml2) {
470
350
  const entries = [];
471
351
  let currentPlugin = null;
@@ -591,6 +471,7 @@ async function parsePluginYamlContracts(projectRoot, pluginName) {
591
471
  }
592
472
  }
593
473
  const ADD_MENU_DIR = path.join("agent-kit", "aiditor", "add-menu");
474
+ const LOCAL_PLUGINS_DIR = path.join("src", "plugins");
594
475
  function packageNameFromItem(item) {
595
476
  if (item.packageName) return item.packageName;
596
477
  if (item.pluginName) return `@jay-framework/${item.pluginName}`;
@@ -606,6 +487,39 @@ function pluginNameFromItem(item) {
606
487
  if (colon > 0) return item.id.slice(0, colon);
607
488
  return null;
608
489
  }
490
+ async function getLocalPluginNames(projectRoot) {
491
+ const names = /* @__PURE__ */ new Set();
492
+ const pluginsDir = path.join(projectRoot, LOCAL_PLUGINS_DIR);
493
+ let entries;
494
+ try {
495
+ entries = await fs.readdir(pluginsDir);
496
+ } catch (e) {
497
+ const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
498
+ if (code === "ENOENT") return names;
499
+ throw e;
500
+ }
501
+ for (const entry of entries) {
502
+ const pluginYaml = path.join(pluginsDir, entry, "plugin.yaml");
503
+ try {
504
+ const yaml2 = await fs.readFile(pluginYaml, "utf-8");
505
+ const nameMatch = yaml2.match(/^name:\s*(.+)\s*$/m);
506
+ if (nameMatch?.[1]) {
507
+ names.add(nameMatch[1].trim());
508
+ continue;
509
+ }
510
+ } catch {
511
+ }
512
+ names.add(entry);
513
+ }
514
+ return names;
515
+ }
516
+ function validationErrorToWarning(err) {
517
+ return {
518
+ code: err.code ?? "catalog-validation-error",
519
+ message: `${err.path}: ${err.message}`,
520
+ sourcePath: err.path
521
+ };
522
+ }
609
523
  async function listAddMenuItems(projectRoot) {
610
524
  const addMenuDir = path.join(projectRoot, ADD_MENU_DIR);
611
525
  const warnings = [];
@@ -623,43 +537,67 @@ async function listAddMenuItems(projectRoot) {
623
537
  const installed = await getInstalledPlugins(projectRoot);
624
538
  const installedPackages = new Set(installed.map((p) => p.packageName));
625
539
  const installedPlugins = new Set(installed.map((p) => p.pluginName));
540
+ for (const localName of await getLocalPluginNames(projectRoot)) {
541
+ installedPlugins.add(localName);
542
+ }
626
543
  const byId = /* @__PURE__ */ new Map();
544
+ const orderedIds = [];
545
+ const lintBlockedIds = /* @__PURE__ */ new Set();
627
546
  for (const filePath of yamlFiles) {
628
547
  const relPath = path.relative(projectRoot, filePath);
629
548
  let rawContent;
630
549
  try {
631
550
  rawContent = await fs.readFile(filePath, "utf-8");
632
551
  } catch {
633
- warnings.push(`Could not read Add Menu catalog file: ${relPath}`);
552
+ warnings.push({
553
+ code: "catalog-read-error",
554
+ message: `Could not read Add Menu catalog file: ${relPath}`,
555
+ sourcePath: relPath
556
+ });
634
557
  continue;
635
558
  }
636
559
  let parsed;
637
560
  try {
638
561
  parsed = load(rawContent);
639
562
  } catch (err) {
640
- warnings.push(
641
- `Invalid YAML in ${relPath}: ${err instanceof Error ? err.message : String(err)}`
642
- );
563
+ warnings.push({
564
+ code: "catalog-parse-error",
565
+ message: `Invalid YAML in ${relPath}: ${err instanceof Error ? err.message : String(err)}`,
566
+ sourcePath: relPath
567
+ });
643
568
  continue;
644
569
  }
645
570
  const validated = validateAddMenuCatalogFile(parsed, relPath);
646
- if (validated.errors.length > 0) {
647
- for (const err of validated.errors) {
648
- warnings.push(`${err.path}: ${err.message}`);
649
- }
571
+ for (const err of validated.errors) {
572
+ warnings.push(validationErrorToWarning(err));
573
+ }
574
+ if (!validated.file) {
650
575
  continue;
651
576
  }
577
+ const linted = lintAddMenuCatalog(validated.file.items, relPath);
578
+ for (const err of linted.errors) {
579
+ warnings.push(err);
580
+ if (err.itemId) lintBlockedIds.add(err.itemId);
581
+ }
582
+ warnings.push(...linted.warnings);
652
583
  for (const item of validated.file.items) {
584
+ if (lintBlockedIds.has(item.id)) continue;
653
585
  const prev = byId.get(item.id);
654
586
  if (prev) {
655
- warnings.push(
656
- `Duplicate Add Menu item id "${item.id}": "${prev.sourcePath}" overwritten by "${relPath}"`
657
- );
587
+ warnings.push({
588
+ code: "catalog-duplicate-id",
589
+ message: `Duplicate Add Menu item id "${item.id}": "${prev.sourcePath}" overwritten by "${relPath}"`,
590
+ itemId: item.id,
591
+ sourcePath: relPath
592
+ });
593
+ byId.set(item.id, { item, sourcePath: relPath });
594
+ continue;
658
595
  }
659
596
  byId.set(item.id, { item, sourcePath: relPath });
597
+ orderedIds.push(item.id);
660
598
  }
661
599
  }
662
- const merged = [...byId.values()].map((entry) => entry.item);
600
+ const merged = orderedIds.map((id) => byId.get(id)?.item).filter((item) => item !== void 0);
663
601
  const filtered = merged.filter((item) => {
664
602
  const pkg = packageNameFromItem(item);
665
603
  const plugin = pluginNameFromItem(item);
@@ -668,7 +606,7 @@ async function listAddMenuItems(projectRoot) {
668
606
  return false;
669
607
  });
670
608
  return {
671
- items: filtered.sort((a, b) => a.title.localeCompare(b.title)),
609
+ items: filtered,
672
610
  categories: groupAddMenuItems(filtered),
673
611
  warnings
674
612
  };
@@ -941,7 +879,7 @@ function buildRouteMigrationPromptSection(routeMigration) {
941
879
  ].join("\n");
942
880
  }
943
881
  function buildPageAssetsPromptSection(assets) {
944
- const active = assets.filter((a) => a.persistOnPage && !a.userRemoved);
882
+ const active = assets.filter((a) => !a.userRemoved);
945
883
  if (active.length === 0) return "";
946
884
  const lines = [
947
885
  "Page assets (in scope for this page — use when referenced in marker instructions or @mentions):"
@@ -4423,7 +4361,6 @@ function detectedToPageAssetEntry(detected) {
4423
4361
  prompt: detected.prompt,
4424
4362
  source: "detected",
4425
4363
  itemId: detected.pluginName ? `${detected.pluginName}:${detected.contractName ?? ""}` : void 0,
4426
- persistOnPage: true,
4427
4364
  addedAt: (/* @__PURE__ */ new Date()).toISOString(),
4428
4365
  detectedKey: detected.detectedKey,
4429
4366
  detectedMeta: {
@@ -4645,6 +4582,34 @@ async function setupAiditor(ctx) {
4645
4582
  ...filesUpdated.length > 0 ? { filesUpdated } : {}
4646
4583
  };
4647
4584
  }
4585
+ const BLOCKED_TAGS = /<\s*(script|iframe|object|embed)\b[^>]*>[\s\S]*?<\/\s*\1\s*>|<\s*(script|iframe|object|embed)\b[^>]*\/?>/gi;
4586
+ const EVENT_HANDLER_ATTR = /\s+on[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi;
4587
+ const JAVASCRIPT_URL = /\b(href|src|xlink:href)\s*=\s*("|')\s*javascript:/gi;
4588
+ function sanitizeAddMenuHtmlFragment(html) {
4589
+ let result = html;
4590
+ result = result.replace(BLOCKED_TAGS, "");
4591
+ BLOCKED_TAGS.lastIndex = 0;
4592
+ result = result.replace(EVENT_HANDLER_ATTR, "");
4593
+ EVENT_HANDLER_ATTR.lastIndex = 0;
4594
+ result = result.replace(JAVASCRIPT_URL, "");
4595
+ JAVASCRIPT_URL.lastIndex = 0;
4596
+ return result.trim();
4597
+ }
4598
+ function isGifPath(src) {
4599
+ return /\.gif($|\?)/i.test(src.trim());
4600
+ }
4601
+ function normalizeAddMenuPresentation(item) {
4602
+ if (item.presentation) return item.presentation;
4603
+ const thumbnail = item.thumbnail?.trim();
4604
+ if (!thumbnail) return void 0;
4605
+ if (isGifPath(thumbnail)) {
4606
+ return { type: "gif", src: thumbnail };
4607
+ }
4608
+ return { type: "image", src: thumbnail };
4609
+ }
4610
+ function normalizeAddMenuBrowseSize(item) {
4611
+ return item.browse?.size ?? "medium";
4612
+ }
4648
4613
  const aiditorShell = makeJayStackComponent().withProps().withSlowlyRender(async () => {
4649
4614
  return phaseOutput({}, { headline: "AIditor" });
4650
4615
  }).withFastRender(async (_props, carryForward) => {
@@ -4675,16 +4640,20 @@ export {
4675
4640
  getPageParamsAction,
4676
4641
  getPluginSetupStatusAction,
4677
4642
  getProjectInfoAction,
4643
+ listAddMenuItems,
4678
4644
  listAddMenuItemsAction,
4679
4645
  listFreezesAction,
4680
4646
  listJayPluginsAction,
4681
4647
  listPluginContractsAction,
4648
+ normalizeAddMenuBrowseSize,
4649
+ normalizeAddMenuPresentation,
4682
4650
  openAddPageFromBriefAction,
4683
4651
  readFileAction,
4684
4652
  reclassifyAddPageAssetAction,
4685
4653
  rerunPluginSetupAction,
4686
4654
  resolveAddMenuContractParamsAction,
4687
4655
  runAiditorPublishAction,
4656
+ sanitizeAddMenuHtmlFragment,
4688
4657
  saveAddPageDraftAction,
4689
4658
  savePageAssetsAction,
4690
4659
  savePageMetaAction,
@@ -290,7 +290,7 @@ body {
290
290
  padding: 5px 8px;
291
291
  background: var(--harmony-bg-surface);
292
292
  border: 1px solid var(--harmony-border);
293
- border-radius: var(--harmony-radius-pill);
293
+ border-radius: 8px;
294
294
  box-shadow:
295
295
  0 6px 28px rgba(22, 45, 61, 0.14),
296
296
  0 2px 6px rgba(22, 45, 61, 0.06);
@@ -896,7 +896,9 @@ body {
896
896
  background: transparent;
897
897
  color: var(--harmony-text-muted);
898
898
  cursor: pointer;
899
- transition: color 0.12s ease, background 0.12s ease;
899
+ transition:
900
+ color 0.12s ease,
901
+ background 0.12s ease;
900
902
  }
901
903
  .url-bar-open-tab:hover:not(:disabled) {
902
904
  color: var(--harmony-text);
@@ -1602,11 +1604,24 @@ body {
1602
1604
  max-height: 94vh;
1603
1605
  display: flex;
1604
1606
  flex-direction: column;
1605
- gap: 10px;
1606
- padding: 14px 16px;
1607
+ gap: 24px;
1608
+ padding: 32px 32px;
1607
1609
  overflow: hidden;
1608
1610
  box-shadow: var(--harmony-shadow-lg);
1609
1611
  }
1612
+ .video-review-panel {
1613
+ position: relative;
1614
+ }
1615
+ .video-review-header {
1616
+ align-items: flex-start;
1617
+ padding-right: 28px;
1618
+ }
1619
+ .video-review-panel .harmony-modal-close {
1620
+ position: absolute;
1621
+ top: 12px;
1622
+ right: 12px;
1623
+ z-index: 1;
1624
+ }
1610
1625
  .video-review-body {
1611
1626
  display: flex;
1612
1627
  flex-direction: column;
@@ -1614,23 +1629,27 @@ body {
1614
1629
  min-height: 0;
1615
1630
  flex: 1 1 auto;
1616
1631
  }
1617
- .video-review-tools {
1618
- display: inline-flex;
1619
- flex-wrap: nowrap;
1620
- gap: 2px;
1621
- align-items: center;
1622
- width: fit-content;
1623
- }
1624
- .video-review-tools .visual-tool-btn {
1625
- padding: 7px 8px;
1626
- min-width: 28px;
1627
- }
1628
1632
  .video-review-capture-root {
1633
+ position: relative;
1629
1634
  flex: 0 1 auto;
1630
1635
  min-height: 0;
1631
1636
  max-height: min(56vh, 480px);
1632
1637
  overflow: hidden;
1633
1638
  }
1639
+ .video-review-toolbar-wrap {
1640
+ position: absolute;
1641
+ bottom: 12px;
1642
+ left: 50%;
1643
+ transform: translateX(-50%);
1644
+ z-index: 12;
1645
+ pointer-events: none;
1646
+ }
1647
+ .video-review-toolbar-wrap > * {
1648
+ pointer-events: auto;
1649
+ }
1650
+ .video-review-toolbar-wrap .annotate-dock {
1651
+ border-radius: 8px;
1652
+ }
1634
1653
  .video-review-capture-root .preview-frame {
1635
1654
  width: 100%;
1636
1655
  height: auto;
@@ -1638,6 +1657,9 @@ body {
1638
1657
  max-height: min(56vh, 480px);
1639
1658
  object-fit: contain;
1640
1659
  display: block;
1660
+ border: 1px solid rgba(0, 0, 0, 0.2);
1661
+ border-radius: 8px;
1662
+ box-sizing: border-box;
1641
1663
  }
1642
1664
  .snapshot-panel {
1643
1665
  width: min(1200px, 96vw);
@@ -1647,10 +1669,12 @@ body {
1647
1669
  .snapshot-header {
1648
1670
  display: flex;
1649
1671
  justify-content: space-between;
1650
- align-items: center;
1651
1672
  gap: 12px;
1652
1673
  flex-wrap: wrap;
1653
1674
  }
1675
+ .snapshot-header {
1676
+ align-items: center;
1677
+ }
1654
1678
  .video-review-title,
1655
1679
  .snapshot-title {
1656
1680
  font-size: 15px;
@@ -1658,9 +1682,21 @@ body {
1658
1682
  color: var(--harmony-text);
1659
1683
  margin: 0;
1660
1684
  }
1661
- .video-review-meta {
1685
+ .video-review-title {
1686
+ font-size: 20px;
1687
+ }
1688
+ .video-review-subtitle {
1689
+ font-size: 16px;
1690
+ color: var(--harmony-text-secondary);
1691
+ margin: 4px 0 0;
1692
+ line-height: 1.35;
1693
+ }
1694
+ .video-review-time {
1695
+ flex-shrink: 0;
1662
1696
  font-size: 12px;
1663
1697
  color: var(--harmony-text-muted);
1698
+ font-variant-numeric: tabular-nums;
1699
+ white-space: nowrap;
1664
1700
  }
1665
1701
  .snapshot-tools {
1666
1702
  display: flex;
@@ -2304,7 +2340,7 @@ body {
2304
2340
  .add-menu-picker-search-icon {
2305
2341
  color: var(--harmony-text-muted);
2306
2342
  font-size: 30px;
2307
- line-height: 1;
2343
+ line-height: 0;
2308
2344
  flex-shrink: 0;
2309
2345
  }
2310
2346
  .add-menu-picker-search-input {
@@ -3874,7 +3910,7 @@ body {
3874
3910
  }
3875
3911
  .add-menu-dock-subhint {
3876
3912
  margin: 0;
3877
- padding: 8px 18px 0;
3913
+ padding: 2px 18px 6px;
3878
3914
  font-size: 12px;
3879
3915
  line-height: 1.35;
3880
3916
  color: var(--harmony-text-secondary);