dsh-select-quote 0.3.21 → 0.4.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/lib/client.js +890 -359
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -95,6 +95,8 @@ const zh = {
|
|
|
95
95
|
"toolbar.askInSide": "在侧边提问",
|
|
96
96
|
"toolbar.sideTaskFailed": "侧边提问创建失败",
|
|
97
97
|
"toolbar.sideTaskUnavailable": "当前无法创建侧边提问",
|
|
98
|
+
"toolbar.sideUnavailable": "侧边提问不可用:宿主的侧边栏服务未就绪",
|
|
99
|
+
"toolbar.translateUnavailable": "翻译不可用:宿主的模型服务未就绪",
|
|
98
100
|
"sideTask.title": "在侧边提问",
|
|
99
101
|
"sideTask.placeholder": "就这段原文提问…",
|
|
100
102
|
"sideTask.send": "发送",
|
|
@@ -238,6 +240,8 @@ const en = {
|
|
|
238
240
|
"toolbar.askInSide": "Ask in side task",
|
|
239
241
|
"toolbar.sideTaskFailed": "Could not start the side task",
|
|
240
242
|
"toolbar.sideTaskUnavailable": "The side task is unavailable here",
|
|
243
|
+
"toolbar.sideUnavailable": "Sidebar questions are unavailable: the host sidebar service is not ready",
|
|
244
|
+
"toolbar.translateUnavailable": "Translation is unavailable: the host model service is not ready",
|
|
241
245
|
"sideTask.title": "Ask in side task",
|
|
242
246
|
"sideTask.placeholder": "Ask about this passage…",
|
|
243
247
|
"sideTask.send": "Send",
|
|
@@ -383,6 +387,8 @@ const ja = {
|
|
|
383
387
|
"toolbar.askInSide": "サイドで質問",
|
|
384
388
|
"toolbar.sideTaskFailed": "サイドタスクを開始できませんでした",
|
|
385
389
|
"toolbar.sideTaskUnavailable": "この画面ではサイドタスクを使えません",
|
|
390
|
+
"toolbar.sideUnavailable": "サイドバー質問は利用できません: ホストのサイドバーサービスが未準備です",
|
|
391
|
+
"toolbar.translateUnavailable": "翻訳は利用できません: ホストのモデルサービスが未準備です",
|
|
386
392
|
"sideTask.title": "サイドで質問",
|
|
387
393
|
"sideTask.placeholder": "この箇所について質問…",
|
|
388
394
|
"sideTask.send": "送信",
|
|
@@ -526,6 +532,8 @@ const es = {
|
|
|
526
532
|
"toolbar.askInSide": "Preguntar en tarea lateral",
|
|
527
533
|
"toolbar.sideTaskFailed": "No se pudo iniciar la tarea lateral",
|
|
528
534
|
"toolbar.sideTaskUnavailable": "La tarea lateral no está disponible aquí",
|
|
535
|
+
"toolbar.sideUnavailable": "Las preguntas laterales no están disponibles: el servicio de barra lateral del host no está listo",
|
|
536
|
+
"toolbar.translateUnavailable": "La traducción no está disponible: el servicio de modelos del host no está listo",
|
|
529
537
|
"sideTask.title": "Preguntar en tarea lateral",
|
|
530
538
|
"sideTask.placeholder": "Pregunta sobre este pasaje…",
|
|
531
539
|
"sideTask.send": "Enviar",
|
|
@@ -671,6 +679,8 @@ const fr = {
|
|
|
671
679
|
"toolbar.askInSide": "Demander dans une tâche latérale",
|
|
672
680
|
"toolbar.sideTaskFailed": "Impossible de démarrer la tâche latérale",
|
|
673
681
|
"toolbar.sideTaskUnavailable": "La tâche latérale n’est pas disponible ici",
|
|
682
|
+
"toolbar.sideUnavailable": "Les questions latérales sont indisponibles : le service de barre latérale de l’hôte n’est pas prêt",
|
|
683
|
+
"toolbar.translateUnavailable": "La traduction est indisponible : le service de modèles de l’hôte n’est pas prêt",
|
|
674
684
|
"sideTask.title": "Demander dans une tâche latérale",
|
|
675
685
|
"sideTask.placeholder": "Posez une question sur ce passage…",
|
|
676
686
|
"sideTask.send": "Envoyer",
|
|
@@ -816,6 +826,8 @@ const de = {
|
|
|
816
826
|
"toolbar.askInSide": "In Seitenaufgabe fragen",
|
|
817
827
|
"toolbar.sideTaskFailed": "Seitenaufgabe konnte nicht gestartet werden",
|
|
818
828
|
"toolbar.sideTaskUnavailable": "Die Seitenaufgabe ist hier nicht verfügbar",
|
|
829
|
+
"toolbar.sideUnavailable": "Seitenfragen sind nicht verfügbar: der Sidebar-Dienst des Hosts ist nicht bereit",
|
|
830
|
+
"toolbar.translateUnavailable": "Übersetzung ist nicht verfügbar: der Modell-Dienst des Hosts ist nicht bereit",
|
|
819
831
|
"sideTask.title": "In Seitenaufgabe fragen",
|
|
820
832
|
"sideTask.placeholder": "Frage zu dieser Passage stellen…",
|
|
821
833
|
"sideTask.send": "Senden",
|
|
@@ -961,6 +973,8 @@ const ru = {
|
|
|
961
973
|
"toolbar.askInSide": "Спросить в боковой задаче",
|
|
962
974
|
"toolbar.sideTaskFailed": "Не удалось открыть боковую задачу",
|
|
963
975
|
"toolbar.sideTaskUnavailable": "Боковая задача недоступна в этом окне",
|
|
976
|
+
"toolbar.sideUnavailable": "Боковые вопросы недоступны: служба боковой панели хоста не готова",
|
|
977
|
+
"toolbar.translateUnavailable": "Перевод недоступен: служба моделей хоста не готова",
|
|
964
978
|
"sideTask.title": "Спросить в боковой задаче",
|
|
965
979
|
"sideTask.placeholder": "Спросите об этом фрагменте…",
|
|
966
980
|
"sideTask.send": "Отправить",
|
|
@@ -1110,6 +1124,8 @@ const pt = {
|
|
|
1110
1124
|
"toolbar.askInSide": "Perguntar na tarefa lateral",
|
|
1111
1125
|
"toolbar.sideTaskFailed": "Não foi possível iniciar a tarefa lateral",
|
|
1112
1126
|
"toolbar.sideTaskUnavailable": "A tarefa lateral não está disponível aqui",
|
|
1127
|
+
"toolbar.sideUnavailable": "Perguntas laterais indisponíveis: o serviço de barra lateral do host não está pronto",
|
|
1128
|
+
"toolbar.translateUnavailable": "A tradução está indisponível: o serviço de modelos do host não está pronto",
|
|
1113
1129
|
"sideTask.title": "Perguntar na tarefa lateral",
|
|
1114
1130
|
"sideTask.placeholder": "Pergunte sobre esta passagem…",
|
|
1115
1131
|
"sideTask.send": "Enviar",
|
|
@@ -1544,7 +1560,7 @@ const COMPONENTS_CSS = [
|
|
|
1544
1560
|
--dsq-shadow: var(--dsw-elevation-prominent, 0 0 0 .5px rgba(0, 0, 0, 0.04), 0 3px 8px rgba(0, 0, 0, 0.04), 0 0 20px rgba(0, 0, 0, 0.05));
|
|
1545
1561
|
/* Every surface above takes the same elevation; declaring it once here is
|
|
1546
1562
|
what keeps the component blocks free of a copied box-shadow. */
|
|
1547
|
-
box-shadow: var(--
|
|
1563
|
+
box-shadow: var(--dsw-elevation-prominent, 0 0 0 0.5px rgba(0, 0, 0, 0.04), 0 3px 8px rgba(0, 0, 0, 0.06));
|
|
1548
1564
|
}
|
|
1549
1565
|
|
|
1550
1566
|
/* 浮层挂载与层带(2026-10-20 定,改前先读):
|
|
@@ -1623,6 +1639,48 @@ const COMPONENTS_CSS = [
|
|
|
1623
1639
|
background: var(--dsq-fill-hover);
|
|
1624
1640
|
}
|
|
1625
1641
|
|
|
1642
|
+
/* An entry point the Host cannot run right now stays in the row, dimmed, and
|
|
1643
|
+
explains itself on hover. Removing it instead would read as a missing
|
|
1644
|
+
feature; a click that only produces a toast makes the user hunt for why. */
|
|
1645
|
+
.dsq_button[aria-disabled='true'],
|
|
1646
|
+
.dsq_touchAction[aria-disabled='true'],
|
|
1647
|
+
.dsq_ctxItem[aria-disabled='true'],
|
|
1648
|
+
.dsq_handoffChevron[aria-disabled='true'] {
|
|
1649
|
+
opacity: 0.42;
|
|
1650
|
+
cursor: default;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
.dsq_button[aria-disabled='true']:hover {
|
|
1654
|
+
background: transparent;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/* Hover tooltip. A native title would be slower and styled by the OS; this
|
|
1658
|
+
rides the same token set as the rest of the chrome. */
|
|
1659
|
+
[data-dsq-tip] {
|
|
1660
|
+
position: relative;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
[data-dsq-tip]:hover::after {
|
|
1664
|
+
content: attr(data-dsq-tip);
|
|
1665
|
+
position: absolute;
|
|
1666
|
+
top: calc(100% + 8px);
|
|
1667
|
+
left: 50%;
|
|
1668
|
+
z-index: 4;
|
|
1669
|
+
max-width: 240px;
|
|
1670
|
+
padding: 5px 9px;
|
|
1671
|
+
border-radius: 8px;
|
|
1672
|
+
border: 0.5px solid var(--dsq-line);
|
|
1673
|
+
background: var(--dsq-bg-raised);
|
|
1674
|
+
color: var(--dsq-label);
|
|
1675
|
+
box-shadow: var(--dsw-elevation-prominent, 0 0 0 0.5px rgba(0, 0, 0, 0.04), 0 3px 8px rgba(0, 0, 0, 0.08));
|
|
1676
|
+
font-size: 11.5px;
|
|
1677
|
+
line-height: 16px;
|
|
1678
|
+
font-weight: 400;
|
|
1679
|
+
white-space: normal;
|
|
1680
|
+
pointer-events: none;
|
|
1681
|
+
transform: translateX(-50%);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1626
1684
|
/* Accent-filled actions: comment commit and annotation save. Only the
|
|
1627
1685
|
metrics differ; fill and hover live here once. */
|
|
1628
1686
|
.dsq_buttonAddComment,
|
|
@@ -1700,16 +1758,30 @@ const COMPONENTS_CSS = [
|
|
|
1700
1758
|
only where they anchor differs. */
|
|
1701
1759
|
.dsq_status,
|
|
1702
1760
|
.dsq_touchStatus {
|
|
1703
|
-
|
|
1761
|
+
display: inline-flex;
|
|
1762
|
+
align-items: center;
|
|
1763
|
+
gap: 6px;
|
|
1764
|
+
padding: 5px 11px 5px 9px;
|
|
1704
1765
|
border-radius: 999px;
|
|
1705
|
-
|
|
1706
|
-
|
|
1766
|
+
border: 0.5px solid var(--dsq-toast-line);
|
|
1767
|
+
/* Surface colours, not an inverted pill: a light page gets a light chip with
|
|
1768
|
+
dark text, a dark page a dark chip with light text. The hairline keeps the
|
|
1769
|
+
chip readable when it sits on a same-tone bubble. */
|
|
1770
|
+
background: var(--dsq-toast-bg);
|
|
1771
|
+
color: var(--dsq-toast-fg);
|
|
1772
|
+
box-shadow: var(--dsw-elevation-prominent, 0 0 0 0.5px rgba(0, 0, 0, 0.04), 0 3px 8px rgba(0, 0, 0, 0.06));
|
|
1707
1773
|
font-size: 12px;
|
|
1708
1774
|
line-height: 18px;
|
|
1709
1775
|
pointer-events: none;
|
|
1710
1776
|
white-space: nowrap;
|
|
1711
1777
|
}
|
|
1712
1778
|
|
|
1779
|
+
/* Every current status is a failure notice, so the icon is the error glyph. */
|
|
1780
|
+
.dsq_statusIcon {
|
|
1781
|
+
flex: none;
|
|
1782
|
+
color: var(--dsq-danger);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1713
1785
|
.dsq_status {
|
|
1714
1786
|
position: absolute;
|
|
1715
1787
|
z-index: 3;
|
|
@@ -2120,8 +2192,9 @@ const COMPONENTS_CSS = [
|
|
|
2120
2192
|
left: 8px;
|
|
2121
2193
|
z-index: 12;
|
|
2122
2194
|
width: 20%;
|
|
2123
|
-
|
|
2124
|
-
|
|
2195
|
+
/* Wide enough for the title and an inline confirm chip on one line. */
|
|
2196
|
+
min-width: 210px;
|
|
2197
|
+
max-width: 240px;
|
|
2125
2198
|
align-items: stretch;
|
|
2126
2199
|
gap: 6px;
|
|
2127
2200
|
pointer-events: auto;
|
|
@@ -2137,6 +2210,9 @@ const COMPONENTS_CSS = [
|
|
|
2137
2210
|
width: 20px;
|
|
2138
2211
|
height: 20px;
|
|
2139
2212
|
padding: 0;
|
|
2213
|
+
/* On exit the confirm chip hands the corner back to this 20px button; the
|
|
2214
|
+
label inside is still fading, so clip it instead of letting it spill. */
|
|
2215
|
+
overflow: hidden;
|
|
2140
2216
|
border-radius: 6px;
|
|
2141
2217
|
color: var(--dsq-label-3);
|
|
2142
2218
|
font-size: 14px;
|
|
@@ -2293,9 +2369,13 @@ const COMPONENTS_CSS = [
|
|
|
2293
2369
|
}
|
|
2294
2370
|
|
|
2295
2371
|
.dsq_summaryItem {
|
|
2372
|
+
position: relative;
|
|
2296
2373
|
display: block;
|
|
2297
2374
|
padding: 8px 10px;
|
|
2298
2375
|
border-radius: 8px;
|
|
2376
|
+
/* The confirm pill overlays the row; clip it to the card so a very narrow
|
|
2377
|
+
summary can never push content outside the visible surface. */
|
|
2378
|
+
overflow: hidden;
|
|
2299
2379
|
transition: background-color 0.12s;
|
|
2300
2380
|
}
|
|
2301
2381
|
|
|
@@ -2304,10 +2384,13 @@ const COMPONENTS_CSS = [
|
|
|
2304
2384
|
}
|
|
2305
2385
|
|
|
2306
2386
|
.dsq_summaryItemHead {
|
|
2387
|
+
position: relative;
|
|
2307
2388
|
gap: 6px;
|
|
2308
2389
|
/* Normal mode's action buttons set the row height; keep it in edit mode too
|
|
2309
2390
|
so the badge and the label never move when the actions disappear. */
|
|
2310
2391
|
min-height: 26px;
|
|
2392
|
+
/* The confirm pill and the row label both clip here, never squeeze the row. */
|
|
2393
|
+
min-width: 0;
|
|
2311
2394
|
}
|
|
2312
2395
|
|
|
2313
2396
|
.dsq_summaryItemBadge {
|
|
@@ -2345,6 +2428,106 @@ const COMPONENTS_CSS = [
|
|
|
2345
2428
|
color: var(--dsq-label);
|
|
2346
2429
|
}
|
|
2347
2430
|
|
|
2431
|
+
/* Second step of a remove: the same button grows a "确认移除" label.
|
|
2432
|
+
*
|
|
2433
|
+
* Layout here is load-bearing. The summary card is 20% wide (min 148px) and the
|
|
2434
|
+
* head row is a flex line that already holds the ordinal badge and the
|
|
2435
|
+
* "选中文本" label. Growing a label *inside* that row cannot work: whatever the
|
|
2436
|
+
* button asks for, the flex line only has the leftover width, and a shrinkable
|
|
2437
|
+
* flex item ends up a few pixels wide with the label wrapping one character per
|
|
2438
|
+
* line — the collapse this rule exists to prevent.
|
|
2439
|
+
*
|
|
2440
|
+
* So the confirm step leaves the flow entirely: the button keeps its 26px
|
|
2441
|
+
* square footprint (nothing shifts), and the "确认移除" pill is an absolutely
|
|
2442
|
+
* positioned overlay measured against that square, so the badge and the row's
|
|
2443
|
+
* label can never squeeze it. */
|
|
2444
|
+
.dsq_summaryItemActions {
|
|
2445
|
+
position: relative;
|
|
2446
|
+
min-width: 0;
|
|
2447
|
+
}
|
|
2448
|
+
/* Second step of a remove: the same button becomes an in-flow pill.
|
|
2449
|
+
*
|
|
2450
|
+
* The shared rule (preview.ts) already grows a "确认移除" label by animating its
|
|
2451
|
+
* max-width. That animation only reads when the button stays in flow: the
|
|
2452
|
+
* sibling edit button slides left as the label grows. So this rule deliberately
|
|
2453
|
+
* does NOT position the button or its label — the original breakage was a flex
|
|
2454
|
+
* squeeze (the row shrank the growing button to a few pixels and the label
|
|
2455
|
+
* wrapped one character per line), not placement. flex:none plus nowrap keeps
|
|
2456
|
+
* the pill at its natural width; the row label truncates instead, so nothing
|
|
2457
|
+
* is covered and nothing wraps. */
|
|
2458
|
+
.dsq_summaryItemActions .dsq_deleteConfirm {
|
|
2459
|
+
flex: none;
|
|
2460
|
+
white-space: nowrap;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/* Collapsed label reserves no width, so the pill only occupies what it shows. */
|
|
2464
|
+
.dsq_summaryItemActions .dsq_deleteConfirm .dsq_deleteConfirmInner {
|
|
2465
|
+
white-space: nowrap;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
/* The row label yields width to the growing pill rather than competing. */
|
|
2469
|
+
.dsq_summaryItemHead .dsq_summaryItemLabel {
|
|
2470
|
+
min-width: 0;
|
|
2471
|
+
margin-top: 0;
|
|
2472
|
+
overflow: hidden;
|
|
2473
|
+
text-overflow: ellipsis;
|
|
2474
|
+
white-space: nowrap;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/* "移除全部" on the summary card, second step.
|
|
2478
|
+
*
|
|
2479
|
+
* The chip keeps the icon button's corner (absolute, right-anchored) and only
|
|
2480
|
+
* claims a fixed width, so it grows leftwards without touching the card layout:
|
|
2481
|
+
* the badge, the title and the hint stay exactly where they were, with no
|
|
2482
|
+
* reflow and no jitter. Because cardClose already sits in that corner, the
|
|
2483
|
+
* confirm step is the same control in the same place — it just gets wider.
|
|
2484
|
+
*
|
|
2485
|
+
* Fixed rather than shrink-to-fit: the width has to hold "× 确认移除" at the
|
|
2486
|
+
* smallest text size the app uses, and a fixed value keeps the two states
|
|
2487
|
+
* visually interchangeable. */
|
|
2488
|
+
.dsq_summary > .dsq_deleteConfirm {
|
|
2489
|
+
position: absolute;
|
|
2490
|
+
top: 4px;
|
|
2491
|
+
right: 4px;
|
|
2492
|
+
z-index: 3;
|
|
2493
|
+
display: inline-flex;
|
|
2494
|
+
align-items: center;
|
|
2495
|
+
justify-content: center;
|
|
2496
|
+
width: 80px;
|
|
2497
|
+
height: 26px;
|
|
2498
|
+
padding: 0;
|
|
2499
|
+
gap: 0;
|
|
2500
|
+
white-space: nowrap;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
.dsq_summary > .dsq_deleteConfirm .dsq_deleteConfirmInner {
|
|
2504
|
+
display: inline-flex;
|
|
2505
|
+
align-items: center;
|
|
2506
|
+
justify-content: center;
|
|
2507
|
+
white-space: nowrap;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
/* The pill reads "× 确认移除": the icon already precedes the label in the DOM,
|
|
2511
|
+
so in flow the two sit side by side inside one chip. */
|
|
2512
|
+
.dsq_summary > .dsq_deleteConfirm .dsq_deleteConfirmInner {
|
|
2513
|
+
display: inline-flex;
|
|
2514
|
+
align-items: center;
|
|
2515
|
+
white-space: nowrap;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
/* Body gives way by truncating, never by wrapping. */
|
|
2519
|
+
.dsq_summaryBody {
|
|
2520
|
+
min-width: 0;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
.dsq_summaryTitle,
|
|
2524
|
+
.dsq_summaryHint {
|
|
2525
|
+
min-width: 0;
|
|
2526
|
+
overflow: hidden;
|
|
2527
|
+
text-overflow: ellipsis;
|
|
2528
|
+
white-space: nowrap;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2348
2531
|
.dsq_summaryItemMain {
|
|
2349
2532
|
min-width: 0;
|
|
2350
2533
|
/* The lead-in lives on the shared wrapper, not on the normal-mode button, so
|
|
@@ -2411,6 +2594,26 @@ const COMPONENTS_CSS = [
|
|
|
2411
2594
|
color: var(--dsq-label-3);
|
|
2412
2595
|
}
|
|
2413
2596
|
|
|
2597
|
+
/* A user message that carries annotations but no words.
|
|
2598
|
+
*
|
|
2599
|
+
* The bubble is Host-rendered now, and the Host decides to show it from the raw
|
|
2600
|
+
* content: an annotation-only message still has content (the protocol block), so
|
|
2601
|
+
* the bubble paints — but our userText rule renders that block as empty text, so
|
|
2602
|
+
* what remains is an empty rounded box above the card. Hide it, and hide it only
|
|
2603
|
+
* when the bubble really holds nothing else:
|
|
2604
|
+
*
|
|
2605
|
+
* bubble > (only child) userText root > (only child) text seat > (only child) empty run
|
|
2606
|
+
*
|
|
2607
|
+
* Any attachment, file card, reference summary or extra JSON block adds a
|
|
2608
|
+
* sibling at one of those levels and keeps the bubble visible. */
|
|
2609
|
+
[data-message-bubble]:has(
|
|
2610
|
+
> span:only-child
|
|
2611
|
+
> span[data-slot='conversation.user.text.node']:only-child
|
|
2612
|
+
> span:empty:only-child
|
|
2613
|
+
) {
|
|
2614
|
+
display: none;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2414
2617
|
/* Inline edit form rendered inside the hover panel item (no extra layer).
|
|
2415
2618
|
Shares .dsq_summaryItemText with the read view, but shows the full quote
|
|
2416
2619
|
instead of clamp-reading it. */
|
|
@@ -2560,6 +2763,10 @@ const COMPONENTS_CSS = [
|
|
|
2560
2763
|
|
|
2561
2764
|
.dsq_translateHead {
|
|
2562
2765
|
gap: 6px;
|
|
2766
|
+
/* Without this the row stretches its children: the 24px close button then
|
|
2767
|
+
pins to the top of a taller row, so its hover plate looks detached from
|
|
2768
|
+
the icon. */
|
|
2769
|
+
align-items: center;
|
|
2563
2770
|
}
|
|
2564
2771
|
|
|
2565
2772
|
.dsq_translateTargetLabel {
|
|
@@ -2571,9 +2778,9 @@ const COMPONENTS_CSS = [
|
|
|
2571
2778
|
|
|
2572
2779
|
.dsq_translateClose {
|
|
2573
2780
|
flex: none;
|
|
2574
|
-
width:
|
|
2575
|
-
height:
|
|
2576
|
-
border-radius:
|
|
2781
|
+
width: 28px;
|
|
2782
|
+
height: 28px;
|
|
2783
|
+
border-radius: 8px;
|
|
2577
2784
|
color: var(--dsq-label-3);
|
|
2578
2785
|
}
|
|
2579
2786
|
|
|
@@ -2751,9 +2958,30 @@ const COMPONENTS_CSS = [
|
|
|
2751
2958
|
|
|
2752
2959
|
.dsq_translateActions {
|
|
2753
2960
|
justify-content: flex-end;
|
|
2961
|
+
align-items: center;
|
|
2754
2962
|
gap: 6px;
|
|
2755
2963
|
}
|
|
2756
2964
|
|
|
2965
|
+
/* Copy result: an icon button in the actions row, matching the close control's
|
|
2966
|
+
footprint so the two read as one family. The check glyph is the copied
|
|
2967
|
+
confirmation; the label stays on aria-label/title. */
|
|
2968
|
+
.dsq_translateCopy {
|
|
2969
|
+
flex: none;
|
|
2970
|
+
width: 28px;
|
|
2971
|
+
height: 28px;
|
|
2972
|
+
border-radius: 8px;
|
|
2973
|
+
color: var(--dsq-label-2);
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
.dsq_translateCopy:hover {
|
|
2977
|
+
background: var(--dsq-fill-hover);
|
|
2978
|
+
color: var(--dsq-label);
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
.dsq_translateCopy[data-copied='true'] {
|
|
2982
|
+
color: var(--dsq-accent);
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2757
2985
|
/* Language dropdown shared by the popover and the settings page. The trigger
|
|
2758
2986
|
sits inline; the menu is a body-level frosted list so neither surface can
|
|
2759
2987
|
clip it. */
|
|
@@ -2932,13 +3160,19 @@ const COMPONENTS_CSS = [
|
|
|
2932
3160
|
white-space: nowrap;
|
|
2933
3161
|
}
|
|
2934
3162
|
|
|
3163
|
+
/* Exit transition (this rule is the state the label lands in when confirm is
|
|
3164
|
+
cancelled). Text leaves first and the box collapses afterwards: a 50ms fade
|
|
3165
|
+
followed by a delayed width run, so the eye never tracks a half-transparent
|
|
3166
|
+
label sliding into the icon. The entering direction is declared on the
|
|
3167
|
+
confirming rule below, which is the state the label lands in on the way in —
|
|
3168
|
+
CSS uses the transition of the destination state, not the origin. */
|
|
2935
3169
|
.dsq_deleteConfirmLabel {
|
|
2936
3170
|
max-width: 0;
|
|
2937
3171
|
margin-left: 0;
|
|
2938
3172
|
overflow: hidden;
|
|
2939
3173
|
opacity: 0;
|
|
2940
3174
|
white-space: nowrap;
|
|
2941
|
-
transition: max-width 0.
|
|
3175
|
+
transition: opacity 0.05s ease, max-width 0.12s ease 0.05s, margin-left 0.12s ease 0.05s;
|
|
2942
3176
|
}
|
|
2943
3177
|
|
|
2944
3178
|
/* One grid item, so the icon-only state stays centered in the 26/20px button
|
|
@@ -2948,10 +3182,12 @@ const COMPONENTS_CSS = [
|
|
|
2948
3182
|
align-items: center;
|
|
2949
3183
|
}
|
|
2950
3184
|
|
|
3185
|
+
/* Enter transition: the box opens first, the text fades in once it has room. */
|
|
2951
3186
|
.dsq_deleteConfirm[data-confirming='true'] .dsq_deleteConfirmLabel {
|
|
2952
3187
|
max-width: 140px;
|
|
2953
3188
|
margin-left: 4px;
|
|
2954
3189
|
opacity: 1;
|
|
3190
|
+
transition: max-width 0.14s ease, margin-left 0.14s ease, opacity 0.1s ease 0.06s;
|
|
2955
3191
|
}
|
|
2956
3192
|
|
|
2957
3193
|
/* Touch selection chrome. Fixed to the visual viewport and mounted on body:
|
|
@@ -3169,66 +3405,59 @@ const COMPONENTS_CSS = [
|
|
|
3169
3405
|
transition: background-color 0.12s;
|
|
3170
3406
|
}
|
|
3171
3407
|
|
|
3172
|
-
/*
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
the
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
.dsq_annMark[data-dsq-ann-index]::after {
|
|
3182
|
-
content: attr(data-dsq-ann-index);
|
|
3408
|
+
/* Badge painted at the END of a mark: the ordinal, the handoff arrow, or the
|
|
3409
|
+
bot glyph for a side question.
|
|
3410
|
+
A real element, not a ::after. A mark that wraps is one inline box spanning
|
|
3411
|
+
several lines, and percentage offsets resolve against a width unrelated to
|
|
3412
|
+
where the text actually ends (measured: a 678px two-line mark resolved
|
|
3413
|
+
+left: 100%+ to 0, a 91px one-line mark to 90.88px). annotate-text.ts
|
|
3414
|
+
therefore measures the last line fragment and writes left/top in pixels;
|
|
3415
|
+
collision offsets become a transform on this element alone. */
|
|
3416
|
+
.dsq_annBadge {
|
|
3183
3417
|
position: absolute;
|
|
3184
|
-
bottom: 8px;
|
|
3185
|
-
left: 100%;
|
|
3186
|
-
/* Above the native selection highlight and the background of a later mark:
|
|
3187
|
-
both otherwise paint over the badge when their ranges meet. */
|
|
3188
|
-
z-index: 2;
|
|
3189
|
-
/* JS shifts colliding badges right/up; the resting position is unchanged. */
|
|
3190
|
-
transform: translate(var(--dsq-ann-badge-x, 0px), var(--dsq-ann-badge-y, 0px));
|
|
3191
3418
|
display: grid;
|
|
3192
3419
|
place-items: center;
|
|
3193
3420
|
min-width: 15px;
|
|
3194
3421
|
height: 15px;
|
|
3195
|
-
margin-left: 2px;
|
|
3196
3422
|
padding: 0 3px;
|
|
3197
3423
|
border-radius: 999px;
|
|
3198
3424
|
background: var(--dsq-mark-color);
|
|
3199
3425
|
color: #fff;
|
|
3200
3426
|
font-size: 10px;
|
|
3427
|
+
font-style: normal;
|
|
3201
3428
|
font-weight: 600;
|
|
3202
3429
|
line-height: 1;
|
|
3203
|
-
/*
|
|
3204
|
-
|
|
3205
|
-
|
|
3430
|
+
/* Above the native selection highlight and the background of a later mark:
|
|
3431
|
+
both otherwise paint over the badge when their ranges meet. */
|
|
3432
|
+
z-index: 2;
|
|
3433
|
+
/* The mark stays the click target; the badge is decoration. */
|
|
3434
|
+
pointer-events: none;
|
|
3435
|
+
/* Never part of a text selection. Otherwise Ctrl+A or a drag across the
|
|
3436
|
+
sentence includes the glyph, and the native highlight paints a coloured
|
|
3437
|
+
box behind a badge that is meant to look like a superscript label. */
|
|
3438
|
+
user-select: none;
|
|
3439
|
+
-webkit-user-select: none;
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
/* Side-question mark: the lucide bot glyph instead of the ordinal, drawn in
|
|
3443
|
+
white to match the badge ink on the accent fill. The glyph is a data URI
|
|
3444
|
+
because the badge is a single element with no room for an inline svg. */
|
|
3445
|
+
.dsq_annBadge[data-dsq-ann-robot] {
|
|
3446
|
+
/* Same padding as an ordinal badge, so the two read as one control; the
|
|
3447
|
+
glyph shrinks to the content box (15px min-width - 6px padding). */
|
|
3448
|
+
padding: 0 3px;
|
|
3449
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 8V4H8'/%3E%3Crect width='16' height='12' x='4' y='8' rx='2'/%3E%3Cpath d='M2 14h2'/%3E%3Cpath d='M20 14h2'/%3E%3Cpath d='M15 13v2'/%3E%3Cpath d='M9 13v2'/%3E%3C/svg%3E");
|
|
3450
|
+
background-repeat: no-repeat;
|
|
3451
|
+
background-position: center;
|
|
3452
|
+
background-size: 9px 9px;
|
|
3206
3453
|
}
|
|
3207
3454
|
|
|
3208
3455
|
/* Arrow badge for a selection sent to another conversation. One glyph, so the
|
|
3209
3456
|
width is fixed instead of digit-based; the box matches the ordinal badge so
|
|
3210
3457
|
collision offsets stay interchangeable. */
|
|
3211
|
-
.
|
|
3212
|
-
content: "↗";
|
|
3213
|
-
position: absolute;
|
|
3214
|
-
bottom: 8px;
|
|
3215
|
-
left: 100%;
|
|
3216
|
-
z-index: 2;
|
|
3217
|
-
transform: translate(var(--dsq-ann-badge-x, 0px), var(--dsq-ann-badge-y, 0px));
|
|
3218
|
-
display: grid;
|
|
3219
|
-
place-items: center;
|
|
3458
|
+
.dsq_annBadge[data-dsq-ann-arrow] {
|
|
3220
3459
|
width: 15px;
|
|
3221
|
-
height: 15px;
|
|
3222
|
-
margin-left: 2px;
|
|
3223
3460
|
padding: 0;
|
|
3224
|
-
border-radius: 999px;
|
|
3225
|
-
background: var(--dsq-mark-color);
|
|
3226
|
-
color: #fff;
|
|
3227
|
-
font-size: 10px;
|
|
3228
|
-
font-weight: 600;
|
|
3229
|
-
line-height: 1;
|
|
3230
|
-
pointer-events: auto;
|
|
3231
|
-
cursor: pointer;
|
|
3232
3461
|
}
|
|
3233
3462
|
|
|
3234
3463
|
.dsq_annMark:hover {
|
|
@@ -3951,7 +4180,14 @@ body {
|
|
|
3951
4180
|
--dsq-font-size: var(--dsh-content-font-size, 14px);
|
|
3952
4181
|
--dsq-font-delta: var(--dsh-content-font-delta, 0px);
|
|
3953
4182
|
--dsq-bg-raised: var(--dsw-alias-bg-layer-1, #fff);
|
|
3954
|
-
|
|
4183
|
+
/* Toast pill. The product token that used to back this (bg-layer-3) is plain
|
|
4184
|
+
white in dsh 0.2.2 while the label inverted token is white too — a white
|
|
4185
|
+
pill with white text. Pair two tokens that are guaranteed to be each
|
|
4186
|
+
other's inverse instead: primary label as the surface, raised surface as
|
|
4187
|
+
the ink. Light theme gives dark-on-light, dark theme flips with it. */
|
|
4188
|
+
--dsq-toast-bg: var(--dsq-bg-raised);
|
|
4189
|
+
--dsq-toast-fg: var(--dsq-label);
|
|
4190
|
+
--dsq-toast-line: var(--dsq-line);
|
|
3955
4191
|
/* The settings switch mirrors the Host's own switch: neutral track when off,
|
|
3956
4192
|
brand fill when on, and a thumb that inverts with the theme. */
|
|
3957
4193
|
--dsq-switch-off: var(--dsw-alias-border-l3, rgba(0, 0, 0, 0.12));
|
|
@@ -4033,6 +4269,7 @@ const styles = {
|
|
|
4033
4269
|
kbd: "dsq_kbd",
|
|
4034
4270
|
divider: "dsq_divider",
|
|
4035
4271
|
status: "dsq_status",
|
|
4272
|
+
statusIcon: "dsq_statusIcon",
|
|
4036
4273
|
ctxMenu: "dsq_ctxMenu",
|
|
4037
4274
|
ctxItem: "dsq_ctxItem",
|
|
4038
4275
|
modelSeat: "dsq_modelSeat",
|
|
@@ -4100,6 +4337,7 @@ const styles = {
|
|
|
4100
4337
|
annDirective: "dsq_annDirective",
|
|
4101
4338
|
annAnchor: "dsq_annAnchor",
|
|
4102
4339
|
annMark: "dsq_annMark",
|
|
4340
|
+
annBadge: "dsq_annBadge",
|
|
4103
4341
|
annMarkFlash: "dsq_annMarkFlash",
|
|
4104
4342
|
annMessageFlash: "dsq_annMessageFlash",
|
|
4105
4343
|
handoffRow: "dsq_handoffRow",
|
|
@@ -4135,6 +4373,7 @@ const styles = {
|
|
|
4135
4373
|
translateHead: "dsq_translateHead",
|
|
4136
4374
|
translateTargetLabel: "dsq_translateTargetLabel",
|
|
4137
4375
|
translateClose: "dsq_translateClose",
|
|
4376
|
+
translateCopy: "dsq_translateCopy",
|
|
4138
4377
|
translateBody: "dsq_translateBody",
|
|
4139
4378
|
translateText: "dsq_translateText",
|
|
4140
4379
|
translateHint: "dsq_translateHint",
|
|
@@ -4506,6 +4745,7 @@ function cleanTextOf(item) {
|
|
|
4506
4745
|
*/
|
|
4507
4746
|
const MARK_ATTR = "data-dsq-ann-mark";
|
|
4508
4747
|
const INDEX_ATTR = "data-dsq-ann-index";
|
|
4748
|
+
const ROBOT_ATTR = "data-dsq-ann-robot";
|
|
4509
4749
|
const ARROW_ATTR = "data-dsq-ann-arrow";
|
|
4510
4750
|
const FLASH_MS = 1500;
|
|
4511
4751
|
const jobs = /* @__PURE__ */ new Map();
|
|
@@ -4597,6 +4837,18 @@ function sameText(a, b) {
|
|
|
4597
4837
|
return norm(a) === norm(b);
|
|
4598
4838
|
}
|
|
4599
4839
|
/**
|
|
4840
|
+
* The mark's own prose, without the badge glyph.
|
|
4841
|
+
*
|
|
4842
|
+
* The badge is a child element carrying the ordinal or the arrow, so plain
|
|
4843
|
+
* `textContent` returns "…text1" and never equals the annotation text.
|
|
4844
|
+
*/
|
|
4845
|
+
function markTextOf(mark) {
|
|
4846
|
+
let text = "";
|
|
4847
|
+
for (const node of mark.childNodes) if (node instanceof Text) text += node.data;
|
|
4848
|
+
else if (node instanceof Element && !node.classList.contains(styles.annBadge)) text += node.textContent ?? "";
|
|
4849
|
+
return text;
|
|
4850
|
+
}
|
|
4851
|
+
/**
|
|
4600
4852
|
* Already decorated by THIS job. A different job may wrap the same span (a
|
|
4601
4853
|
* local pending annotation and the outbound arrow of the same selection are
|
|
4602
4854
|
* separate marks), so the check must be by mark id, not by any mark.
|
|
@@ -4623,6 +4875,24 @@ function makeArrowInteractive(mark, targetLabel) {
|
|
|
4623
4875
|
const label = t("handoff.markAria", { target: targetLabel ?? "" });
|
|
4624
4876
|
if (mark.getAttribute("aria-label") !== label) mark.setAttribute("aria-label", label);
|
|
4625
4877
|
}
|
|
4878
|
+
/**
|
|
4879
|
+
* Attach the badge element to the mark's last fragment.
|
|
4880
|
+
*
|
|
4881
|
+
* Called **after** the mark is in the document: `Range.surroundContents()`
|
|
4882
|
+
* clears the new parent's children before moving the range into it, so a badge
|
|
4883
|
+
* appended beforehand is silently dropped.
|
|
4884
|
+
*/
|
|
4885
|
+
function attachBadge(mark, job) {
|
|
4886
|
+
const badge = document.createElement("i");
|
|
4887
|
+
badge.className = styles.annBadge;
|
|
4888
|
+
badge.setAttribute("aria-hidden", "true");
|
|
4889
|
+
if (job.variant === "arrow") {
|
|
4890
|
+
badge.setAttribute(ARROW_ATTR, "true");
|
|
4891
|
+
badge.textContent = "↗";
|
|
4892
|
+
} else if (job.variant === "robot") badge.setAttribute(ROBOT_ATTR, "true");
|
|
4893
|
+
else badge.textContent = String(job.index);
|
|
4894
|
+
mark.append(badge);
|
|
4895
|
+
}
|
|
4626
4896
|
function createMark(job, withIndex) {
|
|
4627
4897
|
const mark = document.createElement("span");
|
|
4628
4898
|
mark.className = styles.annMark;
|
|
@@ -4664,8 +4934,10 @@ function wrapRange(item, job, from, to) {
|
|
|
4664
4934
|
const range = document.createRange();
|
|
4665
4935
|
range.setStart(node, piece.start - piece.entry.start);
|
|
4666
4936
|
range.setEnd(node, piece.end - piece.entry.start);
|
|
4667
|
-
const
|
|
4937
|
+
const last = i === pieces.length - 1;
|
|
4938
|
+
const mark = createMark(job, last);
|
|
4668
4939
|
range.surroundContents(mark);
|
|
4940
|
+
if (last) attachBadge(mark, job);
|
|
4669
4941
|
wrapped = true;
|
|
4670
4942
|
});
|
|
4671
4943
|
return wrapped;
|
|
@@ -4742,11 +5014,54 @@ function refreshAnnotationMarkLabels() {
|
|
|
4742
5014
|
* never participates in text layout. Collisions are resolved inside each
|
|
4743
5015
|
* message block, and a stack may not climb into the message above.
|
|
4744
5016
|
*/
|
|
5017
|
+
/** The badge element of one mark, when it has one. */
|
|
5018
|
+
function badgeOf(mark) {
|
|
5019
|
+
return mark.querySelector(`.${styles.annBadge}`);
|
|
5020
|
+
}
|
|
5021
|
+
/**
|
|
5022
|
+
* Position one badge at the end of its mark's **last line fragment**.
|
|
5023
|
+
*
|
|
5024
|
+
* `left: 100%` cannot do this: a mark that wraps is one inline box spanning
|
|
5025
|
+
* several lines, and the percentage resolves against a width that has nothing
|
|
5026
|
+
* to do with where the text actually ends (measured: a 678px-wide two-line mark
|
|
5027
|
+
* resolved to `left: 0`, another 91px-wide one to `left: 90.88px`). So the
|
|
5028
|
+
* badge is a real element placed in pixels: measure the last client rect of a
|
|
5029
|
+
* range over the mark's text, then convert it into the mark's own coordinate
|
|
5030
|
+
* space.
|
|
5031
|
+
*/
|
|
5032
|
+
function placeBadge(mark, badge) {
|
|
5033
|
+
const text = mark.firstChild;
|
|
5034
|
+
if (!(text instanceof Text) || text.length === 0) return null;
|
|
5035
|
+
const range = document.createRange();
|
|
5036
|
+
range.setStart(text, 0);
|
|
5037
|
+
range.setEnd(text, text.length);
|
|
5038
|
+
const rects = Array.from(range.getClientRects()).filter((rect) => rect.width > 0 || rect.height > 0);
|
|
5039
|
+
const last = rects[rects.length - 1];
|
|
5040
|
+
if (last === void 0) return null;
|
|
5041
|
+
badge.style.left = "0px";
|
|
5042
|
+
badge.style.top = "0px";
|
|
5043
|
+
badge.style.transform = "none";
|
|
5044
|
+
const origin = badge.getBoundingClientRect();
|
|
5045
|
+
if (origin.width === 0 && origin.height === 0) return null;
|
|
5046
|
+
const height = origin.height || 15;
|
|
5047
|
+
const dx = last.right + 2 - origin.left;
|
|
5048
|
+
const dy = last.bottom - height - 8 - origin.top;
|
|
5049
|
+
badge.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
5050
|
+
return {
|
|
5051
|
+
dx,
|
|
5052
|
+
dy
|
|
5053
|
+
};
|
|
5054
|
+
}
|
|
4745
5055
|
function layoutBadges(scope) {
|
|
4746
5056
|
const marks = Array.from(scope.querySelectorAll(`[${INDEX_ATTR}], [${ARROW_ATTR}]`));
|
|
5057
|
+
const badges = /* @__PURE__ */ new Map();
|
|
5058
|
+
const bases = /* @__PURE__ */ new WeakMap();
|
|
4747
5059
|
for (const mark of marks) {
|
|
4748
|
-
mark
|
|
4749
|
-
|
|
5060
|
+
const badge = badgeOf(mark);
|
|
5061
|
+
if (badge === null) continue;
|
|
5062
|
+
badges.set(mark, badge);
|
|
5063
|
+
const base = placeBadge(mark, badge);
|
|
5064
|
+
if (base !== null) bases.set(badge, base);
|
|
4750
5065
|
}
|
|
4751
5066
|
if (marks.length < 2) return;
|
|
4752
5067
|
const scroller = marks[0]?.closest("[data-conversation-scroll]");
|
|
@@ -4765,18 +5080,19 @@ function layoutBadges(scope) {
|
|
|
4765
5080
|
const ownerRect = owner?.getBoundingClientRect();
|
|
4766
5081
|
const topEdge = Math.max(scrollerTop, ownerRect && ownerRect.height > 0 ? ownerRect.top : scrollerTop) + 4;
|
|
4767
5082
|
const anchors = group.map((mark) => {
|
|
4768
|
-
const
|
|
4769
|
-
|
|
5083
|
+
const badge = badges.get(mark);
|
|
5084
|
+
if (badge === void 0) return null;
|
|
5085
|
+
const rect = badge.getBoundingClientRect();
|
|
4770
5086
|
return {
|
|
4771
|
-
|
|
5087
|
+
badge,
|
|
4772
5088
|
index: Number(mark.getAttribute(INDEX_ATTR) ?? 0),
|
|
4773
|
-
x: rect.
|
|
4774
|
-
y: rect.
|
|
4775
|
-
width:
|
|
4776
|
-
height: 15,
|
|
5089
|
+
x: rect.left,
|
|
5090
|
+
y: rect.top,
|
|
5091
|
+
width: rect.width || 24,
|
|
5092
|
+
height: rect.height || 15,
|
|
4777
5093
|
visible: rect.width > 0 && rect.height > 0
|
|
4778
5094
|
};
|
|
4779
|
-
}).filter((anchor) => anchor.visible).sort((a, b) => a.index - b.index);
|
|
5095
|
+
}).filter((anchor) => anchor !== null && anchor.visible).sort((a, b) => a.index - b.index);
|
|
4780
5096
|
if (anchors.length < 2) continue;
|
|
4781
5097
|
const offsets = placeBadges(anchors, {
|
|
4782
5098
|
right: rightEdge - 4,
|
|
@@ -4784,11 +5100,16 @@ function layoutBadges(scope) {
|
|
|
4784
5100
|
});
|
|
4785
5101
|
anchors.forEach((anchor, i) => {
|
|
4786
5102
|
const { dx, dy } = offsets[i];
|
|
4787
|
-
|
|
4788
|
-
if (
|
|
5103
|
+
const base = bases.get(anchor.badge);
|
|
5104
|
+
if (base === void 0) return;
|
|
5105
|
+
anchor.badge.style.transform = `translate(${base.dx + dx}px, ${base.dy + dy}px)`;
|
|
4789
5106
|
});
|
|
4790
5107
|
}
|
|
4791
5108
|
}
|
|
5109
|
+
/** True when this message row is rendered inside the right sidebar's session pane. */
|
|
5110
|
+
function insideSidePane(node) {
|
|
5111
|
+
return node.closest("[data-sidebar-right-session]") !== null;
|
|
5112
|
+
}
|
|
4792
5113
|
/**
|
|
4793
5114
|
* Apply registered jobs. With `only`, touch just those rendered messages — the
|
|
4794
5115
|
* mutation-driven path — instead of resolving and scanning every message for
|
|
@@ -4806,6 +5127,7 @@ function applyAnnotationMarks(root, only) {
|
|
|
4806
5127
|
const items = only ? job.messageId === "" ? onlyItems : onlyByKey?.get(job.messageId) ?? [] : job.messageId === "" ? lookup?.all ?? [] : [lookup?.byKey.get(job.messageId)].filter((item) => item !== void 0);
|
|
4807
5128
|
let decorated = false;
|
|
4808
5129
|
for (const item of items) {
|
|
5130
|
+
if (job.variant !== "robot" && insideSidePane(item)) continue;
|
|
4809
5131
|
if (refreshMarkIndex(item, job)) {
|
|
4810
5132
|
decorated = true;
|
|
4811
5133
|
continue;
|
|
@@ -4817,10 +5139,16 @@ function applyAnnotationMarks(root, only) {
|
|
|
4817
5139
|
}
|
|
4818
5140
|
if (!only) {
|
|
4819
5141
|
layoutBadges(scope);
|
|
5142
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => layoutBadges(scope));
|
|
4820
5143
|
return;
|
|
4821
5144
|
}
|
|
4822
5145
|
for (const item of only) if (item.isConnected) layoutBadges(item);
|
|
4823
5146
|
}
|
|
5147
|
+
/** Re-place every badge; call after any layout change that moves text. */
|
|
5148
|
+
function relayoutAnnotationMarks(root) {
|
|
5149
|
+
if (typeof document === "undefined") return;
|
|
5150
|
+
layoutBadges(root ?? document.body);
|
|
5151
|
+
}
|
|
4824
5152
|
/** Insert or refresh jobs; identical jobs are no-ops. */
|
|
4825
5153
|
function upsertAnnotationJobs(next) {
|
|
4826
5154
|
let changed = false;
|
|
@@ -4959,7 +5287,7 @@ function findAnnotationMark(index, text, markId) {
|
|
|
4959
5287
|
const marks = [...document.querySelectorAll(`[${INDEX_ATTR}="${index}"]`)].filter((node) => node instanceof HTMLElement);
|
|
4960
5288
|
if (marks.length === 0) return null;
|
|
4961
5289
|
if (text) {
|
|
4962
|
-
const hit = marks.find((mark) => sameText(mark
|
|
5290
|
+
const hit = marks.find((mark) => sameText(markTextOf(mark), text));
|
|
4963
5291
|
if (hit) return hit;
|
|
4964
5292
|
}
|
|
4965
5293
|
return marks[0];
|
|
@@ -5175,86 +5503,10 @@ function openSourceAndLocate(input) {
|
|
|
5175
5503
|
};
|
|
5176
5504
|
window.setTimeout(tick, 0);
|
|
5177
5505
|
}
|
|
5178
|
-
const DRAFT_MARKER_RE = /\u200B/g;
|
|
5179
|
-
/** Plain text from Chat user-node content blocks (string or block array). */
|
|
5180
|
-
function contentBlocksToText(content) {
|
|
5181
|
-
if (typeof content === "string") return content;
|
|
5182
|
-
if (!Array.isArray(content)) return "";
|
|
5183
|
-
return content.map((block) => {
|
|
5184
|
-
if (block && typeof block === "object" && "text" in block) {
|
|
5185
|
-
const text = block.text;
|
|
5186
|
-
return typeof text === "string" ? text : "";
|
|
5187
|
-
}
|
|
5188
|
-
return "";
|
|
5189
|
-
}).join("");
|
|
5190
|
-
}
|
|
5191
|
-
/** Short one-line card title from selected body text. */
|
|
5192
|
-
function previewTitle$1(text, max = 48) {
|
|
5193
|
-
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
5194
|
-
return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;
|
|
5195
|
-
}
|
|
5196
|
-
/** Ensure the draft carries the invisible marker, without disturbing its text. */
|
|
5197
|
-
function withDraftMarker(draft) {
|
|
5198
|
-
return draft.includes("") ? draft : `${draft}`;
|
|
5199
|
-
}
|
|
5200
5506
|
/** Remove every invisible marker from a draft. */
|
|
5201
5507
|
function stripDraftMarker(draft) {
|
|
5202
5508
|
return draft.split("").join("");
|
|
5203
5509
|
}
|
|
5204
|
-
function toQuote(body) {
|
|
5205
|
-
const text = body.join("\n").trim();
|
|
5206
|
-
if (!text) return null;
|
|
5207
|
-
return {
|
|
5208
|
-
title: previewTitle$1(text),
|
|
5209
|
-
body: text
|
|
5210
|
-
};
|
|
5211
|
-
}
|
|
5212
|
-
/**
|
|
5213
|
-
* Single-pass reader for plugin quote blocks. Yields both the parsed quotes
|
|
5214
|
-
* and the leftover display text (question / non-quote lines).
|
|
5215
|
-
*/
|
|
5216
|
-
function scanQuoteMessage(text) {
|
|
5217
|
-
const quotes = [];
|
|
5218
|
-
const restLines = [];
|
|
5219
|
-
let body = null;
|
|
5220
|
-
const close = () => {
|
|
5221
|
-
if (body === null) return;
|
|
5222
|
-
const quote = toQuote(body);
|
|
5223
|
-
if (quote) quotes.push(quote);
|
|
5224
|
-
body = null;
|
|
5225
|
-
};
|
|
5226
|
-
for (const line of text.split("\n")) {
|
|
5227
|
-
const trimmed = line.trimEnd();
|
|
5228
|
-
const isMarker = trimmed === "> [选中文本]" || trimmed.startsWith("> [选中文本]");
|
|
5229
|
-
if (body === null) {
|
|
5230
|
-
if (isMarker) body = [];
|
|
5231
|
-
else restLines.push(line);
|
|
5232
|
-
continue;
|
|
5233
|
-
}
|
|
5234
|
-
if (trimmed.startsWith(">")) {
|
|
5235
|
-
body.push(trimmed.replace(/^>\s?/, ""));
|
|
5236
|
-
continue;
|
|
5237
|
-
}
|
|
5238
|
-
close();
|
|
5239
|
-
if (isMarker) body = [];
|
|
5240
|
-
else restLines.push(line);
|
|
5241
|
-
}
|
|
5242
|
-
close();
|
|
5243
|
-
return {
|
|
5244
|
-
quotes,
|
|
5245
|
-
rest: restLines.join("\n").replace(/^\n+/, "").trim()
|
|
5246
|
-
};
|
|
5247
|
-
}
|
|
5248
|
-
/**
|
|
5249
|
-
* Display text for a user bubble: drop every plugin quote block so the
|
|
5250
|
-
* transcript shows only the question. The durable message (and model payload)
|
|
5251
|
-
* still contains the full `> [选中文本]` text.
|
|
5252
|
-
*/
|
|
5253
|
-
function displayTextWithoutQuote(text) {
|
|
5254
|
-
const clean = text.replace(DRAFT_MARKER_RE, "");
|
|
5255
|
-
if (!clean.includes("> [选中文本]")) return clean;
|
|
5256
|
-
return scanQuoteMessage(clean).rest;
|
|
5257
|
-
}
|
|
5258
5510
|
//#endregion
|
|
5259
5511
|
//#region src/client/protocol/annotation-protocol.ts
|
|
5260
5512
|
/**
|
|
@@ -6096,7 +6348,9 @@ const SHAPES = {
|
|
|
6096
6348
|
"chevron-down": "<path d=\"m6 9 6 6 6-6\"/>",
|
|
6097
6349
|
"chevron-right": "<path d=\"m9 18 6-6-6-6\"/>",
|
|
6098
6350
|
check: "<path d=\"M20 6 9 17l-5-5\"/>",
|
|
6099
|
-
"arrow-up-right": "<path d=\"M7 7h10v10\"/><path d=\"M7 17 17 7\"/>"
|
|
6351
|
+
"arrow-up-right": "<path d=\"M7 7h10v10\"/><path d=\"M7 17 17 7\"/>",
|
|
6352
|
+
"circle-alert": "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4\"/><path d=\"M12 16h.01\"/>",
|
|
6353
|
+
bot: "<path d=\"M12 8V4H8\"/><rect width=\"16\" height=\"12\" x=\"4\" y=\"8\" rx=\"2\"/><path d=\"M2 14h2\"/><path d=\"M20 14h2\"/><path d=\"M15 13v2\"/><path d=\"M9 13v2\"/>"
|
|
6100
6354
|
};
|
|
6101
6355
|
/** Markup form for plain-DOM call sites; `insertAdjacentHTML('afterbegin', …)`. */
|
|
6102
6356
|
function iconMarkup(name, size = 15) {
|
|
@@ -6759,28 +7013,37 @@ function AnnotationSummary({ count, items, onRemoveAll, onItemSelect, onItemRemo
|
|
|
6759
7013
|
//#endregion
|
|
6760
7014
|
//#region src/client/composer/useDraftMarker.ts
|
|
6761
7015
|
/**
|
|
6762
|
-
*
|
|
6763
|
-
* pending: an invisible marker makes an otherwise empty draft non-empty.
|
|
7016
|
+
* True when the draft is nothing but the legacy invisible send marker.
|
|
6764
7017
|
*
|
|
6765
|
-
*
|
|
6766
|
-
*
|
|
7018
|
+
* The plugin used to append U+200B so an annotation-only draft looked non-empty
|
|
7019
|
+
* and the Host's send button stayed enabled. That is gone: it made sending a
|
|
7020
|
+
* wordless message possible, and a Host-rendered bubble for such a message is
|
|
7021
|
+
* an empty box. Older persisted drafts may still carry the character, so the
|
|
7022
|
+
* composer distinguishes "only the marker" (empty the draft) from "marker next
|
|
7023
|
+
* to real text" (strip the marker, keep the text).
|
|
6767
7024
|
*/
|
|
6768
|
-
function
|
|
6769
|
-
|
|
6770
|
-
if (itemCount === 0) return;
|
|
6771
|
-
const current = draftRef.current;
|
|
6772
|
-
if (current.includes("")) return;
|
|
6773
|
-
if (current.trim() !== "") return;
|
|
6774
|
-
setDraft(withDraftMarker(current));
|
|
6775
|
-
}, [
|
|
6776
|
-
itemCount,
|
|
6777
|
-
draftRef,
|
|
6778
|
-
setDraft
|
|
6779
|
-
]);
|
|
7025
|
+
function isMarkerOnly(draft) {
|
|
7026
|
+
return draft.includes("") && stripDraftMarker(draft).trim() === "";
|
|
6780
7027
|
}
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
7028
|
+
//#endregion
|
|
7029
|
+
//#region src/client/composer/draft-write.ts
|
|
7030
|
+
/**
|
|
7031
|
+
* Replace the draft text without clearing atomic nodes when the host supports
|
|
7032
|
+
* an append-only edit.
|
|
7033
|
+
*
|
|
7034
|
+
* `current` is the draft the caller read before computing `next`; on a Lexical
|
|
7035
|
+
* host a revision mismatch simply returns false and the caller falls back.
|
|
7036
|
+
*/
|
|
7037
|
+
function writeDraft(actions, next, current) {
|
|
7038
|
+
if (next === current) return true;
|
|
7039
|
+
const capture = actions.captureInsertion;
|
|
7040
|
+
const insert = actions.insertText;
|
|
7041
|
+
if (typeof capture === "function" && typeof insert === "function") {
|
|
7042
|
+
const span = capture();
|
|
7043
|
+
if (span.start === 0 && span.end === current.length && typeof span.draftRev === "number" && insert.call(actions, next, span)) return true;
|
|
7044
|
+
}
|
|
7045
|
+
actions.setDraft(next);
|
|
7046
|
+
return false;
|
|
6784
7047
|
}
|
|
6785
7048
|
//#endregion
|
|
6786
7049
|
//#region src/client/composer/useSendIntercept.ts
|
|
@@ -6789,15 +7052,16 @@ function clearDraftMarker(draft, setDraft) {
|
|
|
6789
7052
|
* composer, or the primary send button) so the composer's submit path carries
|
|
6790
7053
|
* the JSON protocol block.
|
|
6791
7054
|
*/
|
|
6792
|
-
function useSendIntercept({ itemCount, draftRef,
|
|
7055
|
+
function useSendIntercept({ itemCount, draftRef, actionsRef, sessionId, onSelfWrite, onFolded, foldingRef }) {
|
|
6793
7056
|
(0, react.useEffect)(() => {
|
|
6794
7057
|
if (itemCount === 0) return;
|
|
6795
7058
|
const inject = () => {
|
|
6796
7059
|
const current = getAnnotations();
|
|
6797
7060
|
if (current.length === 0) return;
|
|
6798
7061
|
if (foldingRef !== void 0) foldingRef.current = true;
|
|
6799
|
-
|
|
6800
|
-
|
|
7062
|
+
onSelfWrite();
|
|
7063
|
+
writeDraft(actionsRef.current, composeAnnotatedMessage(draftRef.current, current), draftRef.current);
|
|
7064
|
+
clearAnnotations(sessionId, "sent");
|
|
6801
7065
|
onFolded();
|
|
6802
7066
|
};
|
|
6803
7067
|
const onKeyDown = (event) => {
|
|
@@ -6829,7 +7093,9 @@ function useSendIntercept({ itemCount, draftRef, setDraft, onFolded, foldingRef
|
|
|
6829
7093
|
}, [
|
|
6830
7094
|
itemCount,
|
|
6831
7095
|
draftRef,
|
|
6832
|
-
|
|
7096
|
+
actionsRef,
|
|
7097
|
+
sessionId,
|
|
7098
|
+
onSelfWrite,
|
|
6833
7099
|
onFolded,
|
|
6834
7100
|
foldingRef
|
|
6835
7101
|
]);
|
|
@@ -6882,10 +7148,24 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
6882
7148
|
actionsRef.current = inputActions;
|
|
6883
7149
|
sessionRef.current = sessionId;
|
|
6884
7150
|
const setDraft = (0, react.useCallback)((text) => {
|
|
6885
|
-
|
|
7151
|
+
selfWrites.current += 1;
|
|
7152
|
+
writeDraft(actionsRef.current, text, draftRef.current);
|
|
6886
7153
|
}, []);
|
|
7154
|
+
/**
|
|
7155
|
+
* Bumped whenever this component rewrites the draft itself.
|
|
7156
|
+
*
|
|
7157
|
+
* The rewind path below treats *any* draft carrying a complete protocol
|
|
7158
|
+
* block as a withdrawn message and lifts its annotations back into the
|
|
7159
|
+
* pending list. The host publishes every programmatic write as a normal
|
|
7160
|
+
* draft change, so without this counter the plugin's own write would be read
|
|
7161
|
+
* back as a rewind. The restore records the stamp it ran at and bails when
|
|
7162
|
+
* the draft it is looking at was produced by a later self-write.
|
|
7163
|
+
*/
|
|
7164
|
+
const selfWrites = (0, react.useRef)(0);
|
|
7165
|
+
/** Stamp of the newest self-write the rewind path has already accounted for. */
|
|
7166
|
+
const seenSelfWrite = (0, react.useRef)(0);
|
|
6887
7167
|
const sync = (0, react.useCallback)(() => {
|
|
6888
|
-
setItems(getAnnotations());
|
|
7168
|
+
setItems(getAnnotations(sessionId));
|
|
6889
7169
|
}, []);
|
|
6890
7170
|
(0, react.useEffect)(() => {
|
|
6891
7171
|
bindAnnotationSession(sessionId);
|
|
@@ -6913,15 +7193,14 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
6913
7193
|
card.style.removeProperty("--dsq-quote-pad");
|
|
6914
7194
|
};
|
|
6915
7195
|
}, [items]);
|
|
6916
|
-
useDraftMarker({
|
|
6917
|
-
itemCount: items.length,
|
|
6918
|
-
draftRef,
|
|
6919
|
-
setDraft
|
|
6920
|
-
});
|
|
6921
7196
|
useSendIntercept({
|
|
6922
7197
|
itemCount: items.length,
|
|
6923
7198
|
draftRef,
|
|
6924
|
-
|
|
7199
|
+
actionsRef,
|
|
7200
|
+
sessionId,
|
|
7201
|
+
onSelfWrite: () => {
|
|
7202
|
+
selfWrites.current += 1;
|
|
7203
|
+
},
|
|
6925
7204
|
onFolded: () => setItems(EMPTY),
|
|
6926
7205
|
foldingRef
|
|
6927
7206
|
});
|
|
@@ -6931,9 +7210,11 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
6931
7210
|
return;
|
|
6932
7211
|
}
|
|
6933
7212
|
if (foldingRef.current || composing.current) return;
|
|
7213
|
+
if (seenSelfWrite.current === selfWrites.current) return;
|
|
6934
7214
|
const restored = readComposedDraft(draft);
|
|
6935
7215
|
if (restored === null) return;
|
|
6936
7216
|
foldingRef.current = true;
|
|
7217
|
+
seenSelfWrite.current = selfWrites.current;
|
|
6937
7218
|
restoreAnnotations(sessionId, restored.annotations);
|
|
6938
7219
|
setDraft(restored.rest);
|
|
6939
7220
|
}, [
|
|
@@ -6944,41 +7225,35 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
6944
7225
|
]);
|
|
6945
7226
|
(0, react.useEffect)(() => {
|
|
6946
7227
|
if (composing.current) return;
|
|
6947
|
-
if (
|
|
6948
|
-
|
|
6949
|
-
return;
|
|
6950
|
-
}
|
|
6951
|
-
if (!draft.includes("") && draft.trim() === "") setDraft(withDraftMarker(draft));
|
|
7228
|
+
if (!draft.includes("")) return;
|
|
7229
|
+
setDraft(isMarkerOnly(draft) ? "" : stripDraftMarker(draft));
|
|
6952
7230
|
}, [
|
|
6953
|
-
items,
|
|
6954
7231
|
draft,
|
|
6955
7232
|
setDraft,
|
|
6956
7233
|
composing
|
|
6957
7234
|
]);
|
|
6958
7235
|
const onRemoveAll = (0, react.useCallback)(() => {
|
|
6959
|
-
clearAnnotations(
|
|
7236
|
+
clearAnnotations(sessionId, "discarded");
|
|
6960
7237
|
setItems(EMPTY);
|
|
6961
|
-
|
|
6962
|
-
}, [setDraft]);
|
|
7238
|
+
}, []);
|
|
6963
7239
|
const dropIfEmpty = (0, react.useCallback)(() => {
|
|
6964
|
-
setItems(getAnnotations());
|
|
6965
|
-
|
|
6966
|
-
}, [setDraft]);
|
|
7240
|
+
setItems(getAnnotations(sessionId));
|
|
7241
|
+
}, []);
|
|
6967
7242
|
const onItemSelect = (0, react.useCallback)((index, item) => {
|
|
6968
7243
|
scrollToAnnotation(index + 1, item.text, item.id);
|
|
6969
7244
|
}, []);
|
|
6970
7245
|
const onItemRemove = (0, react.useCallback)((_index, item) => {
|
|
6971
|
-
if (item.id) removeAnnotation(
|
|
7246
|
+
if (item.id) removeAnnotation(sessionId, item.id);
|
|
6972
7247
|
else {
|
|
6973
|
-
const hit = getAnnotations().find((entry) => entry.text === item.text);
|
|
6974
|
-
if (hit) removeAnnotation(
|
|
7248
|
+
const hit = getAnnotations(sessionId).find((entry) => entry.text === item.text);
|
|
7249
|
+
if (hit) removeAnnotation(sessionId, hit.id);
|
|
6975
7250
|
}
|
|
6976
7251
|
dropIfEmpty();
|
|
6977
7252
|
}, [dropIfEmpty]);
|
|
6978
7253
|
const onItemEdit = (0, react.useCallback)((_index, item, comment) => {
|
|
6979
|
-
const id = item.id ?? getAnnotations().find((entry) => entry.text === item.text)?.id;
|
|
7254
|
+
const id = item.id ?? getAnnotations(sessionId).find((entry) => entry.text === item.text)?.id;
|
|
6980
7255
|
if (!id) return;
|
|
6981
|
-
updateAnnotationComment(
|
|
7256
|
+
updateAnnotationComment(sessionId, id, comment);
|
|
6982
7257
|
sync();
|
|
6983
7258
|
}, [sync]);
|
|
6984
7259
|
/**
|
|
@@ -6994,7 +7269,7 @@ function AnnotationPanel({ useInput, inputActions, sessionId }) {
|
|
|
6994
7269
|
showToast(t("handoff.archived", { title: live }));
|
|
6995
7270
|
return;
|
|
6996
7271
|
}
|
|
6997
|
-
const entry = getAnnotations().find((candidate) => candidate.id === item.id);
|
|
7272
|
+
const entry = getAnnotations(sessionId).find((candidate) => candidate.id === item.id);
|
|
6998
7273
|
const handoffId = origin.handoffId ?? entry?.origin?.handoffId;
|
|
6999
7274
|
openSourceAndLocate({
|
|
7000
7275
|
sessionId: origin.sessionId,
|
|
@@ -7604,20 +7879,39 @@ function readSelectionSnapshot(toolbarRoot) {
|
|
|
7604
7879
|
function clearNativeSelection() {
|
|
7605
7880
|
window.getSelection()?.removeAllRanges();
|
|
7606
7881
|
}
|
|
7882
|
+
/**
|
|
7883
|
+
* Put text on the clipboard, falling back to the legacy selection copy.
|
|
7884
|
+
*
|
|
7885
|
+
* The async Clipboard API is the good path, but it is not always available in
|
|
7886
|
+
* practice: it rejects with `NotAllowedError` when the document is not focused
|
|
7887
|
+
* (a popover opened over an unfocused window, DevTools holding focus, an
|
|
7888
|
+
* embedded frame) and on origins the browser does not treat as secure. Only
|
|
7889
|
+
* reaching for the fallback when the API is *missing* turned every one of those
|
|
7890
|
+
* into a hard failure — the copy button reported "copy failed" and the text was
|
|
7891
|
+
* lost. Try the API, and on rejection fall back before giving up.
|
|
7892
|
+
*/
|
|
7607
7893
|
async function copyText(text) {
|
|
7608
|
-
if (navigator.clipboard?.writeText) {
|
|
7894
|
+
if (navigator.clipboard?.writeText) try {
|
|
7609
7895
|
await navigator.clipboard.writeText(text);
|
|
7610
7896
|
return;
|
|
7611
|
-
}
|
|
7897
|
+
} catch {}
|
|
7612
7898
|
const area = document.createElement("textarea");
|
|
7613
7899
|
area.value = text;
|
|
7614
7900
|
area.setAttribute("readonly", "");
|
|
7615
7901
|
area.style.position = "fixed";
|
|
7902
|
+
area.style.top = "0";
|
|
7616
7903
|
area.style.left = "-9999px";
|
|
7617
7904
|
document.body.appendChild(area);
|
|
7905
|
+
const previous = document.activeElement;
|
|
7618
7906
|
area.select();
|
|
7619
|
-
|
|
7620
|
-
|
|
7907
|
+
let copied = false;
|
|
7908
|
+
try {
|
|
7909
|
+
copied = document.execCommand("copy");
|
|
7910
|
+
} finally {
|
|
7911
|
+
area.remove();
|
|
7912
|
+
if (previous instanceof HTMLElement) previous.focus({ preventScroll: true });
|
|
7913
|
+
}
|
|
7914
|
+
if (!copied) throw new Error("the clipboard rejected the copy");
|
|
7621
7915
|
}
|
|
7622
7916
|
//#endregion
|
|
7623
7917
|
//#region src/client/model/rank.ts
|
|
@@ -8407,12 +8701,9 @@ function splitLabelAt(label, accent) {
|
|
|
8407
8701
|
{ text: label.slice(at + needle.length) }
|
|
8408
8702
|
].filter((part) => part.text !== "");
|
|
8409
8703
|
}
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
* here reaches both, so the two entry points cannot drift apart.
|
|
8414
|
-
*/
|
|
8415
|
-
function selectionActions(snapshot, handoff, visibility) {
|
|
8704
|
+
function selectionActions(snapshot, handoff, visibility, availability) {
|
|
8705
|
+
const translateDisabled = availability?.translate === false;
|
|
8706
|
+
const sideDisabled = availability?.sideTask === false;
|
|
8416
8707
|
return [
|
|
8417
8708
|
...linkTarget(snapshot.text, snapshot.anchorHref) === null ? [] : [{
|
|
8418
8709
|
key: "open",
|
|
@@ -8427,7 +8718,11 @@ function selectionActions(snapshot, handoff, visibility) {
|
|
|
8427
8718
|
...visibility?.hideTranslate ? [] : [{
|
|
8428
8719
|
key: "translate",
|
|
8429
8720
|
icon: "languages",
|
|
8430
|
-
label: t("toolbar.translate")
|
|
8721
|
+
label: t("toolbar.translate"),
|
|
8722
|
+
...translateDisabled ? {
|
|
8723
|
+
disabled: true,
|
|
8724
|
+
disabledReason: t("toolbar.translateUnavailable")
|
|
8725
|
+
} : {}
|
|
8431
8726
|
}],
|
|
8432
8727
|
{
|
|
8433
8728
|
key: "add",
|
|
@@ -8444,7 +8739,11 @@ function selectionActions(snapshot, handoff, visibility) {
|
|
|
8444
8739
|
{
|
|
8445
8740
|
key: "side",
|
|
8446
8741
|
icon: "panel-right",
|
|
8447
|
-
label: t("toolbar.askInSide")
|
|
8742
|
+
label: t("toolbar.askInSide"),
|
|
8743
|
+
...sideDisabled ? {
|
|
8744
|
+
disabled: true,
|
|
8745
|
+
disabledReason: t("toolbar.sideUnavailable")
|
|
8746
|
+
} : {}
|
|
8448
8747
|
}
|
|
8449
8748
|
];
|
|
8450
8749
|
}
|
|
@@ -8555,9 +8854,11 @@ function SelectionMenu({ entries, x, y, onClose, initialFocus = "first" }) {
|
|
|
8555
8854
|
role: "menuitem",
|
|
8556
8855
|
className: cx(A.plain, A.row, styles.ctxItem),
|
|
8557
8856
|
tabIndex: -1,
|
|
8857
|
+
"aria-disabled": entry.disabled ? "true" : void 0,
|
|
8858
|
+
"data-dsq-tip": entry.disabled ? entry.disabledReason : void 0,
|
|
8558
8859
|
onPointerDown: keepSelection,
|
|
8559
8860
|
onPointerEnter: (event) => event.currentTarget.focus({ preventScroll: true }),
|
|
8560
|
-
onClick: entry.onClick,
|
|
8861
|
+
onClick: entry.disabled ? void 0 : entry.onClick,
|
|
8561
8862
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
8562
8863
|
name: entry.icon,
|
|
8563
8864
|
size: 15
|
|
@@ -8745,8 +9046,10 @@ function ActionRow({ actions, stacked, onHandoffPick }) {
|
|
|
8745
9046
|
type: "button",
|
|
8746
9047
|
className: cx(A.plain, stacked ? A.col : A.inlineRow, stacked ? styles.touchAction : styles.button),
|
|
8747
9048
|
"aria-haspopup": handoff ? "dialog" : void 0,
|
|
9049
|
+
"aria-disabled": action.disabled ? "true" : void 0,
|
|
9050
|
+
"data-dsq-tip": action.disabled ? action.disabledReason : void 0,
|
|
8748
9051
|
"data-dsq-handoff-trigger": handoff ? "true" : void 0,
|
|
8749
|
-
onClick: action.onClick,
|
|
9052
|
+
onClick: action.disabled ? void 0 : action.onClick,
|
|
8750
9053
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
8751
9054
|
name: action.icon,
|
|
8752
9055
|
size: stacked ? 18 : 14
|
|
@@ -9061,9 +9364,15 @@ function TranslatePanel({ translate, touch = false, quote }) {
|
|
|
9061
9364
|
className: cx(A.row, styles.translateActions),
|
|
9062
9365
|
children: state.status === "done" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
9063
9366
|
type: "button",
|
|
9064
|
-
className: cx(A.plain, A.
|
|
9367
|
+
className: cx(A.plain, A.gridMid, styles.translateCopy),
|
|
9368
|
+
"aria-label": translate.copied ? t("translate.copied") : t("translate.copy"),
|
|
9369
|
+
title: translate.copied ? t("translate.copied") : t("translate.copy"),
|
|
9370
|
+
"data-copied": translate.copied ? "true" : void 0,
|
|
9065
9371
|
onClick: translate.onCopy,
|
|
9066
|
-
children:
|
|
9372
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
9373
|
+
name: translate.copied ? "check" : "copy",
|
|
9374
|
+
size: 16
|
|
9375
|
+
})
|
|
9067
9376
|
}) : state.status === "pending" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
9068
9377
|
type: "button",
|
|
9069
9378
|
className: cx(A.plain, A.inlineRow, styles.button),
|
|
@@ -9170,11 +9479,15 @@ function ToolbarView({ touch, phase, active, status, viewport, actions, comment,
|
|
|
9170
9479
|
})
|
|
9171
9480
|
]
|
|
9172
9481
|
})] }) : null,
|
|
9173
|
-
status ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
9482
|
+
status ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9174
9483
|
className: styles.touchStatus,
|
|
9175
9484
|
style: { bottom: `calc(${viewport.inset}px + 72px + env(safe-area-inset-bottom, 0px))` },
|
|
9176
9485
|
role: "status",
|
|
9177
|
-
children:
|
|
9486
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
9487
|
+
name: "circle-alert",
|
|
9488
|
+
size: 13,
|
|
9489
|
+
className: styles.statusIcon
|
|
9490
|
+
}), status]
|
|
9178
9491
|
}) : null
|
|
9179
9492
|
] }), document.body);
|
|
9180
9493
|
}
|
|
@@ -9246,11 +9559,15 @@ function ToolbarView({ touch, phase, active, status, viewport, actions, comment,
|
|
|
9246
9559
|
onCancel: onCancelSide
|
|
9247
9560
|
})
|
|
9248
9561
|
}) : null,
|
|
9249
|
-
status ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
9562
|
+
status ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9250
9563
|
className: styles.status,
|
|
9251
9564
|
style: toolbarStyle,
|
|
9252
9565
|
role: "status",
|
|
9253
|
-
children:
|
|
9566
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LucideIcon, {
|
|
9567
|
+
name: "circle-alert",
|
|
9568
|
+
size: 13,
|
|
9569
|
+
className: styles.statusIcon
|
|
9570
|
+
}), status]
|
|
9254
9571
|
}) : null
|
|
9255
9572
|
] }), active.layer.host);
|
|
9256
9573
|
}
|
|
@@ -10072,6 +10389,24 @@ function useSelectionWatcher({ toolbarRef, commentWrapRef, phaseRef, touch, dism
|
|
|
10072
10389
|
* its model seat and the send gesture. The transcript surface is the toolbar's;
|
|
10073
10390
|
* this hook owns the question, its route and the host call.
|
|
10074
10391
|
*/
|
|
10392
|
+
/**
|
|
10393
|
+
* Remove the pending draft annotations that stand for this selection.
|
|
10394
|
+
*
|
|
10395
|
+
* A pending item carries every source the selection resolved to; the side send
|
|
10396
|
+
* carries the same locator. Matching on `selectionKeyOf` keeps two annotations
|
|
10397
|
+
* of identical text from different messages apart, and falls back to plain text
|
|
10398
|
+
* when the snapshot has no locator (the selection missed the transcript model).
|
|
10399
|
+
*/
|
|
10400
|
+
function consumeQuote(target, sessionId) {
|
|
10401
|
+
const source = target.source;
|
|
10402
|
+
const wanted = selectionKeyOf(target.text, source === void 0 ? void 0 : {
|
|
10403
|
+
text: target.text,
|
|
10404
|
+
messageId: source.messageId,
|
|
10405
|
+
startOffset: source.startOffset,
|
|
10406
|
+
endOffset: source.endOffset
|
|
10407
|
+
});
|
|
10408
|
+
for (const item of getAnnotations(sessionId)) if (item.sources.some((candidate) => source === void 0 ? candidate.text === target.text : selectionKeyOf(candidate.text, candidate) === wanted)) removeAnnotation(sessionId, item.id);
|
|
10409
|
+
}
|
|
10075
10410
|
function useSideQuestion(input) {
|
|
10076
10411
|
const { phase, activeRef, hide, setStatus, setActive, dismissMenu, setPhase } = input;
|
|
10077
10412
|
const [sideText, setSideText] = (0, react.useState)("");
|
|
@@ -10116,6 +10451,7 @@ function useSideQuestion(input) {
|
|
|
10116
10451
|
run(snapshot, question, sideModel).then(() => {
|
|
10117
10452
|
setSending(false);
|
|
10118
10453
|
clearNativeSelection();
|
|
10454
|
+
consumeQuote(snapshot);
|
|
10119
10455
|
hide();
|
|
10120
10456
|
}, (error) => {
|
|
10121
10457
|
setSending(false);
|
|
@@ -11035,7 +11371,11 @@ function SelectionToolbar({ sessionId }) {
|
|
|
11035
11371
|
left: active.left,
|
|
11036
11372
|
top: active.top
|
|
11037
11373
|
};
|
|
11038
|
-
const
|
|
11374
|
+
const availability = {
|
|
11375
|
+
sideTask: runtime.sideTask !== void 0,
|
|
11376
|
+
translate: runtime.translate !== void 0
|
|
11377
|
+
};
|
|
11378
|
+
const actions = selectionActions(active, handoff.options, visibilityFor(active.text), availability).map((action) => ({
|
|
11039
11379
|
...action,
|
|
11040
11380
|
onClick: () => runAction(action.key, active)
|
|
11041
11381
|
}));
|
|
@@ -11108,7 +11448,7 @@ function SelectionToolbar({ sessionId }) {
|
|
|
11108
11448
|
onClose: handoff.closePicker
|
|
11109
11449
|
}) : null,
|
|
11110
11450
|
menu ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectionMenu, {
|
|
11111
|
-
entries: selectionActions(menu.snapshot, handoff.options, visibilityFor(menu.snapshot.text)).map((action) => ({
|
|
11451
|
+
entries: selectionActions(menu.snapshot, handoff.options, visibilityFor(menu.snapshot.text), availability).map((action) => ({
|
|
11112
11452
|
...action,
|
|
11113
11453
|
label: action.key === "handoff" ? action.menuLabel ?? action.label : action.label,
|
|
11114
11454
|
onClick: () => runAction(action.key, menu.snapshot, "menu")
|
|
@@ -12651,11 +12991,55 @@ function bindChipPreview(chip, index) {
|
|
|
12651
12991
|
*/
|
|
12652
12992
|
const DECO = "dsq-ann-directive";
|
|
12653
12993
|
const DECO_KEY = "dsqAnnDirective";
|
|
12994
|
+
/** True when the text is exactly one directive and nothing else. */
|
|
12995
|
+
function isOnlyDirective(value) {
|
|
12996
|
+
return new RegExp(`^\\s*${ANNOTATION_DIRECTIVE_RE.source}\\s*$`, "u").test(value);
|
|
12997
|
+
}
|
|
12998
|
+
/**
|
|
12999
|
+
* Whether a text node sitting inside `<code>` should be turned into a chip.
|
|
13000
|
+
*
|
|
13001
|
+
* The protocol header tells the model to emit the directive inside inline code,
|
|
13002
|
+
* so the common case is a `<code>` whose entire content is one directive. A
|
|
13003
|
+
* fenced block (inside `pre`) or a sentence that merely mentions the marker
|
|
13004
|
+
* keeps its literal text.
|
|
13005
|
+
*/
|
|
13006
|
+
function shouldChipCode(value, codeText, inPre) {
|
|
13007
|
+
if (inPre) return false;
|
|
13008
|
+
if (codeText == null) return false;
|
|
13009
|
+
if (codeText.trim() !== value.trim()) return false;
|
|
13010
|
+
return isOnlyDirective(value);
|
|
13011
|
+
}
|
|
13012
|
+
/**
|
|
13013
|
+
* Replace a `<code>` that holds nothing but one directive with its chip.
|
|
13014
|
+
*
|
|
13015
|
+
* The code element itself is dropped: keeping it would leave the grey inline
|
|
13016
|
+
* background around a button. The chip carries the same affordance the prose
|
|
13017
|
+
* form gets, so the reply reads the same either way.
|
|
13018
|
+
*/
|
|
13019
|
+
function decorateCodeSpan(code, raw) {
|
|
13020
|
+
const match = new RegExp(`^\\s*${ANNOTATION_DIRECTIVE_RE.source}\\s*$`, "u").exec(raw);
|
|
13021
|
+
if (match === null) return;
|
|
13022
|
+
const index = Number(match[1]);
|
|
13023
|
+
if (!Number.isFinite(index) || index <= 0) return;
|
|
13024
|
+
const chip = document.createElement("button");
|
|
13025
|
+
chip.type = "button";
|
|
13026
|
+
chip.className = cx(styles.annDirective, A.inlineRow);
|
|
13027
|
+
chip.setAttribute("data-dsq-deco", "true");
|
|
13028
|
+
chip.dataset[DECO_KEY] = String(index);
|
|
13029
|
+
chip.textContent = t("chip.annotation", { index });
|
|
13030
|
+
bindChipPreview(chip, index);
|
|
13031
|
+
code.replaceWith(chip);
|
|
13032
|
+
}
|
|
12654
13033
|
function decorateTextNode(node) {
|
|
12655
13034
|
const value = node.nodeValue;
|
|
12656
13035
|
if (!value || !value.includes(":dsh-annotation{")) return;
|
|
12657
13036
|
const parent = node.parentElement;
|
|
12658
13037
|
if (!parent || parent.closest(`[data-${DECO}]`)) return;
|
|
13038
|
+
const code = parent.closest("code");
|
|
13039
|
+
if (code !== null && shouldChipCode(value, code.textContent, code.closest("pre") !== null)) {
|
|
13040
|
+
decorateCodeSpan(code, value);
|
|
13041
|
+
return;
|
|
13042
|
+
}
|
|
12659
13043
|
if (parent.closest("pre, code, [data-dsq-toolbar], [data-dsq-chip-preview]")) return;
|
|
12660
13044
|
if (parent.closest("textarea, input, [data-lexical-editor=\"true\"]")) return;
|
|
12661
13045
|
const frag = document.createDocumentFragment();
|
|
@@ -12742,10 +13126,9 @@ function watchAnnotationDirectives(root) {
|
|
|
12742
13126
|
* from the composer draft — which, at that instant, is the protocol block the
|
|
12743
13127
|
* send gesture just folded in. A message submitted while a turn is running is
|
|
12744
13128
|
* also previewed in the queue dock (`[data-queue-dock]`) from the same wire
|
|
12745
|
-
* content. Both are
|
|
12746
|
-
*
|
|
12747
|
-
*
|
|
12748
|
-
* message replaces it.
|
|
13129
|
+
* content. Both are painted by the chat flow itself rather than by any slot
|
|
13130
|
+
* entry, so the durable-message path never sees them; without this, the raw
|
|
13131
|
+
* JSON sits on screen until (or unless) a durable message replaces it.
|
|
12749
13132
|
*
|
|
12750
13133
|
* The durable message keeps the block (the model needs it). This is display
|
|
12751
13134
|
* only: the echo's protocol run is rewritten in place, and the queue preview's
|
|
@@ -26754,6 +27137,57 @@ function createPluginRemote() {
|
|
|
26754
27137
|
*/
|
|
26755
27138
|
var remote_default = createPluginRemote();
|
|
26756
27139
|
//#endregion
|
|
27140
|
+
//#region src/client/transcript/chat-seat.ts
|
|
27141
|
+
/** Plain text from Chat user-node content blocks (string or block array). */
|
|
27142
|
+
function blocksToText(content) {
|
|
27143
|
+
if (typeof content === "string") return content;
|
|
27144
|
+
if (!Array.isArray(content)) return "";
|
|
27145
|
+
let out = "";
|
|
27146
|
+
for (const block of content) {
|
|
27147
|
+
if (block === null || typeof block !== "object") continue;
|
|
27148
|
+
const text = block.text;
|
|
27149
|
+
if (typeof text === "string") out += text;
|
|
27150
|
+
}
|
|
27151
|
+
return out;
|
|
27152
|
+
}
|
|
27153
|
+
/**
|
|
27154
|
+
* The message text behind one user row, or undefined while it is not loaded.
|
|
27155
|
+
*
|
|
27156
|
+
* Matching is on `rpcId`, not on `messageId`: the seat's source carries the
|
|
27157
|
+
* submission rpc id, and the durable node keeps that same id in its own
|
|
27158
|
+
* `data.source`, while `data.messageId` is a different value.
|
|
27159
|
+
*
|
|
27160
|
+
* Kept as plain data code (no React) so the verification harness can replay it
|
|
27161
|
+
* without a DOM.
|
|
27162
|
+
*/
|
|
27163
|
+
/**
|
|
27164
|
+
* The message text for one chat flow key.
|
|
27165
|
+
*
|
|
27166
|
+
* The activation prompt a side question sends carries `source: { kind: "user" }`
|
|
27167
|
+
* with **no rpcId** (the Host's `startLocal` submits it that way), so the rpcId
|
|
27168
|
+
* match cannot find it. The row's own `data-chat-flow-key` is the durable key
|
|
27169
|
+
* into `nodes.byKey`, which resolves it exactly instead of guessing.
|
|
27170
|
+
*/
|
|
27171
|
+
function chatNodeTextOf(state, flowKey) {
|
|
27172
|
+
if (flowKey === null || flowKey === void 0) return void 0;
|
|
27173
|
+
const byKey = state?.nodes?.byKey;
|
|
27174
|
+
if (!(byKey instanceof Map)) return void 0;
|
|
27175
|
+
const node = byKey.get(flowKey);
|
|
27176
|
+
if (node?.kind !== "user") return void 0;
|
|
27177
|
+
return blocksToText(node.data?.content);
|
|
27178
|
+
}
|
|
27179
|
+
function userMessageTextOf(state, rpcId) {
|
|
27180
|
+
if (rpcId === void 0) return void 0;
|
|
27181
|
+
const byKey = state?.nodes?.byKey;
|
|
27182
|
+
if (!(byKey instanceof Map)) return void 0;
|
|
27183
|
+
for (const value of byKey.values()) {
|
|
27184
|
+
const node = value;
|
|
27185
|
+
if (node?.kind !== "user") continue;
|
|
27186
|
+
if (node.data?.source?.rpcId !== rpcId) continue;
|
|
27187
|
+
return blocksToText(node.data?.content);
|
|
27188
|
+
}
|
|
27189
|
+
}
|
|
27190
|
+
//#endregion
|
|
26757
27191
|
//#region src/client/transcript/TranscriptQuoteCard.tsx
|
|
26758
27192
|
/**
|
|
26759
27193
|
* History-side summary for one user message's annotations.
|
|
@@ -26765,7 +27199,7 @@ var remote_default = createPluginRemote();
|
|
|
26765
27199
|
* scrolls back to the marked text in the source message; the ordinal badge
|
|
26766
27200
|
* matches the `:dsh-annotation{index="N"}` the model refers to.
|
|
26767
27201
|
*/
|
|
26768
|
-
function TranscriptQuoteCard({ node }) {
|
|
27202
|
+
function TranscriptQuoteCard({ node, robot = false }) {
|
|
26769
27203
|
ensureToolbarStyles();
|
|
26770
27204
|
const stackRef = (0, react.useRef)(null);
|
|
26771
27205
|
const quotes = node.data?.quotes ?? [];
|
|
@@ -26781,7 +27215,8 @@ function TranscriptQuoteCard({ node }) {
|
|
|
26781
27215
|
index: index + 1,
|
|
26782
27216
|
text: quote.text,
|
|
26783
27217
|
startOffset: quote.startOffset,
|
|
26784
|
-
endOffset: quote.endOffset
|
|
27218
|
+
endOffset: quote.endOffset,
|
|
27219
|
+
...robot ? { variant: "robot" } : {}
|
|
26785
27220
|
})));
|
|
26786
27221
|
}, [quotes]);
|
|
26787
27222
|
(0, react.useEffect)(() => {
|
|
@@ -26818,6 +27253,29 @@ function TranscriptQuoteCard({ node }) {
|
|
|
26818
27253
|
}
|
|
26819
27254
|
scrollToAnnotation(index + 1, quote.text, markIdOf(index));
|
|
26820
27255
|
}, [markIdOf, quotes]);
|
|
27256
|
+
/**
|
|
27257
|
+
* "From: <source>" opens the conversation this annotation was selected in.
|
|
27258
|
+
*
|
|
27259
|
+
* The composer card has always wired this; the history card rendered the same
|
|
27260
|
+
* button from `item.origin` but passed no handler, so the link was visibly
|
|
27261
|
+
* there and did nothing. A cross-session item has no mark in THIS transcript
|
|
27262
|
+
* by design, so the origin jump is the only way back to its source.
|
|
27263
|
+
*/
|
|
27264
|
+
const onOrigin = (0, react.useCallback)((index, _item) => {
|
|
27265
|
+
const quote = quotes[index];
|
|
27266
|
+
const origin = quote?.origin;
|
|
27267
|
+
if (origin === void 0) return;
|
|
27268
|
+
const live = sessionTitleOf(origin.sessionId, origin.title);
|
|
27269
|
+
if (isSessionArchived(origin.sessionId)) {
|
|
27270
|
+
showToast(t("handoff.archived", { title: live }));
|
|
27271
|
+
return;
|
|
27272
|
+
}
|
|
27273
|
+
openSourceAndLocate({
|
|
27274
|
+
sessionId: origin.sessionId,
|
|
27275
|
+
text: quote.text,
|
|
27276
|
+
...origin.handoffId ? { markId: handoffMarkIdOf(origin.handoffId) } : {}
|
|
27277
|
+
});
|
|
27278
|
+
}, [quotes]);
|
|
26821
27279
|
if (quotes.length === 0) return null;
|
|
26822
27280
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
26823
27281
|
ref: stackRef,
|
|
@@ -26836,129 +27294,183 @@ function TranscriptQuoteCard({ node }) {
|
|
|
26836
27294
|
archived: isSessionArchived(quote.origin.sessionId)
|
|
26837
27295
|
} } : {}
|
|
26838
27296
|
})),
|
|
26839
|
-
onItemSelect: onSelect
|
|
27297
|
+
onItemSelect: onSelect,
|
|
27298
|
+
onItemOrigin: onOrigin
|
|
26840
27299
|
})
|
|
26841
27300
|
});
|
|
26842
27301
|
}
|
|
26843
27302
|
//#endregion
|
|
26844
|
-
//#region src/client/transcript/
|
|
26845
|
-
function isTextBlock(block) {
|
|
26846
|
-
if (block === null || typeof block !== "object") return false;
|
|
26847
|
-
const candidate = block;
|
|
26848
|
-
if ((candidate.type === "image" || candidate.type === "file") && candidate.attachment !== void 0) return false;
|
|
26849
|
-
return candidate.type === "text" || typeof candidate.text === "string";
|
|
26850
|
-
}
|
|
27303
|
+
//#region src/client/transcript/UserBeforeCard.tsx
|
|
26851
27304
|
/**
|
|
26852
|
-
*
|
|
26853
|
-
*
|
|
26854
|
-
*
|
|
26855
|
-
*
|
|
27305
|
+
* Annotation card above a user message, mounted on the Host's own decoration
|
|
27306
|
+
* seat (`conversation.chat.user.before`).
|
|
27307
|
+
*
|
|
27308
|
+
* dsh 0.2.2 makes this seat the official place for message-source decoration,
|
|
27309
|
+
* and it is the reason this plugin mounts its card here instead of overriding
|
|
27310
|
+
* the `user` Chat node. That override used to shadow the Host's own
|
|
27311
|
+
* `UserMessageNodeView`, and once shadowed the Host entry disappeared from the
|
|
27312
|
+
* registry — so the wrapper could never find it to delegate to, fell back to a
|
|
27313
|
+
* private bubble, and the message lost its time/copy row.
|
|
27314
|
+
*
|
|
27315
|
+
* The seat hands the entry `{source, kind}` only: no message body. Two ways back
|
|
27316
|
+
* to the body, in this order:
|
|
27317
|
+
*
|
|
27318
|
+
* 1. `source.rpcId` — a user submission or a `subagents.prompt` delivery both
|
|
27319
|
+
* carry one, and the durable node keeps it in `data.source`.
|
|
27320
|
+
* 2. the row's own `data-chat-flow-key` — the activation prompt of a side
|
|
27321
|
+
* question is submitted as `source: { kind: 'user' }`, with **no rpcId**.
|
|
27322
|
+
*
|
|
27323
|
+
* A subagent session is a fork of its parent: every copied message keeps the
|
|
27324
|
+
* parent's rpcId, so those rows would re-render the parent's cards inside the
|
|
27325
|
+
* child. Inherited rows are hidden there; only the child's own question (the
|
|
27326
|
+
* rpcId-less activation) decorates.
|
|
27327
|
+
*/
|
|
27328
|
+
/**
|
|
27329
|
+
* Annotation summary for the user row this seat decorates.
|
|
27330
|
+
*
|
|
27331
|
+
* Renders nothing when the message carries no annotation block, so rows without
|
|
27332
|
+
* annotations are untouched — the Host keeps ownership of the bubble, its
|
|
27333
|
+
* attachments and its time/copy actions row.
|
|
26856
27334
|
*/
|
|
26857
|
-
function
|
|
26858
|
-
const
|
|
26859
|
-
|
|
26860
|
-
|
|
26861
|
-
|
|
27335
|
+
function UserBeforeCard({ matched, useChat, sessionId }) {
|
|
27336
|
+
const rpcId = typeof matched?.source?.rpcId === "string" ? matched.source.rpcId : void 0;
|
|
27337
|
+
const hostRef = (0, react.useRef)(null);
|
|
27338
|
+
const [flowKey, setFlowKey] = (0, react.useState)(null);
|
|
27339
|
+
(0, react.useLayoutEffect)(() => {
|
|
27340
|
+
if (rpcId !== void 0) return;
|
|
27341
|
+
const key = hostRef.current?.closest("[data-chat-flow-key]")?.getAttribute("data-chat-flow-key");
|
|
27342
|
+
setFlowKey(key ?? null);
|
|
27343
|
+
}, [rpcId]);
|
|
27344
|
+
const select = (read) => typeof useChat === "function" ? useChat(read) : void 0;
|
|
27345
|
+
const rpcText = select((state) => userMessageTextOf(state, rpcId));
|
|
27346
|
+
const keyedText = select((state) => chatNodeTextOf(state, flowKey));
|
|
27347
|
+
const text = rpcText ?? keyedText;
|
|
27348
|
+
const quotes = (0, react.useMemo)(() => {
|
|
27349
|
+
if (typeof text !== "string" || !text.includes("<response-annotations>")) return null;
|
|
27350
|
+
const scanned = scanAnnotatedMessage(text);
|
|
27351
|
+
return scanned.annotations.length > 0 ? scanned.annotations : null;
|
|
27352
|
+
}, [text]);
|
|
27353
|
+
const isSubagent = sessionId !== void 0 && runtime.isSubagentSession?.(sessionId) === true;
|
|
27354
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
27355
|
+
ref: hostRef,
|
|
27356
|
+
style: { display: "contents" },
|
|
27357
|
+
"data-dsq-user-before": "",
|
|
27358
|
+
children: quotes !== null && !(isSubagent && rpcId !== void 0) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TranscriptQuoteCard, {
|
|
27359
|
+
node: { data: { quotes } },
|
|
27360
|
+
robot: isSubagent
|
|
27361
|
+
}) : null
|
|
26862
27362
|
});
|
|
26863
|
-
return rest;
|
|
26864
|
-
}
|
|
26865
|
-
/** Image attachments for the factory-less fallback bubble. */
|
|
26866
|
-
function imageAttachments(raw) {
|
|
26867
|
-
if (!Array.isArray(raw)) return [];
|
|
26868
|
-
const images = [];
|
|
26869
|
-
for (const block of raw) {
|
|
26870
|
-
if (block === null || typeof block !== "object") continue;
|
|
26871
|
-
const candidate = block;
|
|
26872
|
-
if (candidate.type === "image" && candidate.attachment !== void 0) images.push(candidate.attachment);
|
|
26873
|
-
}
|
|
26874
|
-
return images;
|
|
26875
27363
|
}
|
|
27364
|
+
//#endregion
|
|
27365
|
+
//#region src/client/transcript/protocol-rule.ts
|
|
26876
27366
|
/**
|
|
26877
|
-
*
|
|
27367
|
+
* dsh 0.2.2 user-text rule: hide the annotation protocol block.
|
|
27368
|
+
*
|
|
27369
|
+
* The host splits a durable user message into inline nodes through
|
|
27370
|
+
* `uiConversation.userText` rules. Text that no rule claims is rendered as the
|
|
27371
|
+
* literal substring, so a message carrying this plugin's
|
|
27372
|
+
* `<response-annotations>` block would paint the whole JSON — header prose
|
|
27373
|
+
* included — into the bubble.
|
|
26878
27374
|
*
|
|
26879
|
-
*
|
|
26880
|
-
*
|
|
26881
|
-
*
|
|
26882
|
-
*
|
|
26883
|
-
*
|
|
26884
|
-
* actions row, e.g. dsh-rewind's ↶) stays host-owned and follows host changes.
|
|
26885
|
-
* The annotation summary is the only extra DOM, prepended above the host row.
|
|
27375
|
+
* The rule claims exactly the protocol span (header prose and the
|
|
27376
|
+
* `<response-annotations>…</response-annotations>` payload) and renders it as
|
|
27377
|
+
* nothing. Annotation cards themselves are not built here: this rule only
|
|
27378
|
+
* removes text. The cards come from the `conversation.chat.user.before` chain,
|
|
27379
|
+
* which the host still renders above the bubble.
|
|
26886
27380
|
*
|
|
26887
|
-
*
|
|
26888
|
-
* `
|
|
26889
|
-
* `
|
|
26890
|
-
*
|
|
26891
|
-
*
|
|
27381
|
+
* Contract (see `UserTextDocument` in `@deepseek-ai/dsh-client-ui-conversation`):
|
|
27382
|
+
* `match(text, from, context)` must return a node whose `start >= from`, whose
|
|
27383
|
+
* `end > start`, whose `end <= text.length`, and whose `id` is either this
|
|
27384
|
+
* rule's id or the reserved `text` id. Returning `undefined` means "nothing
|
|
27385
|
+
* here". Evaluators run the head of every rule at each cursor, so the rule must
|
|
27386
|
+
* be cheap and must report the earliest claimed position, not the first
|
|
27387
|
+
* occurrence in the document.
|
|
26892
27388
|
*/
|
|
26893
|
-
|
|
26894
|
-
|
|
26895
|
-
|
|
26896
|
-
|
|
26897
|
-
|
|
26898
|
-
|
|
26899
|
-
|
|
26900
|
-
|
|
26901
|
-
|
|
26902
|
-
|
|
26903
|
-
|
|
26904
|
-
|
|
26905
|
-
|
|
26906
|
-
|
|
26907
|
-
|
|
26908
|
-
|
|
26909
|
-
|
|
26910
|
-
|
|
26911
|
-
|
|
26912
|
-
|
|
26913
|
-
|
|
26914
|
-
if (Host !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [cards, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Host, {
|
|
26915
|
-
...rest,
|
|
26916
|
-
node: displayNode
|
|
26917
|
-
})] });
|
|
26918
|
-
const images = imageAttachments(node.data?.content);
|
|
26919
|
-
const loadImage = props.loadImage;
|
|
26920
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [cards, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
26921
|
-
className: cx(A.colEnd, styles.userRow),
|
|
26922
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
26923
|
-
className: cx(A.colEnd, styles.userStack),
|
|
26924
|
-
"data-dsq-fallback-content": "true",
|
|
26925
|
-
children: [images.length > 0 && loadImage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
26926
|
-
className: cx(A.colEnd, styles.userImages),
|
|
26927
|
-
"data-message-attachments": true,
|
|
26928
|
-
children: images.map((attachment, index) => {
|
|
26929
|
-
const url = loadImage(attachment);
|
|
26930
|
-
return url === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
26931
|
-
src: url,
|
|
26932
|
-
alt: ""
|
|
26933
|
-
}, `image-${index}`);
|
|
26934
|
-
})
|
|
26935
|
-
}) : null, text ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
26936
|
-
className: styles.userBubble,
|
|
26937
|
-
children: text
|
|
26938
|
-
}) : null]
|
|
26939
|
-
})
|
|
26940
|
-
})] });
|
|
26941
|
-
}
|
|
26942
|
-
//#endregion
|
|
26943
|
-
//#region src/client/transcript/host-user.ts
|
|
26944
|
-
const HOST_NODE_NAME = "UserMessageNodeView";
|
|
26945
|
-
/** Component name across memo/forwardRef/plain function shapes. */
|
|
26946
|
-
function componentName(component) {
|
|
26947
|
-
const candidate = component;
|
|
26948
|
-
return candidate?.type?.name ?? candidate?.render?.name ?? candidate?.name ?? candidate?.displayName;
|
|
27389
|
+
/** Rule id registered with `uiConversation.userText`. */
|
|
27390
|
+
const PROTOCOL_RULE_ID = "select-quote.annotations";
|
|
27391
|
+
/**
|
|
27392
|
+
* Runs after the host's own rules (`session` = 10, whitespace = 1000/1010) so
|
|
27393
|
+
* a `@session` reference inside the block cannot claim a prefix of it and leave
|
|
27394
|
+
* a half-block behind. Lower order wins ties at the same start.
|
|
27395
|
+
*/
|
|
27396
|
+
const PROTOCOL_RULE_ORDER = 10100;
|
|
27397
|
+
const BLOCK_OPEN = "<response-annotations>";
|
|
27398
|
+
const BLOCK_CLOSE = "</response-annotations>";
|
|
27399
|
+
const HEADER_MARKER = "# Response annotations:";
|
|
27400
|
+
/**
|
|
27401
|
+
* True when `between` is the instruction body that sits between the header
|
|
27402
|
+
* title and the open marker: prose lines followed by one blank line, with no
|
|
27403
|
+
* second paragraph break. An unrelated quotation of the title leaves a whole
|
|
27404
|
+
* paragraph of other text there instead, and is rejected.
|
|
27405
|
+
*/
|
|
27406
|
+
function isProtocolHeader(between) {
|
|
27407
|
+
const trimmed = between.replace(/\n+$/u, "");
|
|
27408
|
+
if (trimmed === "") return false;
|
|
27409
|
+
return !trimmed.includes("\n\n");
|
|
26949
27410
|
}
|
|
26950
27411
|
/**
|
|
26951
|
-
* The
|
|
26952
|
-
* identified safely.
|
|
27412
|
+
* The protocol span that begins at or after `from`, or undefined.
|
|
26953
27413
|
*
|
|
26954
|
-
*
|
|
26955
|
-
*
|
|
26956
|
-
*
|
|
26957
|
-
*
|
|
27414
|
+
* The close marker may itself appear inside a JSON string of an older message
|
|
27415
|
+
* (written before `<` was escaped), so the scan accepts the first close marker
|
|
27416
|
+
* that leaves a JSON payload the composer's own reader would accept. A block
|
|
27417
|
+
* whose payload is unreadable is still hidden as long as both markers are
|
|
27418
|
+
* present — protocol text must never reach the transcript, valid or not.
|
|
26958
27419
|
*/
|
|
26959
|
-
function
|
|
26960
|
-
const
|
|
26961
|
-
|
|
27420
|
+
function nextProtocolSpan(text, from) {
|
|
27421
|
+
const open = text.indexOf(BLOCK_OPEN, from);
|
|
27422
|
+
if (open < 0) return void 0;
|
|
27423
|
+
const payloadStart = open + 22;
|
|
27424
|
+
let close = text.indexOf(BLOCK_CLOSE, payloadStart);
|
|
27425
|
+
let end = -1;
|
|
27426
|
+
while (close >= 0) {
|
|
27427
|
+
const payload = text.slice(payloadStart, close).trim().replace(/\\u003c/gu, "<");
|
|
27428
|
+
try {
|
|
27429
|
+
JSON.parse(payload);
|
|
27430
|
+
end = close + 23;
|
|
27431
|
+
break;
|
|
27432
|
+
} catch {
|
|
27433
|
+
close = text.indexOf(BLOCK_CLOSE, close + 23);
|
|
27434
|
+
}
|
|
27435
|
+
}
|
|
27436
|
+
if (end < 0) {
|
|
27437
|
+
const firstClose = text.indexOf(BLOCK_CLOSE, payloadStart);
|
|
27438
|
+
if (firstClose < 0) return void 0;
|
|
27439
|
+
end = firstClose + 23;
|
|
27440
|
+
}
|
|
27441
|
+
const header = text.lastIndexOf(HEADER_MARKER, open);
|
|
27442
|
+
let start = open;
|
|
27443
|
+
if (header >= from && isProtocolHeader(text.slice(header + 23, open))) start = header;
|
|
27444
|
+
let trimmed = end;
|
|
27445
|
+
while (trimmed < text.length && (text[trimmed] === "\n" || text[trimmed] === "\r")) trimmed += 1;
|
|
27446
|
+
return {
|
|
27447
|
+
start,
|
|
27448
|
+
end: trimmed
|
|
27449
|
+
};
|
|
27450
|
+
}
|
|
27451
|
+
/**
|
|
27452
|
+
* Build the rule. `data.text` must equal the claimed substring byte for byte —
|
|
27453
|
+
* the host validates `text` nodes against `text.slice(start, end)`; `display`
|
|
27454
|
+
* is what actually renders, and an empty display hides the block.
|
|
27455
|
+
*/
|
|
27456
|
+
function createProtocolRule() {
|
|
27457
|
+
return {
|
|
27458
|
+
id: PROTOCOL_RULE_ID,
|
|
27459
|
+
order: PROTOCOL_RULE_ORDER,
|
|
27460
|
+
match(text, from) {
|
|
27461
|
+
const span = nextProtocolSpan(text, from);
|
|
27462
|
+
if (span === void 0) return void 0;
|
|
27463
|
+
return {
|
|
27464
|
+
id: "text",
|
|
27465
|
+
start: span.start,
|
|
27466
|
+
end: span.end,
|
|
27467
|
+
data: {
|
|
27468
|
+
text: text.slice(span.start, span.end),
|
|
27469
|
+
display: ""
|
|
27470
|
+
}
|
|
27471
|
+
};
|
|
27472
|
+
}
|
|
27473
|
+
};
|
|
26962
27474
|
}
|
|
26963
27475
|
//#endregion
|
|
26964
27476
|
//#region src/client/state/annotation-persistence.ts
|
|
@@ -27223,13 +27735,13 @@ function createAnnotationPersistence() {
|
|
|
27223
27735
|
* Selection toolbar is root-scoped (`shell.overlay`) so transcript selections
|
|
27224
27736
|
* are always observed. Annotation summary stays on the session composer.
|
|
27225
27737
|
*
|
|
27226
|
-
* History summary
|
|
27227
|
-
* `
|
|
27228
|
-
* `
|
|
27229
|
-
*
|
|
27230
|
-
*
|
|
27231
|
-
*
|
|
27232
|
-
*
|
|
27738
|
+
* History summary mounts on the Host's own decoration seat
|
|
27739
|
+
* (`conversation.chat.user.before`) rather than by overriding a Chat node:
|
|
27740
|
+
* overriding the `user` node shadowed the Host's `UserMessageNodeView`, which
|
|
27741
|
+
* then left the registry and could not be delegated to — the message lost its
|
|
27742
|
+
* time/copy actions row. The seat keeps the bubble Host-owned; the card only
|
|
27743
|
+
* reads the message body through the `useChat` snapshot (see
|
|
27744
|
+
* `transcript/UserBeforeCard.tsx`).
|
|
27233
27745
|
*
|
|
27234
27746
|
* Language is the host's decision, never this plugin's: dictionaries go to
|
|
27235
27747
|
* `ctx.locale`, and `t()` resolves against whatever language the user picked in
|
|
@@ -27246,6 +27758,35 @@ function createAnnotationPersistence() {
|
|
|
27246
27758
|
const inject = ["slots", "locale"];
|
|
27247
27759
|
function apply(ctx) {
|
|
27248
27760
|
ensureToolbarStyles();
|
|
27761
|
+
try {
|
|
27762
|
+
applyBody(ctx);
|
|
27763
|
+
} catch (error) {
|
|
27764
|
+
console.error("dsh-select-quote: activation failed", error);
|
|
27765
|
+
throw error;
|
|
27766
|
+
}
|
|
27767
|
+
}
|
|
27768
|
+
/**
|
|
27769
|
+
* Annotation cards hang on the Host's own user-message decoration seat.
|
|
27770
|
+
*
|
|
27771
|
+
* `conversation.chat.user.before` is the official place for message-source
|
|
27772
|
+
* decoration in dsh 0.2.2, and using it is what keeps the bubble Host-owned:
|
|
27773
|
+
* the message keeps its attachments, reference summary and time/copy actions
|
|
27774
|
+
* row, and this plugin only adds the summary above it.
|
|
27775
|
+
*
|
|
27776
|
+
* The entry receives `{source, kind}` — no message body — so the card resolves
|
|
27777
|
+
* the body through the `useChat` seat (see `transcript/UserBeforeCard.tsx`),
|
|
27778
|
+
* matching the durable node by `source.rpcId`.
|
|
27779
|
+
*/
|
|
27780
|
+
function registerHistoryCard(ctx) {
|
|
27781
|
+
ctx.slots.inject("conversation.chat.user.before", () => ctx.slots.register({
|
|
27782
|
+
name: "conversation.chat.user.before",
|
|
27783
|
+
select: (owner) => {
|
|
27784
|
+
return owner?.kind === "user" ? owner : null;
|
|
27785
|
+
}
|
|
27786
|
+
}, UserBeforeCard));
|
|
27787
|
+
}
|
|
27788
|
+
function applyBody(ctx) {
|
|
27789
|
+
registerHistoryCard(ctx);
|
|
27249
27790
|
const hostLanguageIds = new Set(ctx.locale.getSnapshot().locales.map((language) => language.id.toLowerCase()));
|
|
27250
27791
|
for (const language of OPTIONAL_LANGUAGES) {
|
|
27251
27792
|
if (hostLanguageIds.has(language.id)) continue;
|
|
@@ -27359,11 +27900,12 @@ function apply(ctx) {
|
|
|
27359
27900
|
let publishedTranslate;
|
|
27360
27901
|
const publish = () => {
|
|
27361
27902
|
const api = scope.get("remote.selectQuoteSideTask");
|
|
27362
|
-
|
|
27363
|
-
if (api !== void 0 && sidebar !== void 0) {
|
|
27903
|
+
if (api !== void 0) {
|
|
27364
27904
|
const openSideTask = async (snapshot, question, model) => {
|
|
27365
27905
|
const sessionId = runtime.sessionId;
|
|
27366
27906
|
if (!sessionId) throw new Error("no active session");
|
|
27907
|
+
const sidebar = scope.get("sidebarRight");
|
|
27908
|
+
if (sidebar === void 0) throw new Error("the right sidebar is unavailable");
|
|
27367
27909
|
const source = snapshot.source;
|
|
27368
27910
|
const prompt = composeQuotedMessage(question, {
|
|
27369
27911
|
text: snapshot.text,
|
|
@@ -27465,6 +28007,13 @@ function apply(ctx) {
|
|
|
27465
28007
|
});
|
|
27466
28008
|
ctx.effect(() => watchAnnotationDirectives(), "dsh-select-quote: annotation directives");
|
|
27467
28009
|
ctx.effect(() => watchAnnotationMarks(), "dsh-select-quote: annotation marks");
|
|
28010
|
+
ctx.effect(() => {
|
|
28011
|
+
if (typeof document === "undefined" || typeof window === "undefined") return () => {};
|
|
28012
|
+
const relayout = () => relayoutAnnotationMarks();
|
|
28013
|
+
window.addEventListener("resize", relayout);
|
|
28014
|
+
document.fonts?.ready.then(relayout).catch(() => {});
|
|
28015
|
+
return () => window.removeEventListener("resize", relayout);
|
|
28016
|
+
}, "dsh-select-quote: mark badge relayout");
|
|
27468
28017
|
ctx.effect(() => bindAnnotationMarkPreview(), "dsh-select-quote: mark preview");
|
|
27469
28018
|
ctx.effect(() => watchSubmissionEcho(), "dsh-select-quote: submission echo");
|
|
27470
28019
|
ctx.effect(() => {
|
|
@@ -27495,6 +28044,11 @@ function apply(ctx) {
|
|
|
27495
28044
|
setAnnotationPersistence(null);
|
|
27496
28045
|
};
|
|
27497
28046
|
}, "dsh-select-quote: annotation persistence");
|
|
28047
|
+
ctx.inject(["uiConversation"], (scope) => {
|
|
28048
|
+
const rules = scope.uiConversation?.userText;
|
|
28049
|
+
if (typeof rules?.register !== "function") return;
|
|
28050
|
+
scope.effect(() => rules.register(createProtocolRule()), "dsh-select-quote: user-text rule");
|
|
28051
|
+
});
|
|
27498
28052
|
ctx.slots.inject("shell.overlay", () => {
|
|
27499
28053
|
ctx.slots.register({
|
|
27500
28054
|
name: "shell.overlay",
|
|
@@ -27516,29 +28070,6 @@ function apply(ctx) {
|
|
|
27516
28070
|
order: 1
|
|
27517
28071
|
}, SessionBridge);
|
|
27518
28072
|
});
|
|
27519
|
-
const hostNodeOf = (key, self) => {
|
|
27520
|
-
return pickHostNode(typeof ctx.slots.entries === "function" ? ctx.slots.entries("conversation.chat.node") : [], self, key);
|
|
27521
|
-
};
|
|
27522
|
-
function UserNode(props) {
|
|
27523
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMessageDisplay, {
|
|
27524
|
-
...props,
|
|
27525
|
-
hostNodeOf: () => hostNodeOf("user", UserNode)
|
|
27526
|
-
});
|
|
27527
|
-
}
|
|
27528
|
-
function SteeringNode(props) {
|
|
27529
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMessageDisplay, {
|
|
27530
|
-
...props,
|
|
27531
|
-
hostNodeOf: () => hostNodeOf("steering", SteeringNode)
|
|
27532
|
-
});
|
|
27533
|
-
}
|
|
27534
|
-
for (const [key, View] of [["user", UserNode], ["steering", SteeringNode]]) ctx.slots.inject("conversation.chat.node", () => {
|
|
27535
|
-
ctx.slots.register({
|
|
27536
|
-
name: "conversation.chat.node",
|
|
27537
|
-
key,
|
|
27538
|
-
priority: -10,
|
|
27539
|
-
locale: "chat"
|
|
27540
|
-
}, View);
|
|
27541
|
-
});
|
|
27542
28073
|
}
|
|
27543
28074
|
/** Invisible session-scoped bridge that publishes sessionId for the root toolbar. */
|
|
27544
28075
|
function SessionBridge({ sessionId }) {
|