dsh-recall-plugin 2.4.15 → 2.5.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/CHANGELOG.md +35 -0
- package/README.en.md +9 -1
- package/README.md +9 -1
- package/lib/client.js +1279 -134
- package/lib/config.js +6 -1
- package/lib/routes-core.js +1 -1
- package/lib/routes-manage.js +2 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
// primary 底色上文本随主题翻转,见 plan-settings-ui V1 核验记录)。
|
|
8
8
|
":root{--dsh-recall-btn-danger-hover:brightness(1.08);--dsh-recall-tree-indent:24px;--dsh-recall-space-1:4px;--dsh-recall-space-2:8px;--dsh-recall-space-3:12px;--dsh-recall-input-w:120px;--dsh-recall-switch-w:36px}",
|
|
9
9
|
".dsh-recall-row{flex-direction:column;align-items:flex-end;gap:6px;display:flex}",
|
|
10
|
+
// 新面(factory)本层包裹官方 UserContent,官方 `.userStack` 自带
|
|
11
|
+
// max-width:min(calc(chat*0.702),82%);外层再限一次即双层收缩,故全宽只在
|
|
12
|
+
// 官方面放开。legacy 自绘面(0.1.x / factory 回落)仍靠原 cap 约束长文本。
|
|
10
13
|
".dsh-recall-stack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}",
|
|
14
|
+
".dsh-recall-stack-official{width:100%;max-width:100%}",
|
|
11
15
|
".dsh-recall-bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:1.5;white-space:pre-wrap;word-break:break-word}",
|
|
12
16
|
".dsh-recall-json{margin:0;max-width:100%;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;background:var(--dsw-alias-markdown-code-block)}",
|
|
13
17
|
// 文件卡片(消息里的 file 块,非图片附件):复刻官方 UserStyleBubble 的附件卡
|
|
@@ -26,6 +30,88 @@
|
|
|
26
30
|
".dsh-recall-panel{width:min(480px,100%);box-sizing:border-box;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;text-align:left;box-shadow:0 8px 28px rgba(0,0,0,.22)}",
|
|
27
31
|
".dsh-recall-panel-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:1.5}",
|
|
28
32
|
".dsh-recall-panel-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5;word-break:break-word}",
|
|
33
|
+
// 消息位置的编辑卡(issue #26):官方不允许第二个 composer 实例,这张卡是
|
|
34
|
+
// 插件自绘的「官方同款」——外框逐字对齐官方 composer 卡片(panel 圆角 /
|
|
35
|
+
// input-major 底 / soft 阴影 / 14-24 字号行高),底部官方输入框保持原样
|
|
36
|
+
".dsh-recall-stack-editing{width:100%;max-width:100%}",
|
|
37
|
+
".dsh-recall-composer{width:100%;box-sizing:border-box;display:flex;flex-direction:column;gap:10px;padding:10px 12px 8px;border-radius:var(--dsw-radius-panel);background:var(--dsw-specific-input-major);box-shadow:var(--dsw-elevation-soft);font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px))}",
|
|
38
|
+
// 卡内文本区:透明无环(外框已给足层次),随内容增高到 260px 后内部滚动;
|
|
39
|
+
// 高度由 edit-node 按 scrollHeight 写成内联样式
|
|
40
|
+
".dsh-recall-composer .dsh-recall-edit-area{width:100%;box-sizing:border-box;min-height:48px;max-height:260px;padding:2px 2px 0;background:0 0;color:var(--dsw-alias-label-primary);border:0;border-radius:0;font-size:inherit;line-height:inherit;font-family:inherit;resize:none;overflow-y:auto;box-shadow:none}",
|
|
41
|
+
// 官方输入区聚焦时没有边框/描边变化(只有光标),编辑区跟随
|
|
42
|
+
".dsh-recall-edit-area:focus,.dsh-recall-edit-area:focus-visible{outline:none;border:0}",
|
|
43
|
+
".dsh-recall-edit-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:1.5}",
|
|
44
|
+
".dsh-recall-composer-attachments{display:flex;flex-wrap:wrap;gap:6px}",
|
|
45
|
+
".dsh-recall-composer-attach{display:inline-flex;align-items:center;gap:2px;max-width:100%;padding:2px 4px 2px 10px;border-radius:999px;corner-shape:round;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}",
|
|
46
|
+
".dsh-recall-composer-attach-name{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
47
|
+
".dsh-recall-composer-attach-x{border:0;background:0;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;line-height:1;padding:2px 6px;border-radius:999px;corner-shape:round}",
|
|
48
|
+
".dsh-recall-composer-attach-x:hover{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}",
|
|
49
|
+
// 图片附件:固定 56px 缩略图,删除键做成角标,避免名字挤占宽度
|
|
50
|
+
".dsh-recall-composer-attach-image{position:relative;width:56px;height:56px;padding:0;border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-3)}",
|
|
51
|
+
".dsh-recall-composer-thumb{display:block;width:100%;height:100%;object-fit:cover}",
|
|
52
|
+
".dsh-recall-composer-attach-x-badge{position:absolute;top:2px;right:2px;width:18px;height:18px;padding:0;display:grid;place-items:center;font-size:12px;line-height:1;color:#fff;background:rgba(0,0,0,.55)}",
|
|
53
|
+
".dsh-recall-composer-attach-x-badge:hover{background:var(--dsw-alias-state-error-primary);color:#fff}",
|
|
54
|
+
// 底行:加号 → 只读 chip → 提示 → 发送。发送键对齐官方(圆形 brand 底)
|
|
55
|
+
".dsh-recall-composer-foot{display:flex;align-items:center;gap:8px;min-height:32px}",
|
|
56
|
+
".dsh-recall-composer-add{position:relative;flex:none;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:50%;corner-shape:round;background:0 0;color:var(--dsw-alias-label-secondary);cursor:pointer}",
|
|
57
|
+
".dsh-recall-composer-add:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
58
|
+
'.dsh-recall-composer-add[aria-disabled="true"]{opacity:.4;cursor:default}',
|
|
59
|
+
// 视觉隐藏但可聚焦:label 点击开选择框,键盘也能 Tab 进来
|
|
60
|
+
".dsh-recall-composer-file{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}",
|
|
61
|
+
".dsh-recall-composer-chip{flex:none;max-width:180px;padding:2px 8px;border-radius:999px;corner-shape:round;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
62
|
+
// 模型 chip 是按钮:菜单锚在它上方,打开时不改变底行高度
|
|
63
|
+
".dsh-recall-composer-model{position:relative;flex:none;display:inline-flex;min-width:0}",
|
|
64
|
+
// chip 内部与官方 trigger 同构:模型名(常规色)+ 档位(caption 色)+ chevron
|
|
65
|
+
// 静止态不加底色(官方 trigger 同样透明),只在 hover/展开时给一层 hover 底
|
|
66
|
+
".dsh-recall-composer-chip-btn{display:inline-flex;align-items:center;gap:4px;height:28px;max-width:260px;padding:0 6px 0 10px;border:0;font-family:inherit;cursor:pointer;background:0 0}",
|
|
67
|
+
".dsh-recall-composer-chip-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
|
|
68
|
+
".dsh-recall-composer-chip-btn.is-open{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
69
|
+
".dsh-recall-composer-chip-btn:disabled{opacity:.6;cursor:default}",
|
|
70
|
+
".dsh-recall-composer-chip-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary)}",
|
|
71
|
+
".dsh-recall-composer-chip-effort{flex:none;color:var(--dsw-alias-label-caption)}",
|
|
72
|
+
".dsh-recall-composer-chip-chevron{flex:none;display:grid;place-items:center;color:var(--dsw-alias-label-caption);transition:transform .12s}",
|
|
73
|
+
".dsh-recall-composer-chip-btn.is-open .dsh-recall-composer-chip-chevron{transform:rotate(180deg)}",
|
|
74
|
+
// 模型菜单:度量对齐官方(4px 内边距、34px 行高、13px 名字、md 圆角、prominent 阴影)。
|
|
75
|
+
// 底色用不透明 bg-base——官方那层半透明材质是构建期哈希类名,跨版本不可依赖
|
|
76
|
+
".dsh-recall-menu{position:absolute;left:0;bottom:calc(100% + 6px);z-index:7;display:flex;flex-direction:column;width:max-content;min-width:min(240px,100vw - 32px);max-width:min(420px,100vw - 32px);max-height:min(360px,100vh - 96px);padding:4px;border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-base);box-shadow:var(--dsw-elevation-prominent);overflow:hidden}",
|
|
77
|
+
".dsh-recall-menu-cell{display:flex;align-items:center;gap:6px;width:100%;height:34px;padding:0 8px;border:0;border-radius:var(--dsw-radius-md);background:0 0;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;text-align:left;cursor:pointer}",
|
|
78
|
+
".dsh-recall-menu-cell:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
79
|
+
".dsh-recall-menu-cell-label{flex:none;white-space:nowrap}",
|
|
80
|
+
".dsh-recall-menu-cell-value{margin-left:auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:13px}",
|
|
81
|
+
".dsh-recall-menu-cell-chevron{flex:none;display:grid;place-items:center;color:var(--dsw-alias-menu-icon,var(--dsw-alias-label-tertiary))}",
|
|
82
|
+
".dsh-recall-menu-search-row{flex:none;margin:2px 0 3px}",
|
|
83
|
+
".dsh-recall-menu-search{display:flex;align-items:center;padding:5px 7px;border-radius:var(--dsw-radius-md)}",
|
|
84
|
+
".dsh-recall-menu-search input{flex:1;min-width:0;border:0;background:0 0;outline:none;font-family:inherit;font-size:12px;color:var(--dsw-alias-label-primary)}",
|
|
85
|
+
".dsh-recall-menu-search input::placeholder{color:var(--dsw-alias-label-caption)}",
|
|
86
|
+
".dsh-recall-menu-groups{min-height:0;overflow-y:auto}",
|
|
87
|
+
// 分组标题吸顶:长列表滚动时保持「当前是哪家」可见,与官方一致
|
|
88
|
+
".dsh-recall-menu-heading{position:sticky;top:0;z-index:1;padding:4px 7px 2px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
89
|
+
".dsh-recall-menu-option{display:flex;align-items:center;gap:6px;width:100%;min-height:34px;padding:5px 7px;border:0;border-radius:var(--dsw-radius-md);background:0 0;color:inherit;font-size:13px;line-height:18px;text-align:left;cursor:pointer}",
|
|
90
|
+
".dsh-recall-menu-option:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
|
|
91
|
+
".dsh-recall-menu-option:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}",
|
|
92
|
+
".dsh-recall-menu-option-active{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
93
|
+
".dsh-recall-menu-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
94
|
+
".dsh-recall-menu-check{flex:0 0 14px;display:grid;place-items:center;color:var(--dsw-alias-label-primary)}",
|
|
95
|
+
".dsh-recall-menu-empty{padding:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
|
|
96
|
+
".dsh-recall-composer-hint{margin-left:auto;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
97
|
+
".dsh-recall-composer-send{flex:none;width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:50%;corner-shape:round;background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground);cursor:pointer}",
|
|
98
|
+
".dsh-recall-composer-send:disabled{opacity:.4;cursor:default}",
|
|
99
|
+
".dsh-recall-composer-dot{width:8px;height:8px;border-radius:50%;corner-shape:round;background:currentColor;animation:dsh-recall-pulse 1.2s ease-in-out infinite}",
|
|
100
|
+
// 窄屏:提示让位,否则 chip 与发送键会被挤到换行
|
|
101
|
+
"@media (max-width:480px){.dsh-recall-composer-hint{display:none}.dsh-recall-composer-chip{max-width:120px}}",
|
|
102
|
+
// 编辑态隐藏底部输入框的统计 dock(回合/步骤、tok/s、缓存、Trace):编辑焦点
|
|
103
|
+
// 在消息处的卡片上,dock 属全局概览,留着只会分心;挂在既有的 data-editing 上
|
|
104
|
+
'body:has(.dsh-recall-row[data-editing="true"]) [data-composer-dock]{display:none}',
|
|
105
|
+
// 版本翻页器(‹ X/N ›):灰字 + 无边框的轻量控件,附在回合操作区末尾,
|
|
106
|
+
// 视觉重量低于复制/重试等官方动作,不抢注意力
|
|
107
|
+
".dsh-recall-pager{display:inline-flex;align-items:center;gap:2px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1}",
|
|
108
|
+
".dsh-recall-pager-btn{border:0;background:0;color:inherit;cursor:pointer;padding:3px 6px;border-radius:6px;font-size:13px;line-height:1}",
|
|
109
|
+
".dsh-recall-pager-btn:hover:not(:disabled){color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
110
|
+
".dsh-recall-pager-btn:disabled{opacity:.35;cursor:default}",
|
|
111
|
+
".dsh-recall-pager-count{min-width:30px;text-align:center;font-variant-numeric:tabular-nums}",
|
|
112
|
+
".dsh-recall-pager[data-busy=true]{opacity:.6;pointer-events:none}",
|
|
113
|
+
// 无家族可翻时容器是空的::empty 让占位为 0,不在操作区留一段空隙
|
|
114
|
+
".dsh-recall-pager:empty{display:none}",
|
|
29
115
|
".dsh-recall-list{max-height:220px;overflow:auto;display:flex;flex-direction:column;gap:2px;padding:4px 0}",
|
|
30
116
|
".dsh-recall-file{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:1.5}",
|
|
31
117
|
".dsh-recall-badge{flex:none;font-size:12px;line-height:1.5;padding:0 6px;border-radius:6px}",
|
|
@@ -377,6 +463,39 @@
|
|
|
377
463
|
"action.copied": "\u5DF2\u590D\u5236",
|
|
378
464
|
"action.recall": "\u64A4\u56DE",
|
|
379
465
|
"action.recall.title": "\u6574\u6BB5\u56DE\u9000\uFF1A\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u4E00\u5E76\u56DE\u5230\u8BE5\u6D88\u606F\u4E4B\u524D",
|
|
466
|
+
"action.edit": "\u7F16\u8F91",
|
|
467
|
+
"action.edit.title": "\u7F16\u8F91\u5E76\u91CD\u53D1\uFF1A\u5C31\u5730\u6539\u5B8C\u518D\u53D1\u51FA\u53BB\uFF08\u65B0\u5EFA\u4F1A\u8BDD\uFF0C\u4E0D\u52A8\u9879\u76EE\u6587\u4EF6\uFF09",
|
|
468
|
+
// ---- 就地编辑面板(issue #26)----
|
|
469
|
+
"edit.title": "\u7F16\u8F91\u5E76\u91CD\u53D1",
|
|
470
|
+
"edit.note": "\u63D0\u4EA4\u540E\u4F1A\u65B0\u5EFA\u4F1A\u8BDD\uFF1A\u8BE5\u6D88\u606F\u53CA\u4E4B\u540E\u7684\u5BF9\u8BDD\u4E0D\u518D\u51FA\u73B0\u5728\u65B0\u4F1A\u8BDD\u91CC\uFF0C\u539F\u4F1A\u8BDD\u5F52\u6863\u4FDD\u5B58\u3002\u9879\u76EE\u6587\u4EF6\u4E0D\u4F1A\u88AB\u6539\u52A8\u3002",
|
|
471
|
+
"edit.attachments": "\u539F\u6D88\u606F\u7684 {n} \u4E2A\u9644\u4EF6\u4F1A\u4E00\u5E76\u5E26\u5165\u65B0\u6D88\u606F\u3002",
|
|
472
|
+
"edit.placeholder": "\u6D88\u606F\u5185\u5BB9",
|
|
473
|
+
"edit.submit": "\u7F16\u8F91\u5E76\u53D1\u9001",
|
|
474
|
+
"edit.submit.draftOnly": "\u7F16\u8F91\u5E76\u56DE\u586B",
|
|
475
|
+
"edit.submitting": "\u6B63\u5728\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\u2026",
|
|
476
|
+
"edit.retry": "\u91CD\u8BD5",
|
|
477
|
+
"edit.error.title": "\u65E0\u6CD5\u7F16\u8F91\u5E76\u91CD\u53D1",
|
|
478
|
+
"edit.back": "\u8FD4\u56DE\u4FEE\u6539",
|
|
479
|
+
"edit.noCut": "\u8BE5\u6D88\u606F\u662F\u672C\u4F1A\u8BDD\u4E2D\u7B2C\u4E00\u6761\u7528\u6237\u6D88\u606F\uFF0C\u6CA1\u6709\u53EF\u56DE\u9000\u7684\u5BF9\u8BDD\u5207\u70B9\uFF0C\u65E0\u6CD5\u7F16\u8F91\u91CD\u53D1\u3002",
|
|
480
|
+
"edit.forkFailed": "\u5BF9\u8BDD\u672A\u80FD\u56DE\u9000\uFF08\u672A\u521B\u5EFA\u65B0\u4F1A\u8BDD\uFF09\uFF0C\u7F16\u8F91\u91CD\u53D1\u5DF2\u4E2D\u6B62\u3002",
|
|
481
|
+
"edit.empty": "\u6D88\u606F\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A\u3002",
|
|
482
|
+
// 官方输入框锚定面(issue #26 二次迭代):输入面就是官方 composer 本身,
|
|
483
|
+
// 这里只提示两条键盘出口,不重复画输入控件
|
|
484
|
+
"edit.official.hint": "Enter \u53D1\u9001\u5E76\u65B0\u5EFA\u4F1A\u8BDD \xB7 Esc \u53D6\u6D88",
|
|
485
|
+
"edit.official.retry": "Enter \u91CD\u8BD5 \xB7 Esc \u53D6\u6D88\u7F16\u8F91",
|
|
486
|
+
"edit.attachment.add": "\u6DFB\u52A0\u9644\u4EF6",
|
|
487
|
+
"edit.attachment.remove": "\u79FB\u9664\u9644\u4EF6 {name}",
|
|
488
|
+
"edit.model.label": "\u6A21\u578B",
|
|
489
|
+
"edit.model.menu": "\u6A21\u578B\u4E0E\u63A8\u7406\u7B49\u7EA7",
|
|
490
|
+
"edit.model.effort": "\u63A8\u7406\u7B49\u7EA7",
|
|
491
|
+
"edit.model.search": "\u641C\u7D22\u6A21\u578B\u2026",
|
|
492
|
+
"edit.model.empty": "\u6CA1\u6709\u5339\u914D\u7684\u6A21\u578B",
|
|
493
|
+
"edit.model.noLevels": "\u8BE5\u6A21\u578B\u6CA1\u6709\u53EF\u9009\u7B49\u7EA7",
|
|
494
|
+
"edit.model.providerDefault": "Default",
|
|
495
|
+
// ---- 版本翻页器(‹ X/N ›)----
|
|
496
|
+
"pager.prev": "\u4E0A\u4E00\u7248\u672C",
|
|
497
|
+
"pager.next": "\u4E0B\u4E00\u7248\u672C",
|
|
498
|
+
"pager.label": "\u7248\u672C {n}/{total}",
|
|
380
499
|
// ---- toast(降级提示 / 快照反馈)----
|
|
381
500
|
"toast.tag": "\u64A4\u56DE\u63D2\u4EF6",
|
|
382
501
|
"toast.skipped": "\u5FEB\u7167\u5DF2\u8DF3\u8FC7\u672A\u7EB3\u5165\u7684\u8DEF\u5F84\uFF1A{names}\uFF08\u64A4\u56DE\u4E0D\u4F1A\u6062\u590D\u6216\u5220\u9664\u8FD9\u4E9B\u8DEF\u5F84\uFF09",
|
|
@@ -384,6 +503,7 @@
|
|
|
384
503
|
"toast.snapshotFailed": "\u5FEB\u7167\u5931\u8D25\uFF1A{error}",
|
|
385
504
|
"toast.staleQueue": "\u64A4\u56DE\u524D\u7684\u4E00\u6761\u6392\u961F\u6D88\u606F\u672A\u88AB\u81EA\u52A8\u6E05\u7406\uFF0C\u53EF\u70B9\u51FB\u8BE5\u5361\u7247\u53F3\u4E0A\u89D2\u7684\u5220\u9664\u6309\u94AE\u624B\u52A8\u79FB\u9664",
|
|
386
505
|
"toast.fileAttachRefill": "\u88AB\u64A4\u56DE\u6D88\u606F\u91CC\u7684\u6587\u4EF6\u9644\u4EF6\u65E0\u6CD5\u81EA\u52A8\u56DE\u586B\uFF08\u5B98\u65B9\u63A5\u53E3\u53EA\u652F\u6301\u56FE\u7247\u56DE\u8BFB\uFF09\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9\u6587\u4EF6",
|
|
506
|
+
"toast.pagerFailed": "\u5207\u6362\u7248\u672C\u5931\u8D25\uFF1A{error}",
|
|
387
507
|
// ---- init / snapshot-info 下发的一次性说明 ----
|
|
388
508
|
"notice.unsupported": "\u64A4\u56DE\u63D2\u4EF6\u4EC5\u652F\u6301 Windows / Linux / macOS\uFF0C\u5F53\u524D\u5E73\u53F0\u7684\u5FEB\u7167\u4E0D\u53EF\u7528\u3002",
|
|
389
509
|
"notice.gitMissing": "\u672A\u68C0\u6D4B\u5230 git CLI\uFF0C\u64A4\u56DE\u529F\u80FD\u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09\u3002\u5B89\u88C5 git \u5E76\u91CD\u542F DSH \u540E\u5373\u53EF\u4F7F\u7528\u3002",
|
|
@@ -455,6 +575,8 @@
|
|
|
455
575
|
"config.refillDraft.hint": "\u64A4\u56DE\u540E\u628A\u6D88\u606F\u6587\u672C\u56DE\u586B\u8F93\u5165\u6846\uFF0C\u4FBF\u4E8E\u6539\u5B8C\u91CD\u53D1",
|
|
456
576
|
"config.archiveOriginal.label": "\u5F52\u6863\u539F\u4F1A\u8BDD",
|
|
457
577
|
"config.archiveOriginal.hint": "\u539F\u4F1A\u8BDD\u5F52\u6863\u9690\u85CF\uFF0C\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF1B\u5173\u95ED\u5219\u7559\u5728\u5217\u8868\u4FBF\u4E8E\u5BF9\u7167",
|
|
578
|
+
"config.editSubmitsImmediately.label": "\u7F16\u8F91\u540E\u81EA\u52A8\u53D1\u9001",
|
|
579
|
+
"config.editSubmitsImmediately.hint": "\u300C\u7F16\u8F91\u5E76\u91CD\u53D1\u300D\u63D0\u4EA4\u540E\u76F4\u63A5\u53D1\u51FA\u6539\u540E\u7684\u6D88\u606F\uFF1B\u5173\u95ED\u5219\u53EA\u56DE\u586B\u8F93\u5165\u6846\uFF0C\u7531\u4F60\u6309\u56DE\u8F66\u53D1\u9001",
|
|
458
580
|
"config.gcSnaps.label": "gc \u89E6\u53D1\u6761\u6570",
|
|
459
581
|
"config.gcSnaps.hint": "\u6BCF\u79EF\u7D2F\u591A\u5C11\u6761\u5FEB\u7167\u89E6\u53D1\u4E00\u6B21 gc",
|
|
460
582
|
"config.gcSnaps.suffix": "\u6761",
|
|
@@ -597,6 +719,37 @@
|
|
|
597
719
|
"action.copied": "Copied",
|
|
598
720
|
"action.recall": "Recall",
|
|
599
721
|
"action.recall.title": "Roll back: files and chat return to before this message",
|
|
722
|
+
"action.edit": "Edit",
|
|
723
|
+
"action.edit.title": "Edit and resend in place (new session, project files untouched)",
|
|
724
|
+
// ---- 就地编辑面板(issue #26)----
|
|
725
|
+
"edit.title": "Edit and resend",
|
|
726
|
+
"edit.note": "Submitting creates a new session: this message and everything after it will not appear there, and the original session is archived. Project files are left untouched.",
|
|
727
|
+
"edit.attachments": "{n} attachment(s) from the original message will be carried over.",
|
|
728
|
+
"edit.placeholder": "Message text",
|
|
729
|
+
"edit.submit": "Edit and send",
|
|
730
|
+
"edit.submit.draftOnly": "Edit and refill",
|
|
731
|
+
"edit.submitting": "Rolling back to before this message\u2026",
|
|
732
|
+
"edit.retry": "Retry",
|
|
733
|
+
"edit.error.title": "Could not edit and resend",
|
|
734
|
+
"edit.back": "Back to editing",
|
|
735
|
+
"edit.noCut": "This is the first user message in the session, so there is no earlier turn boundary to fork from.",
|
|
736
|
+
"edit.forkFailed": "The chat could not be rolled back (no new session was created), so the edit was not sent.",
|
|
737
|
+
"edit.empty": "Message text cannot be empty.",
|
|
738
|
+
"edit.official.hint": "Enter sends into a new session \xB7 Esc cancels",
|
|
739
|
+
"edit.official.retry": "Enter to retry \xB7 Esc to cancel",
|
|
740
|
+
"edit.attachment.add": "Add attachment",
|
|
741
|
+
"edit.attachment.remove": "Remove attachment {name}",
|
|
742
|
+
"edit.model.label": "Model",
|
|
743
|
+
"edit.model.menu": "Model and reasoning effort",
|
|
744
|
+
"edit.model.effort": "Reasoning effort",
|
|
745
|
+
"edit.model.search": "Search models\u2026",
|
|
746
|
+
"edit.model.empty": "No matching models",
|
|
747
|
+
"edit.model.noLevels": "No effort levels for this model",
|
|
748
|
+
"edit.model.providerDefault": "Default",
|
|
749
|
+
// ---- 版本翻页器(‹ X/N ›)----
|
|
750
|
+
"pager.prev": "Previous version",
|
|
751
|
+
"pager.next": "Next version",
|
|
752
|
+
"pager.label": "Version {n} of {total}",
|
|
600
753
|
// ---- toast ----
|
|
601
754
|
"toast.tag": "Recall plugin",
|
|
602
755
|
"toast.skipped": "Snapshot skipped unlisted paths: {names} (rollback will not restore or delete them)",
|
|
@@ -604,6 +757,7 @@
|
|
|
604
757
|
"toast.snapshotFailed": "Snapshot failed: {error}",
|
|
605
758
|
"toast.staleQueue": "A queued message from before the recall could not be cleaned up automatically; remove it with the delete button on that card",
|
|
606
759
|
"toast.fileAttachRefill": "File attachments from the recalled message cannot be refilled automatically (the official API only reads images back); please re-select the files",
|
|
760
|
+
"toast.pagerFailed": "Could not switch version: {error}",
|
|
607
761
|
// ---- init / snapshot-info 说明 ----
|
|
608
762
|
"notice.unsupported": "The recall plugin supports Windows / Linux / macOS only; snapshots are unavailable on this platform.",
|
|
609
763
|
"notice.gitMissing": "git CLI not found, so recall is unavailable (the snapshot engine depends on git). Install git and restart DSH to enable it.",
|
|
@@ -675,6 +829,8 @@
|
|
|
675
829
|
"config.refillDraft.hint": "Put the message text back into the input box after a recall, so you can edit and resend",
|
|
676
830
|
"config.archiveOriginal.label": "Archive original session",
|
|
677
831
|
"config.archiveOriginal.hint": "Archive and hide the original session (recoverable from Archive); when off it stays in the list for comparison",
|
|
832
|
+
"config.editSubmitsImmediately.label": "Send after editing",
|
|
833
|
+
"config.editSubmitsImmediately.hint": "Edit and resend sends the changed message right away; when off it only refills the input box so you can press Enter yourself",
|
|
678
834
|
"config.gcSnaps.label": "gc trigger count",
|
|
679
835
|
"config.gcSnaps.hint": "Run gc after this many snapshots accumulate",
|
|
680
836
|
"config.gcSnaps.suffix": "snapshots",
|
|
@@ -897,7 +1053,7 @@
|
|
|
897
1053
|
return String(m || e || fallback);
|
|
898
1054
|
}
|
|
899
1055
|
const noticeShown = /* @__PURE__ */ new Set();
|
|
900
|
-
function mountToast(
|
|
1056
|
+
function mountToast(text2) {
|
|
901
1057
|
if (typeof document === "undefined") return;
|
|
902
1058
|
try {
|
|
903
1059
|
let dismiss2 = function() {
|
|
@@ -914,7 +1070,7 @@
|
|
|
914
1070
|
tag.className = "dsh-recall-toast-tag";
|
|
915
1071
|
tag.textContent = t("toast.tag");
|
|
916
1072
|
const body = document.createElement("span");
|
|
917
|
-
body.textContent =
|
|
1073
|
+
body.textContent = text2;
|
|
918
1074
|
el.appendChild(tag);
|
|
919
1075
|
el.appendChild(body);
|
|
920
1076
|
el.addEventListener("click", () => dismiss2(), { once: true });
|
|
@@ -925,21 +1081,21 @@
|
|
|
925
1081
|
} catch (e) {
|
|
926
1082
|
}
|
|
927
1083
|
}
|
|
928
|
-
function showNotice(kind,
|
|
1084
|
+
function showNotice(kind, text2) {
|
|
929
1085
|
if (noticeShown.has(kind)) return;
|
|
930
1086
|
noticeShown.add(kind);
|
|
931
|
-
mountToast(
|
|
1087
|
+
mountToast(text2);
|
|
932
1088
|
}
|
|
933
1089
|
const toastLastShown = /* @__PURE__ */ new Map();
|
|
934
|
-
function showThrottledToast(
|
|
935
|
-
const key = String(
|
|
1090
|
+
function showThrottledToast(text2) {
|
|
1091
|
+
const key = String(text2).slice(0, 80);
|
|
936
1092
|
const now = Date.now();
|
|
937
1093
|
if (now - (toastLastShown.get(key) || 0) < 10 * 60 * 1e3) return;
|
|
938
1094
|
if (toastLastShown.size > 50) toastLastShown.clear();
|
|
939
1095
|
toastLastShown.set(key, now);
|
|
940
|
-
mountToast(
|
|
1096
|
+
mountToast(text2);
|
|
941
1097
|
}
|
|
942
|
-
const pluginConfig = { refillDraft: true, archiveOriginal: true };
|
|
1098
|
+
const pluginConfig = { refillDraft: true, archiveOriginal: true, editSubmitsImmediately: true };
|
|
943
1099
|
const initMap = /* @__PURE__ */ new Map();
|
|
944
1100
|
function ensureInit(sessionId) {
|
|
945
1101
|
if (!sessionId) return Promise.resolve();
|
|
@@ -950,6 +1106,7 @@
|
|
|
950
1106
|
const cfg = res.config;
|
|
951
1107
|
if (typeof cfg.refillDraft === "boolean") pluginConfig.refillDraft = cfg.refillDraft;
|
|
952
1108
|
if (typeof cfg.archiveOriginal === "boolean") pluginConfig.archiveOriginal = cfg.archiveOriginal;
|
|
1109
|
+
if (typeof cfg.editSubmitsImmediately === "boolean") pluginConfig.editSubmitsImmediately = cfg.editSubmitsImmediately;
|
|
953
1110
|
setLocalePref(cfg.locale);
|
|
954
1111
|
}
|
|
955
1112
|
const notice = res && res.notice;
|
|
@@ -973,17 +1130,17 @@
|
|
|
973
1130
|
initMap.set(sessionId, done);
|
|
974
1131
|
return done;
|
|
975
1132
|
}
|
|
976
|
-
function writeClipboard(
|
|
1133
|
+
function writeClipboard(text2) {
|
|
977
1134
|
try {
|
|
978
1135
|
if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
979
|
-
return navigator.clipboard.writeText(
|
|
1136
|
+
return navigator.clipboard.writeText(text2).then(() => true, () => false);
|
|
980
1137
|
}
|
|
981
1138
|
} catch (e) {
|
|
982
1139
|
}
|
|
983
1140
|
try {
|
|
984
1141
|
if (typeof document !== "undefined" && typeof document.execCommand === "function") {
|
|
985
1142
|
const el = document.createElement("textarea");
|
|
986
|
-
el.value =
|
|
1143
|
+
el.value = text2;
|
|
987
1144
|
el.setAttribute("readonly", "");
|
|
988
1145
|
el.style.position = "fixed";
|
|
989
1146
|
el.style.left = "-9999px";
|
|
@@ -1089,6 +1246,612 @@
|
|
|
1089
1246
|
}
|
|
1090
1247
|
}
|
|
1091
1248
|
|
|
1249
|
+
// src/client/edit-node.ts
|
|
1250
|
+
function buildEditFlow(React, t) {
|
|
1251
|
+
function EditIcon() {
|
|
1252
|
+
return React.createElement(
|
|
1253
|
+
"svg",
|
|
1254
|
+
{ width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
1255
|
+
React.createElement("path", { d: "M10.6 2.9 13.1 5.4 5.8 12.7 3 13.4l.7-2.8 6.9-7.7z" }),
|
|
1256
|
+
React.createElement("path", { d: "M9.4 4.1 11.9 6.6" })
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
function EditPanel(props) {
|
|
1260
|
+
const stage = props.stage;
|
|
1261
|
+
const text2 = stage.stage === "editing" || stage.stage === "error" ? stage.text : "";
|
|
1262
|
+
if (stage.stage === "idle") return null;
|
|
1263
|
+
const error = stage.stage === "error" ? stage.message : "";
|
|
1264
|
+
const busy = stage.stage === "submitting";
|
|
1265
|
+
const empty = text2.trim() === "" && props.attachments.length === 0;
|
|
1266
|
+
const intake = (list) => {
|
|
1267
|
+
if (!list) return;
|
|
1268
|
+
const files = [];
|
|
1269
|
+
for (const file of Array.from(list)) files.push(file);
|
|
1270
|
+
if (files.length) props.onAddFiles(files);
|
|
1271
|
+
};
|
|
1272
|
+
return React.createElement(
|
|
1273
|
+
"div",
|
|
1274
|
+
{
|
|
1275
|
+
className: "dsh-recall-edit dsh-recall-composer",
|
|
1276
|
+
onPaste: (event) => {
|
|
1277
|
+
const files = event.clipboardData ? event.clipboardData.files : null;
|
|
1278
|
+
if (!files || files.length === 0) return;
|
|
1279
|
+
event.preventDefault();
|
|
1280
|
+
intake(files);
|
|
1281
|
+
},
|
|
1282
|
+
onDragOver: (event) => {
|
|
1283
|
+
if (event.dataTransfer && Array.from(event.dataTransfer.types).indexOf("Files") >= 0) event.preventDefault();
|
|
1284
|
+
},
|
|
1285
|
+
onDrop: (event) => {
|
|
1286
|
+
const files = event.dataTransfer ? event.dataTransfer.files : null;
|
|
1287
|
+
if (!files || files.length === 0) return;
|
|
1288
|
+
event.preventDefault();
|
|
1289
|
+
intake(files);
|
|
1290
|
+
}
|
|
1291
|
+
},
|
|
1292
|
+
React.createElement("textarea", {
|
|
1293
|
+
className: "dsh-recall-edit-area",
|
|
1294
|
+
value: text2,
|
|
1295
|
+
// 行数按内容估算(这里不能挂 hooks:EditPanel 是以普通函数被父组件
|
|
1296
|
+
// 调用的,hook 会记到父组件身上导致数量不一致)
|
|
1297
|
+
rows: Math.min(8, Math.max(2, text2.split("\n").length + 1)),
|
|
1298
|
+
autoFocus: true,
|
|
1299
|
+
disabled: busy,
|
|
1300
|
+
placeholder: t("edit.placeholder"),
|
|
1301
|
+
"aria-label": t("edit.placeholder"),
|
|
1302
|
+
onChange: (e) => props.onChange(e.target.value),
|
|
1303
|
+
// Enter 发送、Shift+Enter 换行:与官方 composer 同一套键盘语义;输入法
|
|
1304
|
+
// 组合期间的 Enter 是在选词,必须放行
|
|
1305
|
+
onKeyDown: (e) => {
|
|
1306
|
+
if (e.key !== "Enter" || e.shiftKey) return;
|
|
1307
|
+
if (e.nativeEvent && e.nativeEvent.isComposing) return;
|
|
1308
|
+
e.preventDefault();
|
|
1309
|
+
if (!empty && !busy) props.onSubmit();
|
|
1310
|
+
}
|
|
1311
|
+
}),
|
|
1312
|
+
props.attachments.length > 0 ? React.createElement(
|
|
1313
|
+
"div",
|
|
1314
|
+
{ className: "dsh-recall-composer-attachments" },
|
|
1315
|
+
props.attachments.map((item) => item.previewUrl !== void 0 ? React.createElement(
|
|
1316
|
+
"span",
|
|
1317
|
+
{ className: "dsh-recall-composer-attach dsh-recall-composer-attach-image", key: item.key },
|
|
1318
|
+
React.createElement("img", { className: "dsh-recall-composer-thumb", src: item.previewUrl, alt: item.name, title: item.name }),
|
|
1319
|
+
React.createElement("button", {
|
|
1320
|
+
type: "button",
|
|
1321
|
+
className: "dsh-recall-composer-attach-x dsh-recall-composer-attach-x-badge",
|
|
1322
|
+
"aria-label": t("edit.attachment.remove", { name: item.name }),
|
|
1323
|
+
onClick: () => props.onRemoveAttachment(item.key)
|
|
1324
|
+
}, "\xD7")
|
|
1325
|
+
) : React.createElement(
|
|
1326
|
+
"span",
|
|
1327
|
+
{ className: "dsh-recall-composer-attach", key: item.key },
|
|
1328
|
+
React.createElement("span", { className: "dsh-recall-composer-attach-name", title: item.name }, item.name),
|
|
1329
|
+
React.createElement("button", {
|
|
1330
|
+
type: "button",
|
|
1331
|
+
className: "dsh-recall-composer-attach-x",
|
|
1332
|
+
"aria-label": t("edit.attachment.remove", { name: item.name }),
|
|
1333
|
+
onClick: () => props.onRemoveAttachment(item.key)
|
|
1334
|
+
}, "\xD7")
|
|
1335
|
+
))
|
|
1336
|
+
) : null,
|
|
1337
|
+
error !== "" ? React.createElement("div", { className: "dsh-recall-edit-error", role: "alert" }, error) : null,
|
|
1338
|
+
React.createElement(
|
|
1339
|
+
"div",
|
|
1340
|
+
{ className: "dsh-recall-composer-foot" },
|
|
1341
|
+
// label 包住隐藏的 file input:原生点击即开选择框,不需要 ref 与命令式
|
|
1342
|
+
// 点击;input 用视觉隐藏而不是 display:none,键盘仍能 Tab 到它
|
|
1343
|
+
React.createElement(
|
|
1344
|
+
"label",
|
|
1345
|
+
{
|
|
1346
|
+
className: "dsh-recall-composer-add",
|
|
1347
|
+
title: t("edit.attachment.add"),
|
|
1348
|
+
"aria-label": t("edit.attachment.add"),
|
|
1349
|
+
"aria-disabled": busy ? "true" : void 0
|
|
1350
|
+
},
|
|
1351
|
+
React.createElement(
|
|
1352
|
+
"svg",
|
|
1353
|
+
{ width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", "aria-hidden": true },
|
|
1354
|
+
React.createElement("path", { d: "M8 3.2v9.6M3.2 8h9.6" })
|
|
1355
|
+
),
|
|
1356
|
+
React.createElement("input", {
|
|
1357
|
+
className: "dsh-recall-composer-file",
|
|
1358
|
+
type: "file",
|
|
1359
|
+
multiple: true,
|
|
1360
|
+
disabled: busy,
|
|
1361
|
+
onChange: (e) => {
|
|
1362
|
+
intake(e.target.files);
|
|
1363
|
+
}
|
|
1364
|
+
})
|
|
1365
|
+
),
|
|
1366
|
+
props.meta.permission !== "" ? React.createElement("span", { className: "dsh-recall-composer-chip" }, props.meta.permission) : null,
|
|
1367
|
+
// 模型:目录服务可用时是下拉(选择走官方 selectModel,同步到底部 chip),
|
|
1368
|
+
// 不可用则退回只读 chip(值抄自底部输入框)
|
|
1369
|
+
props.modelLabel !== "" ? React.createElement(
|
|
1370
|
+
"span",
|
|
1371
|
+
{ className: "dsh-recall-composer-model" },
|
|
1372
|
+
React.createElement(
|
|
1373
|
+
"button",
|
|
1374
|
+
{
|
|
1375
|
+
type: "button",
|
|
1376
|
+
// 与官方 trigger 同构:模型名 + 淡色档位 + 尾部 chevron(展开时翻转)
|
|
1377
|
+
className: "dsh-recall-composer-chip dsh-recall-composer-chip-btn" + (props.modelOpen ? " is-open" : ""),
|
|
1378
|
+
"aria-haspopup": "menu",
|
|
1379
|
+
"aria-expanded": props.modelOpen,
|
|
1380
|
+
disabled: props.modelBusy,
|
|
1381
|
+
onClick: props.onToggleModel
|
|
1382
|
+
},
|
|
1383
|
+
React.createElement("span", { className: "dsh-recall-composer-chip-name" }, props.modelLabel),
|
|
1384
|
+
React.createElement(
|
|
1385
|
+
"span",
|
|
1386
|
+
{ className: "dsh-recall-composer-chip-effort" },
|
|
1387
|
+
props.modelEffort !== "" ? props.modelEffort : t("edit.model.providerDefault")
|
|
1388
|
+
),
|
|
1389
|
+
React.createElement(
|
|
1390
|
+
"span",
|
|
1391
|
+
{ className: "dsh-recall-composer-chip-chevron" },
|
|
1392
|
+
React.createElement(
|
|
1393
|
+
"svg",
|
|
1394
|
+
{ width: 12, height: 12, viewBox: "0 0 16 16", fill: "none", "aria-hidden": true },
|
|
1395
|
+
React.createElement("path", { d: "M4 6.2l4 4 4-4", stroke: "currentColor", strokeWidth: 1.2, strokeLinecap: "round", strokeLinejoin: "round" })
|
|
1396
|
+
)
|
|
1397
|
+
)
|
|
1398
|
+
),
|
|
1399
|
+
props.modelNodes
|
|
1400
|
+
) : props.meta.model !== "" ? React.createElement("span", { className: "dsh-recall-composer-chip" }, props.meta.model) : null,
|
|
1401
|
+
// 仅做「这是编辑而非新消息」的说明;细节(新建会话/归档)由提示承担
|
|
1402
|
+
React.createElement(
|
|
1403
|
+
"span",
|
|
1404
|
+
{ className: "dsh-recall-composer-hint" },
|
|
1405
|
+
busy ? t("edit.submitting") : error !== "" ? t("edit.official.retry") : t("edit.official.hint")
|
|
1406
|
+
),
|
|
1407
|
+
React.createElement("button", {
|
|
1408
|
+
type: "button",
|
|
1409
|
+
className: "dsh-recall-composer-send",
|
|
1410
|
+
"aria-label": busy ? t("edit.submitting") : error !== "" ? t("edit.retry") : props.submitLabel,
|
|
1411
|
+
title: busy ? t("edit.submitting") : error !== "" ? t("edit.retry") : props.submitLabel,
|
|
1412
|
+
disabled: busy || error === "" && empty,
|
|
1413
|
+
onClick: props.onSubmit
|
|
1414
|
+
}, busy ? React.createElement("span", { className: "dsh-recall-composer-dot" }) : React.createElement(
|
|
1415
|
+
"svg",
|
|
1416
|
+
{ width: 16, height: 16, viewBox: "0 0 16 16", "aria-hidden": true },
|
|
1417
|
+
React.createElement("path", {
|
|
1418
|
+
d: "M8.31 0.98c.36.07.67.22.95.45.22.18.47.43.72.68l4.73 4.73-1.42 1.41L9 3.96v11.08H7V3.96L2.71 8.25 1.29 6.83l4.73-4.73c.25-.25.49-.49.72-.68.24-.19.55-.39.95-.45.21-.03.41-.02.62.01z",
|
|
1419
|
+
fill: "currentColor"
|
|
1420
|
+
})
|
|
1421
|
+
))
|
|
1422
|
+
)
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
return { EditIcon, EditPanel };
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/client/edit-dismiss.ts
|
|
1429
|
+
function installEditDismiss(doc, opts) {
|
|
1430
|
+
const onKeyDown = (event) => {
|
|
1431
|
+
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
1432
|
+
if (opts.popupSelector !== "" && doc.querySelector(opts.popupSelector) !== null) return;
|
|
1433
|
+
event.preventDefault();
|
|
1434
|
+
event.stopPropagation();
|
|
1435
|
+
opts.onDismiss();
|
|
1436
|
+
};
|
|
1437
|
+
doc.addEventListener("keydown", onKeyDown, true);
|
|
1438
|
+
let disposed = false;
|
|
1439
|
+
return () => {
|
|
1440
|
+
if (disposed) return;
|
|
1441
|
+
disposed = true;
|
|
1442
|
+
doc.removeEventListener("keydown", onKeyDown, true);
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// src/client/model-picker.ts
|
|
1447
|
+
var text = (value) => typeof value === "string" ? value : "";
|
|
1448
|
+
function buildModelPicker(ctx, sessionId) {
|
|
1449
|
+
if (!sessionId) return null;
|
|
1450
|
+
let directory = null;
|
|
1451
|
+
const resolve = () => {
|
|
1452
|
+
if (directory !== null) return directory;
|
|
1453
|
+
try {
|
|
1454
|
+
const resolver = ctx.get("modelDirectories");
|
|
1455
|
+
if (!resolver || typeof resolver.directoryFor !== "function") return null;
|
|
1456
|
+
directory = resolver.directoryFor(sessionId) || null;
|
|
1457
|
+
return directory;
|
|
1458
|
+
} catch (e) {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
const readState = () => {
|
|
1463
|
+
const dir = resolve();
|
|
1464
|
+
const snapshot = dir && dir.store && typeof dir.store.getSnapshot === "function" ? dir.store.getSnapshot() : null;
|
|
1465
|
+
return snapshot && typeof snapshot === "object" ? snapshot : null;
|
|
1466
|
+
};
|
|
1467
|
+
return {
|
|
1468
|
+
async load() {
|
|
1469
|
+
const dir = resolve();
|
|
1470
|
+
if (!dir) return null;
|
|
1471
|
+
if (typeof dir.load === "function") {
|
|
1472
|
+
try {
|
|
1473
|
+
await dir.load();
|
|
1474
|
+
} catch (e) {
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
const state = readState();
|
|
1478
|
+
if (!state) return null;
|
|
1479
|
+
const groups = [];
|
|
1480
|
+
for (const group of Array.isArray(state.groups) ? state.groups : []) {
|
|
1481
|
+
const provider = text(group && group.id);
|
|
1482
|
+
if (provider === "") continue;
|
|
1483
|
+
const options = [];
|
|
1484
|
+
for (const model of Array.isArray(group.models) ? group.models : []) {
|
|
1485
|
+
const modelId = text(model && model.id);
|
|
1486
|
+
if (modelId === "") continue;
|
|
1487
|
+
const efforts = [];
|
|
1488
|
+
for (const level of model.reasoning && Array.isArray(model.reasoning.efforts) ? model.reasoning.efforts : []) {
|
|
1489
|
+
const id = text(level && level.id);
|
|
1490
|
+
if (id !== "") efforts.push({ id, name: text(level.name) || id });
|
|
1491
|
+
}
|
|
1492
|
+
const defaultEffort = text(model.reasoning && model.reasoning.defaultEffort);
|
|
1493
|
+
options.push({
|
|
1494
|
+
key: provider + "/" + modelId,
|
|
1495
|
+
provider,
|
|
1496
|
+
model: modelId,
|
|
1497
|
+
label: text(model.name) || modelId,
|
|
1498
|
+
efforts,
|
|
1499
|
+
...defaultEffort === "" ? {} : { defaultEffort }
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
groups.push({ id: provider, name: text(group.name) || provider, options });
|
|
1503
|
+
}
|
|
1504
|
+
const current = state.current || null;
|
|
1505
|
+
const currentProvider = text(current && current.provider);
|
|
1506
|
+
const currentModel = text(current && current.model);
|
|
1507
|
+
const flat = groups.reduce((all, group) => all.concat(group.options), []);
|
|
1508
|
+
const active = flat.find((option) => option.provider === currentProvider && option.model === currentModel);
|
|
1509
|
+
const effortId = text(current && current.reasoningEffort) || active && active.defaultEffort || "";
|
|
1510
|
+
const effortHit = active ? active.efforts.find((level) => level.id === effortId) : void 0;
|
|
1511
|
+
return {
|
|
1512
|
+
label: active ? active.label : currentModel,
|
|
1513
|
+
activeKey: active ? active.key : "",
|
|
1514
|
+
effortLabel: effortHit ? effortHit.name : "",
|
|
1515
|
+
activeEffortId: effortId,
|
|
1516
|
+
groups
|
|
1517
|
+
};
|
|
1518
|
+
},
|
|
1519
|
+
async select(option, effortId) {
|
|
1520
|
+
const dir = resolve();
|
|
1521
|
+
if (!dir || typeof dir.select !== "function") return "model selection is unavailable";
|
|
1522
|
+
try {
|
|
1523
|
+
const result = await dir.select({
|
|
1524
|
+
provider: option.provider,
|
|
1525
|
+
model: option.model,
|
|
1526
|
+
...effortId === "" ? {} : { reasoningEffort: effortId }
|
|
1527
|
+
});
|
|
1528
|
+
if (result && result.ok === false) {
|
|
1529
|
+
const error = result.error || {};
|
|
1530
|
+
return text(error.message) || text(error.code) || "model selection failed";
|
|
1531
|
+
}
|
|
1532
|
+
return null;
|
|
1533
|
+
} catch (e) {
|
|
1534
|
+
return e instanceof Error ? e.message : String(e);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// src/client/model-menu.ts
|
|
1541
|
+
function renderModelMenu(React, t, props) {
|
|
1542
|
+
const chevron = () => React.createElement("svg", {
|
|
1543
|
+
width: 14,
|
|
1544
|
+
height: 14,
|
|
1545
|
+
viewBox: "0 0 16 16",
|
|
1546
|
+
fill: "none",
|
|
1547
|
+
"aria-hidden": true,
|
|
1548
|
+
strokeWidth: 1
|
|
1549
|
+
}, React.createElement("path", { d: "M6 12L9.29289 8.70711C9.68342 8.31658 9.68342 7.68342 9.29289 7.29289L6 4", stroke: "currentColor" }));
|
|
1550
|
+
const check = () => React.createElement(
|
|
1551
|
+
"span",
|
|
1552
|
+
{ className: "dsh-recall-menu-check" },
|
|
1553
|
+
React.createElement(
|
|
1554
|
+
"svg",
|
|
1555
|
+
{ width: 14, height: 14, viewBox: "0 0 16 16", fill: "none", "aria-hidden": true },
|
|
1556
|
+
React.createElement("path", { d: "M3.2 8.4l3.1 3.1 6.5-6.9", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" })
|
|
1557
|
+
)
|
|
1558
|
+
);
|
|
1559
|
+
const cell = (label, value, onClick) => React.createElement(
|
|
1560
|
+
"button",
|
|
1561
|
+
{
|
|
1562
|
+
type: "button",
|
|
1563
|
+
role: "menuitem",
|
|
1564
|
+
className: "dsh-recall-menu-cell",
|
|
1565
|
+
onClick
|
|
1566
|
+
},
|
|
1567
|
+
React.createElement("span", { className: "dsh-recall-menu-cell-label" }, label),
|
|
1568
|
+
React.createElement("span", { className: "dsh-recall-menu-cell-value" }, value),
|
|
1569
|
+
React.createElement("span", { className: "dsh-recall-menu-cell-chevron" }, chevron())
|
|
1570
|
+
);
|
|
1571
|
+
if (props.pane === "main") {
|
|
1572
|
+
return React.createElement(
|
|
1573
|
+
"div",
|
|
1574
|
+
{ className: "dsh-recall-menu", role: "menu", "aria-label": t("edit.model.menu") },
|
|
1575
|
+
cell(t("edit.model.label"), props.modelLabel, () => props.onPane("list")),
|
|
1576
|
+
cell(t("edit.model.effort"), props.effortLabel !== "" ? props.effortLabel : t("edit.model.providerDefault"), () => props.onPane("effort"))
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
if (props.pane === "effort") {
|
|
1580
|
+
const option = props.activeOption;
|
|
1581
|
+
const levels = [];
|
|
1582
|
+
if (option === null || option.defaultEffort === void 0) levels.push({ id: "", name: t("edit.model.providerDefault") });
|
|
1583
|
+
for (const level of option ? option.efforts : []) levels.push(level);
|
|
1584
|
+
return React.createElement(
|
|
1585
|
+
"div",
|
|
1586
|
+
{ className: "dsh-recall-menu", role: "menu", "aria-label": t("edit.model.effort") },
|
|
1587
|
+
levels.length === 0 ? React.createElement("div", { className: "dsh-recall-menu-empty" }, t("edit.model.noLevels")) : levels.map((level) => React.createElement(
|
|
1588
|
+
"button",
|
|
1589
|
+
{
|
|
1590
|
+
key: "effort-" + level.id,
|
|
1591
|
+
type: "button",
|
|
1592
|
+
role: "menuitemradio",
|
|
1593
|
+
"aria-checked": level.id === props.activeEffortId,
|
|
1594
|
+
disabled: props.busy,
|
|
1595
|
+
className: "dsh-recall-menu-option" + (level.id === props.activeEffortId ? " dsh-recall-menu-option-active" : ""),
|
|
1596
|
+
onClick: () => props.onPickEffort(level.id)
|
|
1597
|
+
},
|
|
1598
|
+
React.createElement("span", { className: "dsh-recall-menu-name" }, level.name),
|
|
1599
|
+
level.id === props.activeEffortId ? check() : React.createElement("span", { className: "dsh-recall-menu-check" })
|
|
1600
|
+
))
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
const query = props.query.trim().toLowerCase();
|
|
1604
|
+
const groups = props.groups.map((group) => ({
|
|
1605
|
+
id: group.id,
|
|
1606
|
+
name: group.name,
|
|
1607
|
+
options: query === "" ? group.options : group.options.filter((option) => option.label.toLowerCase().indexOf(query) >= 0)
|
|
1608
|
+
})).filter((group) => group.options.length > 0);
|
|
1609
|
+
const total = groups.reduce((count, group) => count + group.options.length, 0);
|
|
1610
|
+
return React.createElement(
|
|
1611
|
+
"div",
|
|
1612
|
+
{ className: "dsh-recall-menu", role: "group", "aria-label": t("edit.model.menu") },
|
|
1613
|
+
// 搜索:官方在候选超过 4 条时才显示,这里同样按总数决定
|
|
1614
|
+
props.groups.reduce((count, group) => count + group.options.length, 0) > 4 ? React.createElement(
|
|
1615
|
+
"div",
|
|
1616
|
+
{ className: "dsh-recall-menu-search-row" },
|
|
1617
|
+
React.createElement(
|
|
1618
|
+
"span",
|
|
1619
|
+
{ className: "dsh-recall-menu-search" },
|
|
1620
|
+
React.createElement("input", {
|
|
1621
|
+
type: "text",
|
|
1622
|
+
role: "searchbox",
|
|
1623
|
+
value: props.query,
|
|
1624
|
+
placeholder: t("edit.model.search"),
|
|
1625
|
+
"aria-label": t("edit.model.search"),
|
|
1626
|
+
onChange: (e) => props.onQuery(e.target.value)
|
|
1627
|
+
})
|
|
1628
|
+
)
|
|
1629
|
+
) : null,
|
|
1630
|
+
React.createElement(
|
|
1631
|
+
"div",
|
|
1632
|
+
{ className: "dsh-recall-menu-groups", role: "menu", "aria-label": t("edit.model.label") },
|
|
1633
|
+
total === 0 ? React.createElement("div", { className: "dsh-recall-menu-empty" }, t("edit.model.empty")) : groups.map((group) => React.createElement(
|
|
1634
|
+
"section",
|
|
1635
|
+
{ key: "group-" + group.id, className: "dsh-recall-menu-group" },
|
|
1636
|
+
React.createElement("div", { className: "dsh-recall-menu-heading" }, group.name),
|
|
1637
|
+
group.options.map((option) => React.createElement(
|
|
1638
|
+
"button",
|
|
1639
|
+
{
|
|
1640
|
+
key: option.key,
|
|
1641
|
+
type: "button",
|
|
1642
|
+
role: "menuitemradio",
|
|
1643
|
+
"aria-checked": option.key === props.activeKey,
|
|
1644
|
+
disabled: props.busy,
|
|
1645
|
+
title: option.label,
|
|
1646
|
+
className: "dsh-recall-menu-option" + (option.key === props.activeKey ? " dsh-recall-menu-option-active" : ""),
|
|
1647
|
+
onClick: () => props.onPick(option)
|
|
1648
|
+
},
|
|
1649
|
+
React.createElement("span", { className: "dsh-recall-menu-name" }, option.label),
|
|
1650
|
+
option.key === props.activeKey ? check() : React.createElement("span", { className: "dsh-recall-menu-check" })
|
|
1651
|
+
))
|
|
1652
|
+
))
|
|
1653
|
+
)
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/client/draft-fill.ts
|
|
1658
|
+
function defaultAttachmentName(mediaType, index) {
|
|
1659
|
+
const text2 = String(mediaType || "");
|
|
1660
|
+
const slash = text2.indexOf("/");
|
|
1661
|
+
const sub = (slash >= 0 ? text2.slice(slash + 1) : text2).replace(/[^a-z0-9.+-]/gi, "");
|
|
1662
|
+
return "attachment-" + String(index + 1) + "." + (sub || "bin");
|
|
1663
|
+
}
|
|
1664
|
+
function buildDraftFill(ctx) {
|
|
1665
|
+
function fillDraft(targetSessionId, draftText, attachmentFiles, opts) {
|
|
1666
|
+
if (!targetSessionId || !draftText && attachmentFiles === null) return;
|
|
1667
|
+
let attempts = 0;
|
|
1668
|
+
let attachStarted = false;
|
|
1669
|
+
let attachDone = attachmentFiles === null;
|
|
1670
|
+
let submitted = false;
|
|
1671
|
+
let shellRef = null;
|
|
1672
|
+
const submitIfReady = () => {
|
|
1673
|
+
if (!opts || opts.submit !== true || submitted || !shellRef) return;
|
|
1674
|
+
submitted = true;
|
|
1675
|
+
submitShell(shellRef, draftText);
|
|
1676
|
+
};
|
|
1677
|
+
const attempt = () => {
|
|
1678
|
+
let textDone = draftText === "";
|
|
1679
|
+
try {
|
|
1680
|
+
const conversation = ctx.get("conversation");
|
|
1681
|
+
if (conversation && conversation.input && typeof conversation.input.shell === "function") {
|
|
1682
|
+
const shell = conversation.input.shell(targetSessionId);
|
|
1683
|
+
if (shell) {
|
|
1684
|
+
shellRef = shell;
|
|
1685
|
+
if (!textDone) {
|
|
1686
|
+
if (shell.actions && typeof shell.actions.setDraft === "function") {
|
|
1687
|
+
shell.actions.setDraft(draftText);
|
|
1688
|
+
textDone = true;
|
|
1689
|
+
} else if (typeof shell.setDraft === "function") {
|
|
1690
|
+
shell.setDraft(draftText);
|
|
1691
|
+
textDone = true;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (!attachStarted && attachmentFiles !== null) {
|
|
1695
|
+
attachStarted = startAttachDrafts(conversation, shell, targetSessionId, attachmentFiles, () => {
|
|
1696
|
+
attachDone = true;
|
|
1697
|
+
submitIfReady();
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
} catch (e) {
|
|
1703
|
+
}
|
|
1704
|
+
if (textDone && attachDone) {
|
|
1705
|
+
submitIfReady();
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
if ((!textDone || !attachStarted) && attempts++ < 8) setTimeout(attempt, 150);
|
|
1709
|
+
};
|
|
1710
|
+
attempt();
|
|
1711
|
+
}
|
|
1712
|
+
function submitShell(shell, expected) {
|
|
1713
|
+
const actions = shell.actions;
|
|
1714
|
+
const submit = actions && actions.submit;
|
|
1715
|
+
if (typeof submit !== "function") {
|
|
1716
|
+
recallNodeLog.warn("\u8F93\u5165\u533A\u672A\u66B4\u9732 submit\uFF0C\u7F16\u8F91\u540E\u7684\u6D88\u606F\u672A\u80FD\u81EA\u52A8\u53D1\u51FA");
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
let tries = 0;
|
|
1720
|
+
const tick = () => {
|
|
1721
|
+
const snap = shell.snapshot;
|
|
1722
|
+
const draft = snap && typeof snap.draft === "string" ? snap.draft : null;
|
|
1723
|
+
if (draft !== null && draft !== expected && tries++ < 6) {
|
|
1724
|
+
setTimeout(tick, 50);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
try {
|
|
1728
|
+
submit.call(actions);
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
recallNodeLog.warn("send after edit failed:", error);
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
setTimeout(tick, 0);
|
|
1734
|
+
}
|
|
1735
|
+
function startAttachDrafts(conversation, shell, sessionId, files, onDone) {
|
|
1736
|
+
if (typeof conversation.createDrafts !== "function") return false;
|
|
1737
|
+
const actions = shell.actions;
|
|
1738
|
+
let add = null;
|
|
1739
|
+
if (actions && typeof actions.addAttachments === "function") add = (ids) => actions.addAttachments ? actions.addAttachments(ids) : false;
|
|
1740
|
+
else if (typeof shell.addAttachments === "function") add = (ids) => shell.addAttachments ? shell.addAttachments(ids) : false;
|
|
1741
|
+
if (add === null) return false;
|
|
1742
|
+
(async () => {
|
|
1743
|
+
try {
|
|
1744
|
+
let list = [];
|
|
1745
|
+
try {
|
|
1746
|
+
list = await files;
|
|
1747
|
+
} catch (e) {
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
if (!Array.isArray(list) || list.length === 0) return;
|
|
1751
|
+
try {
|
|
1752
|
+
const drafts = conversation.createDrafts ? conversation.createDrafts(sessionId, list) : [];
|
|
1753
|
+
const descriptors = Array.isArray(drafts) ? drafts : [];
|
|
1754
|
+
const ids = descriptors.map((draft) => draft && draft.id).filter((id) => typeof id === "string" && id !== "");
|
|
1755
|
+
if (ids.length === 0) return;
|
|
1756
|
+
const accepted = add(ids);
|
|
1757
|
+
if (accepted === false && typeof conversation.releaseDraftAttachments === "function") conversation.releaseDraftAttachments(descriptors);
|
|
1758
|
+
} catch (e) {
|
|
1759
|
+
}
|
|
1760
|
+
} finally {
|
|
1761
|
+
if (onDone) onDone();
|
|
1762
|
+
}
|
|
1763
|
+
})();
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
function preloadAttachmentFiles(sourceSessionId, refs) {
|
|
1767
|
+
return (async () => {
|
|
1768
|
+
const sessions = ctx.sessions;
|
|
1769
|
+
const binding = sessions && typeof sessions.binding === "function" ? sessions.binding(sourceSessionId) : void 0;
|
|
1770
|
+
const session = binding && binding.session;
|
|
1771
|
+
if (!session || typeof session.readAttachment !== "function") return [];
|
|
1772
|
+
const out = [];
|
|
1773
|
+
for (const ref of refs) {
|
|
1774
|
+
try {
|
|
1775
|
+
const result = await session.readAttachment(ref.attachmentId);
|
|
1776
|
+
if (!result || result.ok !== true || !result.value || result.value.data === void 0 || result.value.data === null) continue;
|
|
1777
|
+
const raw = result.value.data;
|
|
1778
|
+
const bytes = raw instanceof Uint8Array ? raw : Uint8Array.from(raw);
|
|
1779
|
+
const meta = result.value.attachment;
|
|
1780
|
+
const mediaType = meta && typeof meta.mediaType === "string" && meta.mediaType !== "" ? meta.mediaType : "application/octet-stream";
|
|
1781
|
+
const name = ref.name || (meta && typeof meta.name === "string" && meta.name !== "" ? meta.name : defaultAttachmentName(mediaType, out.length));
|
|
1782
|
+
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1783
|
+
out.push(new File([buffer], name, { type: mediaType }));
|
|
1784
|
+
} catch (e) {
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
return out;
|
|
1788
|
+
})();
|
|
1789
|
+
}
|
|
1790
|
+
return { fillDraft, preloadAttachmentFiles };
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/client/version-tree.ts
|
|
1794
|
+
var KEY_PREFIX = "dsh-recall:versions:";
|
|
1795
|
+
var activeSession = { id: "" };
|
|
1796
|
+
function storage() {
|
|
1797
|
+
try {
|
|
1798
|
+
return typeof localStorage === "undefined" ? null : localStorage;
|
|
1799
|
+
} catch (e) {
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function readIds(rootId) {
|
|
1804
|
+
const store = storage();
|
|
1805
|
+
if (!store || !rootId) return [];
|
|
1806
|
+
try {
|
|
1807
|
+
const raw = store.getItem(KEY_PREFIX + rootId);
|
|
1808
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
1809
|
+
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string" && id !== "") : [];
|
|
1810
|
+
} catch (e) {
|
|
1811
|
+
return [];
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
function writeIds(rootId, ids) {
|
|
1815
|
+
const store = storage();
|
|
1816
|
+
if (!store || !rootId) return;
|
|
1817
|
+
try {
|
|
1818
|
+
store.setItem(KEY_PREFIX + rootId, JSON.stringify(ids));
|
|
1819
|
+
} catch (e) {
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
function findRootOf(sessionId) {
|
|
1823
|
+
const store = storage();
|
|
1824
|
+
if (!store || !sessionId) return null;
|
|
1825
|
+
for (let i = 0; i < store.length; i++) {
|
|
1826
|
+
const key = store.key(i);
|
|
1827
|
+
if (!key || !key.startsWith(KEY_PREFIX)) continue;
|
|
1828
|
+
const rootId = key.slice(KEY_PREFIX.length);
|
|
1829
|
+
if (readIds(rootId).indexOf(sessionId) >= 0) return rootId;
|
|
1830
|
+
}
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
function familyOf(sessionId) {
|
|
1834
|
+
const id = String(sessionId || "");
|
|
1835
|
+
if (!id) return null;
|
|
1836
|
+
const rootId = findRootOf(id);
|
|
1837
|
+
if (!rootId) return null;
|
|
1838
|
+
const ids = readIds(rootId);
|
|
1839
|
+
const index = ids.indexOf(id);
|
|
1840
|
+
if (index < 0 || ids.length < 2) return null;
|
|
1841
|
+
return { rootId, ids, index };
|
|
1842
|
+
}
|
|
1843
|
+
function recordVersion(parentId, childId) {
|
|
1844
|
+
const parent = String(parentId || "");
|
|
1845
|
+
const child = String(childId || "");
|
|
1846
|
+
if (!parent || !child || parent === child) return;
|
|
1847
|
+
const rootId = findRootOf(parent) || parent;
|
|
1848
|
+
const ids = readIds(rootId);
|
|
1849
|
+
const next = ids.length === 0 ? [parent] : ids.slice();
|
|
1850
|
+
if (next.indexOf(child) >= 0) return;
|
|
1851
|
+
next.push(child);
|
|
1852
|
+
writeIds(rootId, next);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1092
1855
|
// src/client/recall-node.ts
|
|
1093
1856
|
function attachmentRefsFromBlocks(blocks) {
|
|
1094
1857
|
const refs = [];
|
|
@@ -1096,18 +1859,12 @@
|
|
|
1096
1859
|
if (!block || block.type !== "image" && block.type !== "file") continue;
|
|
1097
1860
|
const ref = block.attachment;
|
|
1098
1861
|
if (!ref || typeof ref.attachmentId !== "string" || ref.attachmentId === "") continue;
|
|
1099
|
-
const entry = { attachmentId: ref.attachmentId };
|
|
1862
|
+
const entry = { attachmentId: ref.attachmentId, kind: block.type };
|
|
1100
1863
|
if (typeof ref.name === "string" && ref.name !== "") entry.name = ref.name;
|
|
1101
1864
|
refs.push(entry);
|
|
1102
1865
|
}
|
|
1103
1866
|
return refs;
|
|
1104
1867
|
}
|
|
1105
|
-
function defaultAttachmentName(mediaType, index) {
|
|
1106
|
-
const text = String(mediaType || "");
|
|
1107
|
-
const slash = text.indexOf("/");
|
|
1108
|
-
const sub = (slash >= 0 ? text.slice(slash + 1) : text).replace(/[^a-z0-9.+-]/gi, "");
|
|
1109
|
-
return "attachment-" + String(index + 1) + "." + (sub || "bin");
|
|
1110
|
-
}
|
|
1111
1868
|
function fileExtOf(name) {
|
|
1112
1869
|
const dot = name.lastIndexOf(".");
|
|
1113
1870
|
const raw = dot > 0 && dot < name.length - 1 ? name.slice(dot + 1) : "";
|
|
@@ -1145,6 +1902,8 @@
|
|
|
1145
1902
|
}
|
|
1146
1903
|
function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc, uiWorkspaceSvc) {
|
|
1147
1904
|
const { api, ensureInit, showThrottledToast, writeClipboard, clockText: clockText2, pluginConfig, messageFor, t } = util;
|
|
1905
|
+
const { EditIcon, EditPanel } = buildEditFlow(React, t);
|
|
1906
|
+
const { fillDraft, preloadAttachmentFiles } = buildDraftFill(ctx);
|
|
1148
1907
|
function CopyIcon() {
|
|
1149
1908
|
return React.createElement(
|
|
1150
1909
|
"svg",
|
|
@@ -1313,89 +2072,6 @@
|
|
|
1313
2072
|
}
|
|
1314
2073
|
return null;
|
|
1315
2074
|
}
|
|
1316
|
-
function fillDraft(targetSessionId, draftText, attachmentFiles) {
|
|
1317
|
-
if (!targetSessionId || !draftText && attachmentFiles === null) return;
|
|
1318
|
-
let attempts = 0;
|
|
1319
|
-
let attachStarted = false;
|
|
1320
|
-
const attempt = () => {
|
|
1321
|
-
let textDone = draftText === "";
|
|
1322
|
-
try {
|
|
1323
|
-
const conversation = ctx.get("conversation");
|
|
1324
|
-
if (conversation && conversation.input && typeof conversation.input.shell === "function") {
|
|
1325
|
-
const shell = conversation.input.shell(targetSessionId);
|
|
1326
|
-
if (shell) {
|
|
1327
|
-
if (!textDone) {
|
|
1328
|
-
if (shell.actions && typeof shell.actions.setDraft === "function") {
|
|
1329
|
-
shell.actions.setDraft(draftText);
|
|
1330
|
-
textDone = true;
|
|
1331
|
-
} else if (typeof shell.setDraft === "function") {
|
|
1332
|
-
shell.setDraft(draftText);
|
|
1333
|
-
textDone = true;
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
if (!attachStarted && attachmentFiles !== null) {
|
|
1337
|
-
attachStarted = startAttachDrafts(conversation, shell, targetSessionId, attachmentFiles);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
} catch (e) {
|
|
1342
|
-
}
|
|
1343
|
-
const attachDone = attachmentFiles === null || attachStarted;
|
|
1344
|
-
if ((!textDone || !attachDone) && attempts++ < 8) setTimeout(attempt, 150);
|
|
1345
|
-
};
|
|
1346
|
-
attempt();
|
|
1347
|
-
}
|
|
1348
|
-
function startAttachDrafts(conversation, shell, sessionId, files) {
|
|
1349
|
-
if (typeof conversation.createDrafts !== "function") return false;
|
|
1350
|
-
const actions = shell.actions;
|
|
1351
|
-
let add = null;
|
|
1352
|
-
if (actions && typeof actions.addAttachments === "function") add = (ids) => actions.addAttachments ? actions.addAttachments(ids) : false;
|
|
1353
|
-
else if (typeof shell.addAttachments === "function") add = (ids) => shell.addAttachments ? shell.addAttachments(ids) : false;
|
|
1354
|
-
if (add === null) return false;
|
|
1355
|
-
(async () => {
|
|
1356
|
-
let list = [];
|
|
1357
|
-
try {
|
|
1358
|
-
list = await files;
|
|
1359
|
-
} catch (e) {
|
|
1360
|
-
return;
|
|
1361
|
-
}
|
|
1362
|
-
if (!Array.isArray(list) || list.length === 0) return;
|
|
1363
|
-
try {
|
|
1364
|
-
const drafts = conversation.createDrafts ? conversation.createDrafts(sessionId, list) : [];
|
|
1365
|
-
const descriptors = Array.isArray(drafts) ? drafts : [];
|
|
1366
|
-
const ids = descriptors.map((draft) => draft && draft.id).filter((id) => typeof id === "string" && id !== "");
|
|
1367
|
-
if (ids.length === 0) return;
|
|
1368
|
-
const accepted = add(ids);
|
|
1369
|
-
if (accepted === false && typeof conversation.releaseDraftAttachments === "function") conversation.releaseDraftAttachments(descriptors);
|
|
1370
|
-
} catch (e) {
|
|
1371
|
-
}
|
|
1372
|
-
})();
|
|
1373
|
-
return true;
|
|
1374
|
-
}
|
|
1375
|
-
function preloadAttachmentFiles(sourceSessionId, refs) {
|
|
1376
|
-
return (async () => {
|
|
1377
|
-
const sessions = ctx.sessions;
|
|
1378
|
-
const binding = sessions && typeof sessions.binding === "function" ? sessions.binding(sourceSessionId) : void 0;
|
|
1379
|
-
const session = binding && binding.session;
|
|
1380
|
-
if (!session || typeof session.readAttachment !== "function") return [];
|
|
1381
|
-
const out = [];
|
|
1382
|
-
for (const ref of refs) {
|
|
1383
|
-
try {
|
|
1384
|
-
const result = await session.readAttachment(ref.attachmentId);
|
|
1385
|
-
if (!result || result.ok !== true || !result.value || result.value.data === void 0 || result.value.data === null) continue;
|
|
1386
|
-
const raw = result.value.data;
|
|
1387
|
-
const bytes = raw instanceof Uint8Array ? raw : Uint8Array.from(raw);
|
|
1388
|
-
const meta = result.value.attachment;
|
|
1389
|
-
const mediaType = meta && typeof meta.mediaType === "string" && meta.mediaType !== "" ? meta.mediaType : "application/octet-stream";
|
|
1390
|
-
const name = ref.name || (meta && typeof meta.name === "string" && meta.name !== "" ? meta.name : defaultAttachmentName(mediaType, out.length));
|
|
1391
|
-
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1392
|
-
out.push(new File([buffer], name, { type: mediaType }));
|
|
1393
|
-
} catch (e) {
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
return out;
|
|
1397
|
-
})();
|
|
1398
|
-
}
|
|
1399
2075
|
function removeStaleQueueItems(targetSessionId, itemIds) {
|
|
1400
2076
|
if (!Array.isArray(itemIds) || itemIds.length === 0) return;
|
|
1401
2077
|
const deadline = Date.now() + 5e3;
|
|
@@ -1431,15 +2107,29 @@
|
|
|
1431
2107
|
const data = node && node.data ? node.data : {};
|
|
1432
2108
|
const messageId = node ? String(node.id || node.key || "") : "";
|
|
1433
2109
|
const blocks = Array.isArray(data.content) ? data.content : [];
|
|
1434
|
-
const
|
|
2110
|
+
const text2 = blocks.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
1435
2111
|
const imageBlocks = blocks.filter((b) => b && b.type === "image" && b.attachment).map((b) => ({ attachment: b.attachment }));
|
|
1436
2112
|
const fileBlocks = blocks.filter((b) => b && b.type === "file");
|
|
1437
2113
|
const rest = blocks.filter((b) => !b || !(b.type === "text" && typeof b.text === "string") && !(b.type === "image" && b.attachment) && b.type !== "file");
|
|
1438
2114
|
const attachmentRefs = attachmentRefsFromBlocks(blocks);
|
|
2115
|
+
activeSession.id = String(sessionId || "");
|
|
1439
2116
|
const [copied, setCopied] = React.useState(false);
|
|
1440
2117
|
const [hasSnapshot, setHasSnapshot] = React.useState(false);
|
|
1441
2118
|
const [recall, setRecall] = React.useState({ stage: "idle" });
|
|
1442
2119
|
const [scope, setScope] = React.useState("both");
|
|
2120
|
+
const [edit, setEdit] = React.useState({ stage: "idle" });
|
|
2121
|
+
const stackRef = React.useRef(null);
|
|
2122
|
+
const [editFiles, setEditFiles] = React.useState([]);
|
|
2123
|
+
const [droppedRefs, setDroppedRefs] = React.useState([]);
|
|
2124
|
+
const [refPreviews, setRefPreviews] = React.useState({});
|
|
2125
|
+
const fileUrlsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
2126
|
+
const [editMeta, setEditMeta] = React.useState({ permission: "", model: "" });
|
|
2127
|
+
const [modelState, setModelState] = React.useState(null);
|
|
2128
|
+
const [modelOpen, setModelOpen] = React.useState(false);
|
|
2129
|
+
const [modelBusy, setModelBusy] = React.useState(false);
|
|
2130
|
+
const [modelPane, setModelPane] = React.useState("main");
|
|
2131
|
+
const [modelQuery, setModelQuery] = React.useState("");
|
|
2132
|
+
const pickerRef = React.useRef(null);
|
|
1443
2133
|
React.useEffect(() => {
|
|
1444
2134
|
let alive = true;
|
|
1445
2135
|
let timer = null;
|
|
@@ -1468,8 +2158,8 @@
|
|
|
1468
2158
|
}
|
|
1469
2159
|
if (res && (res.artifactSeg || res.notice)) {
|
|
1470
2160
|
if (recent) {
|
|
1471
|
-
const
|
|
1472
|
-
showThrottledToast(
|
|
2161
|
+
const text3 = res.artifactSeg ? t("notice.buildRoot", { seg: res.artifactSeg }) : String(res.notice).slice(0, 140);
|
|
2162
|
+
showThrottledToast(text3);
|
|
1473
2163
|
}
|
|
1474
2164
|
return;
|
|
1475
2165
|
}
|
|
@@ -1491,7 +2181,7 @@
|
|
|
1491
2181
|
}, [messageId, sessionId]);
|
|
1492
2182
|
const onCopy = () => {
|
|
1493
2183
|
if (copied) return;
|
|
1494
|
-
writeClipboard(
|
|
2184
|
+
writeClipboard(text2).then(() => {
|
|
1495
2185
|
setCopied(true);
|
|
1496
2186
|
const timer = ctx.timer;
|
|
1497
2187
|
if (timer && typeof timer.timeout === "function") {
|
|
@@ -1501,8 +2191,238 @@
|
|
|
1501
2191
|
}
|
|
1502
2192
|
});
|
|
1503
2193
|
};
|
|
2194
|
+
const readComposerMeta = () => {
|
|
2195
|
+
const empty = { permission: "", model: "" };
|
|
2196
|
+
if (typeof document === "undefined") return empty;
|
|
2197
|
+
const card = document.querySelector("[data-composer-card]");
|
|
2198
|
+
if (!card) return empty;
|
|
2199
|
+
const textOf = (slot) => {
|
|
2200
|
+
const el = card.querySelector('[data-slot="' + slot + '"]');
|
|
2201
|
+
if (!el) return "";
|
|
2202
|
+
const raw = typeof el.innerText === "string" ? el.innerText : el.textContent;
|
|
2203
|
+
return String(raw || "").replace(/\s+/g, " ").trim();
|
|
2204
|
+
};
|
|
2205
|
+
return { permission: textOf("conversation.input.permission"), model: textOf("conversation.input.model") };
|
|
2206
|
+
};
|
|
2207
|
+
const resolveImage = typeof loadImage === "function" ? loadImage : null;
|
|
2208
|
+
const revokePreview = (url) => {
|
|
2209
|
+
if (url.indexOf("blob:") !== 0) return;
|
|
2210
|
+
if (typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
2211
|
+
};
|
|
2212
|
+
const previewOfFile = (file) => {
|
|
2213
|
+
const cached = fileUrlsRef.current.get(file);
|
|
2214
|
+
if (cached !== void 0) return cached;
|
|
2215
|
+
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return "";
|
|
2216
|
+
const url = URL.createObjectURL(file);
|
|
2217
|
+
fileUrlsRef.current.set(file, url);
|
|
2218
|
+
return url;
|
|
2219
|
+
};
|
|
2220
|
+
const keptRefs = () => attachmentRefs.filter((ref) => droppedRefs.indexOf(ref.attachmentId) < 0);
|
|
2221
|
+
const editAttachments = () => {
|
|
2222
|
+
const items = [];
|
|
2223
|
+
let index = 0;
|
|
2224
|
+
for (const ref of keptRefs()) {
|
|
2225
|
+
const item = { key: "ref:" + ref.attachmentId, name: ref.name || defaultAttachmentName("application/octet-stream", index) };
|
|
2226
|
+
const preview = refPreviews[ref.attachmentId];
|
|
2227
|
+
if (ref.kind === "image" && typeof preview === "string" && preview !== "") item.previewUrl = preview;
|
|
2228
|
+
items.push(item);
|
|
2229
|
+
index++;
|
|
2230
|
+
}
|
|
2231
|
+
for (const [fileIndex, file] of editFiles.entries()) {
|
|
2232
|
+
const item = { key: "file:" + fileIndex, name: file.name || "attachment-" + (fileIndex + 1) };
|
|
2233
|
+
if (typeof file.type === "string" && file.type.indexOf("image/") === 0) {
|
|
2234
|
+
const preview = previewOfFile(file);
|
|
2235
|
+
if (preview !== "") item.previewUrl = preview;
|
|
2236
|
+
}
|
|
2237
|
+
items.push(item);
|
|
2238
|
+
}
|
|
2239
|
+
return items;
|
|
2240
|
+
};
|
|
2241
|
+
const removeEditAttachment = (key) => {
|
|
2242
|
+
if (key.indexOf("ref:") === 0) {
|
|
2243
|
+
setDroppedRefs((prev) => prev.concat(key.slice(4)));
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
if (key.indexOf("file:") !== 0) return;
|
|
2247
|
+
const index = Number(key.slice(5));
|
|
2248
|
+
if (!Number.isFinite(index)) return;
|
|
2249
|
+
const removed = editFiles[index];
|
|
2250
|
+
if (removed !== void 0) {
|
|
2251
|
+
const url = fileUrlsRef.current.get(removed);
|
|
2252
|
+
if (url !== void 0) {
|
|
2253
|
+
revokePreview(url);
|
|
2254
|
+
fileUrlsRef.current.delete(removed);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
setEditFiles((prev) => prev.filter((_, i) => i !== index));
|
|
2258
|
+
};
|
|
2259
|
+
const openEdit = () => {
|
|
2260
|
+
if (edit.stage === "submitting" || recall.stage === "loading" || recall.stage === "executing") return;
|
|
2261
|
+
if (edit.stage === "editing") return;
|
|
2262
|
+
if (edit.stage === "error") {
|
|
2263
|
+
setEdit({ stage: "editing", text: edit.text, attachments: edit.attachments });
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
setRecall({ stage: "idle" });
|
|
2267
|
+
setEditFiles([]);
|
|
2268
|
+
setDroppedRefs([]);
|
|
2269
|
+
setRefPreviews({});
|
|
2270
|
+
setEditMeta(readComposerMeta());
|
|
2271
|
+
setModelState(null);
|
|
2272
|
+
setModelOpen(false);
|
|
2273
|
+
setModelPane("main");
|
|
2274
|
+
setModelQuery("");
|
|
2275
|
+
const picker = buildModelPicker(ctx, String(sessionId || ""));
|
|
2276
|
+
pickerRef.current = picker;
|
|
2277
|
+
if (picker) {
|
|
2278
|
+
picker.load().then((state) => {
|
|
2279
|
+
if (state) setModelState(state);
|
|
2280
|
+
}).catch(() => {
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
if (resolveImage) {
|
|
2284
|
+
for (const block of imageBlocks) {
|
|
2285
|
+
const attachment = block.attachment;
|
|
2286
|
+
const id = attachment && typeof attachment.attachmentId === "string" ? attachment.attachmentId : "";
|
|
2287
|
+
if (id === "") continue;
|
|
2288
|
+
Promise.resolve(resolveImage(attachment)).then((url) => {
|
|
2289
|
+
if (typeof url !== "string" || url === "") return;
|
|
2290
|
+
setRefPreviews((prev) => prev[id] !== void 0 ? prev : { ...prev, [id]: url });
|
|
2291
|
+
}).catch(() => {
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
setEdit({ stage: "editing", text: text2, attachments: attachmentRefs.length });
|
|
2296
|
+
};
|
|
2297
|
+
const editFail = (nextText, message) => {
|
|
2298
|
+
setEdit({ stage: "error", text: nextText, attachments: attachmentRefs.length, message });
|
|
2299
|
+
};
|
|
2300
|
+
const closeEdit = () => {
|
|
2301
|
+
setEdit({ stage: "idle" });
|
|
2302
|
+
setEditFiles([]);
|
|
2303
|
+
setDroppedRefs([]);
|
|
2304
|
+
setRefPreviews({});
|
|
2305
|
+
for (const url of fileUrlsRef.current.values()) revokePreview(url);
|
|
2306
|
+
fileUrlsRef.current.clear();
|
|
2307
|
+
setModelState(null);
|
|
2308
|
+
setModelOpen(false);
|
|
2309
|
+
pickerRef.current = null;
|
|
2310
|
+
};
|
|
2311
|
+
React.useEffect(() => () => {
|
|
2312
|
+
for (const url of fileUrlsRef.current.values()) revokePreview(url);
|
|
2313
|
+
fileUrlsRef.current.clear();
|
|
2314
|
+
}, []);
|
|
2315
|
+
const toggleModelMenu = () => {
|
|
2316
|
+
if (!pickerRef.current || modelBusy) return;
|
|
2317
|
+
setModelOpen((prev) => {
|
|
2318
|
+
if (prev) return false;
|
|
2319
|
+
setModelPane("main");
|
|
2320
|
+
setModelQuery("");
|
|
2321
|
+
return true;
|
|
2322
|
+
});
|
|
2323
|
+
};
|
|
2324
|
+
const activeModelOption = () => {
|
|
2325
|
+
if (!modelState) return null;
|
|
2326
|
+
for (const group of modelState.groups) {
|
|
2327
|
+
for (const option of group.options) {
|
|
2328
|
+
if (option.key === modelState.activeKey) return option;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
return null;
|
|
2332
|
+
};
|
|
2333
|
+
const commitModel = (option, effortId) => {
|
|
2334
|
+
const picker = pickerRef.current;
|
|
2335
|
+
if (!picker || modelBusy) return;
|
|
2336
|
+
setModelBusy(true);
|
|
2337
|
+
picker.select(option, effortId).then((error) => {
|
|
2338
|
+
if (error !== null) {
|
|
2339
|
+
setModelBusy(false);
|
|
2340
|
+
showThrottledToast(error);
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
return picker.load().then((state) => {
|
|
2344
|
+
setModelBusy(false);
|
|
2345
|
+
setModelOpen(false);
|
|
2346
|
+
setModelPane("main");
|
|
2347
|
+
if (state) setModelState(state);
|
|
2348
|
+
});
|
|
2349
|
+
}).catch(() => {
|
|
2350
|
+
setModelBusy(false);
|
|
2351
|
+
});
|
|
2352
|
+
};
|
|
2353
|
+
const pickModel = (option) => {
|
|
2354
|
+
const current = modelState;
|
|
2355
|
+
const effort = current !== null && option.key === current.activeKey ? current.activeEffortId : option.defaultEffort || "";
|
|
2356
|
+
commitModel(option, effort);
|
|
2357
|
+
};
|
|
2358
|
+
const pickEffort = (effortId) => {
|
|
2359
|
+
const option = activeModelOption();
|
|
2360
|
+
if (option) commitModel(option, effortId);
|
|
2361
|
+
};
|
|
2362
|
+
React.useEffect(() => {
|
|
2363
|
+
if (!modelOpen) return;
|
|
2364
|
+
const doc = typeof document === "undefined" ? null : document;
|
|
2365
|
+
if (!doc) return;
|
|
2366
|
+
const onPointerDown = (event) => {
|
|
2367
|
+
const target = event.target;
|
|
2368
|
+
if (target && typeof target.closest === "function" && target.closest(".dsh-recall-composer-model") !== null) return;
|
|
2369
|
+
setModelOpen(false);
|
|
2370
|
+
};
|
|
2371
|
+
doc.addEventListener("pointerdown", onPointerDown, true);
|
|
2372
|
+
return () => doc.removeEventListener("pointerdown", onPointerDown, true);
|
|
2373
|
+
}, [modelOpen]);
|
|
2374
|
+
const editPanelOpen = edit.stage === "editing" || edit.stage === "error";
|
|
2375
|
+
React.useEffect(() => {
|
|
2376
|
+
if (!editPanelOpen) return;
|
|
2377
|
+
const doc = typeof document === "undefined" ? null : document;
|
|
2378
|
+
if (!doc) return;
|
|
2379
|
+
return installEditDismiss(doc, {
|
|
2380
|
+
popupSelector: "",
|
|
2381
|
+
onDismiss: () => {
|
|
2382
|
+
if (modelOpen) {
|
|
2383
|
+
setModelOpen(false);
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
closeEdit();
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
}, [editPanelOpen, modelOpen]);
|
|
2390
|
+
const submitEdit = () => {
|
|
2391
|
+
if (edit.stage !== "editing" && edit.stage !== "error") return;
|
|
2392
|
+
const nextText = edit.text;
|
|
2393
|
+
const kept = keptRefs();
|
|
2394
|
+
if (nextText.trim() === "" && kept.length === 0 && editFiles.length === 0) {
|
|
2395
|
+
editFail(nextText, t("edit.empty"));
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
const extraFiles = editFiles.length > 0 ? Promise.resolve(editFiles.slice()) : null;
|
|
2399
|
+
setEdit({ stage: "submitting" });
|
|
2400
|
+
api("preview", { messageId, sessionId }).then((res) => {
|
|
2401
|
+
if (!res || !res.ok) {
|
|
2402
|
+
editFail(nextText, messageFor(res, t("fallback.preview")));
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
const cut = typeof res.cutSeq === "number" ? res.cutSeq : null;
|
|
2406
|
+
if (cut === null) {
|
|
2407
|
+
editFail(nextText, t("edit.noCut"));
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
executeRecall({
|
|
2411
|
+
scope: "session-only",
|
|
2412
|
+
cutSeq: cut,
|
|
2413
|
+
total: typeof res.total === "number" ? res.total : (res.changes || []).length,
|
|
2414
|
+
treeId: res.treeId || null,
|
|
2415
|
+
editText: nextText,
|
|
2416
|
+
editRefs: kept,
|
|
2417
|
+
editFiles: extraFiles
|
|
2418
|
+
});
|
|
2419
|
+
}).catch((error) => {
|
|
2420
|
+
editFail(nextText, String(error));
|
|
2421
|
+
});
|
|
2422
|
+
};
|
|
1504
2423
|
const openPreview = () => {
|
|
1505
2424
|
if (recall.stage === "loading" || recall.stage === "executing") return;
|
|
2425
|
+
closeEdit();
|
|
1506
2426
|
setScope("both");
|
|
1507
2427
|
setRecall({ stage: "loading" });
|
|
1508
2428
|
api("preview", { messageId, sessionId }).then((res) => {
|
|
@@ -1523,24 +2443,45 @@
|
|
|
1523
2443
|
setRecall({ stage: "error", message: String(error) });
|
|
1524
2444
|
});
|
|
1525
2445
|
};
|
|
1526
|
-
const executeRecall = () => {
|
|
1527
|
-
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
const
|
|
1531
|
-
const
|
|
1532
|
-
const
|
|
1533
|
-
|
|
1534
|
-
|
|
2446
|
+
const executeRecall = (overrides) => {
|
|
2447
|
+
const editText = overrides && typeof overrides.editText === "string" ? overrides.editText : null;
|
|
2448
|
+
const isEdit = editText !== null;
|
|
2449
|
+
if (!isEdit && recall.stage !== "confirm") return;
|
|
2450
|
+
const changes = recall.stage === "confirm" ? recall.changes || [] : [];
|
|
2451
|
+
const activeScope = overrides && overrides.scope === "session-only" ? "session-only" : overrides && overrides.scope === "both" ? "both" : scope;
|
|
2452
|
+
const previewCut = overrides && overrides.cutSeq !== void 0 ? overrides.cutSeq : recall.stage === "confirm" && typeof recall.cutSeq === "number" ? recall.cutSeq : null;
|
|
2453
|
+
const setStage = (stage) => {
|
|
2454
|
+
if (!isEdit) {
|
|
2455
|
+
setRecall(stage);
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
if (stage.stage === "error") {
|
|
2459
|
+
setEdit({ stage: "error", text: editText === null ? "" : editText, attachments: attachmentRefs.length, message: stage.message });
|
|
2460
|
+
}
|
|
2461
|
+
};
|
|
2462
|
+
const notifyBase = { sessionId: String(sessionId || ""), messageId, scope: activeScope };
|
|
2463
|
+
const previewTotal = overrides && typeof overrides.total === "number" ? overrides.total : recall.stage === "confirm" && typeof recall.total === "number" ? recall.total : changes.length;
|
|
2464
|
+
const previewTreeId = overrides && overrides.treeId !== void 0 ? overrides.treeId : recall.stage === "confirm" ? recall.treeId : null;
|
|
2465
|
+
const refsForFiles = overrides && overrides.editRefs ? overrides.editRefs : attachmentRefs;
|
|
2466
|
+
const wantAttachments = refsForFiles.length > 0 && sessionId && (isEdit || pluginConfig.refillDraft);
|
|
2467
|
+
const baseFiles = wantAttachments ? preloadAttachmentFiles(sessionId, refsForFiles) : null;
|
|
2468
|
+
const inlineFiles = overrides && overrides.editFiles ? overrides.editFiles : null;
|
|
2469
|
+
const attachmentFiles = baseFiles !== null && inlineFiles !== null ? Promise.all([baseFiles, inlineFiles]).then(([base, extra]) => base.concat(extra)) : baseFiles !== null ? baseFiles : inlineFiles;
|
|
2470
|
+
setStage({ stage: "executing", changes });
|
|
2471
|
+
api("execute", { messageId, sessionId, previewTotal, previewTreeId: previewTreeId || void 0, scope: activeScope, previewAt: Date.now() }).then(async (res) => {
|
|
1535
2472
|
if (!res || !res.ok) {
|
|
1536
2473
|
if (res && res.code === "STALE") {
|
|
1537
|
-
|
|
2474
|
+
if (isEdit) {
|
|
2475
|
+
setStage({ stage: "error", message: t("err.STALE") });
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
setStage({ stage: "loading" });
|
|
1538
2479
|
api("preview", { messageId, sessionId }).then((res2) => {
|
|
1539
2480
|
if (!res2 || !res2.ok) {
|
|
1540
|
-
|
|
2481
|
+
setStage({ stage: "error", message: messageFor(res2, t("fallback.preview")) });
|
|
1541
2482
|
return;
|
|
1542
2483
|
}
|
|
1543
|
-
|
|
2484
|
+
setStage({
|
|
1544
2485
|
stage: "confirm",
|
|
1545
2486
|
changes: res2.changes || [],
|
|
1546
2487
|
total: typeof res2.total === "number" ? res2.total : (res2.changes || []).length,
|
|
@@ -1550,7 +2491,7 @@
|
|
|
1550
2491
|
cutSeq: typeof res2.cutSeq === "number" ? res2.cutSeq : null
|
|
1551
2492
|
});
|
|
1552
2493
|
}).catch((error) => {
|
|
1553
|
-
|
|
2494
|
+
setStage({ stage: "error", message: String(error) });
|
|
1554
2495
|
});
|
|
1555
2496
|
return;
|
|
1556
2497
|
}
|
|
@@ -1561,7 +2502,7 @@
|
|
|
1561
2502
|
error: messageFor(res, "")
|
|
1562
2503
|
})).catch(() => {
|
|
1563
2504
|
});
|
|
1564
|
-
|
|
2505
|
+
setStage({ stage: "error", message: messageFor(res, t("fallback.rollback")) });
|
|
1565
2506
|
return;
|
|
1566
2507
|
}
|
|
1567
2508
|
const cutSeq = typeof res.cutSeq === "number" ? res.cutSeq : previewCut;
|
|
@@ -1577,6 +2518,7 @@
|
|
|
1577
2518
|
else if (typeof sessionsSvc.open === "function") sessionsSvc.open(childId);
|
|
1578
2519
|
chatReverted = true;
|
|
1579
2520
|
fillTarget = childId;
|
|
2521
|
+
recordVersion(String(sessionId || ""), childId);
|
|
1580
2522
|
removeStaleQueueItems(childId, res.staleQueueItemIds);
|
|
1581
2523
|
api("lineage-record", { childId, parentId: sessionId }).catch(() => {
|
|
1582
2524
|
});
|
|
@@ -1597,12 +2539,23 @@
|
|
|
1597
2539
|
});
|
|
1598
2540
|
}
|
|
1599
2541
|
}
|
|
1600
|
-
if (pluginConfig.refillDraft) {
|
|
1601
|
-
fillDraft(fillTarget, text, attachmentFiles);
|
|
1602
|
-
if (fileBlocks.length > 0) showThrottledToast(t("toast.fileAttachRefill"));
|
|
1603
|
-
}
|
|
1604
2542
|
setHasSnapshot(false);
|
|
1605
|
-
|
|
2543
|
+
if (isEdit) {
|
|
2544
|
+
if (chatReverted) {
|
|
2545
|
+
fillDraft(fillTarget, editText === null ? "" : editText, attachmentFiles, {
|
|
2546
|
+
submit: pluginConfig.editSubmitsImmediately
|
|
2547
|
+
});
|
|
2548
|
+
closeEdit();
|
|
2549
|
+
} else {
|
|
2550
|
+
setEdit({ stage: "error", text: editText === null ? "" : editText, attachments: attachmentRefs.length, message: chatError || t("edit.forkFailed") });
|
|
2551
|
+
}
|
|
2552
|
+
} else {
|
|
2553
|
+
if (pluginConfig.refillDraft) {
|
|
2554
|
+
fillDraft(fillTarget, text2, attachmentFiles);
|
|
2555
|
+
if (fileBlocks.length > 0) showThrottledToast(t("toast.fileAttachRefill"));
|
|
2556
|
+
}
|
|
2557
|
+
setRecall({ stage: "done", count: typeof res.count === "number" ? res.count : changes.length, chatReverted, chatError });
|
|
2558
|
+
}
|
|
1606
2559
|
if (chatError === "") {
|
|
1607
2560
|
api("notify", buildRecallNotify(notifyBase, {
|
|
1608
2561
|
outcome: "complete",
|
|
@@ -1615,7 +2568,7 @@
|
|
|
1615
2568
|
});
|
|
1616
2569
|
}
|
|
1617
2570
|
}).catch((error) => {
|
|
1618
|
-
|
|
2571
|
+
setStage({ stage: "error", message: String(error) });
|
|
1619
2572
|
api("notify", buildRecallNotify(notifyBase, { outcome: "execute-threw", cutSeq: previewCut, error: String(error) })).catch(() => {
|
|
1620
2573
|
});
|
|
1621
2574
|
});
|
|
@@ -1659,8 +2612,8 @@
|
|
|
1659
2612
|
)
|
|
1660
2613
|
));
|
|
1661
2614
|
}
|
|
1662
|
-
if (
|
|
1663
|
-
bubbleChildren.push(React.createElement("div", { className: "dsh-recall-bubble", key: "text" },
|
|
2615
|
+
if (text2 !== "") {
|
|
2616
|
+
bubbleChildren.push(React.createElement("div", { className: "dsh-recall-bubble", key: "text" }, text2));
|
|
1664
2617
|
}
|
|
1665
2618
|
for (let i = 0; i < rest.length; i++) {
|
|
1666
2619
|
bubbleChildren.push(React.createElement("pre", { className: "dsh-recall-json", key: "rest-" + i }, JSON.stringify(rest[i], null, 2)));
|
|
@@ -1685,13 +2638,72 @@
|
|
|
1685
2638
|
title: t("action.recall.title"),
|
|
1686
2639
|
onClick: openPreview
|
|
1687
2640
|
}, React.createElement(UndoIcon, {})));
|
|
2641
|
+
if (edit.stage === "idle") {
|
|
2642
|
+
actions.push(React.createElement("button", {
|
|
2643
|
+
key: "edit",
|
|
2644
|
+
type: "button",
|
|
2645
|
+
className: "dsh-recall-action",
|
|
2646
|
+
"aria-label": t("action.edit"),
|
|
2647
|
+
title: t("action.edit.title"),
|
|
2648
|
+
onClick: openEdit
|
|
2649
|
+
}, React.createElement(EditIcon, {})));
|
|
2650
|
+
}
|
|
1688
2651
|
}
|
|
2652
|
+
const stackClass = "dsh-recall-stack" + (officialFace ? " dsh-recall-stack-official" : "");
|
|
2653
|
+
const editing = edit.stage !== "idle";
|
|
2654
|
+
const modelNodes = modelOpen && modelState ? renderModelMenu(React, t, {
|
|
2655
|
+
pane: modelPane,
|
|
2656
|
+
query: modelQuery,
|
|
2657
|
+
groups: modelState.groups,
|
|
2658
|
+
activeKey: modelState.activeKey,
|
|
2659
|
+
activeOption: activeModelOption(),
|
|
2660
|
+
modelLabel: modelState.label,
|
|
2661
|
+
effortLabel: modelState.effortLabel,
|
|
2662
|
+
activeEffortId: modelState.activeEffortId,
|
|
2663
|
+
busy: modelBusy,
|
|
2664
|
+
onPane: (next) => {
|
|
2665
|
+
setModelPane(next);
|
|
2666
|
+
if (next !== "list") setModelQuery("");
|
|
2667
|
+
},
|
|
2668
|
+
onQuery: setModelQuery,
|
|
2669
|
+
onPick: pickModel,
|
|
2670
|
+
onPickEffort: pickEffort
|
|
2671
|
+
}) : null;
|
|
2672
|
+
const bubble = bubbleChildren.length > 0 ? React.createElement("div", { className: stackClass, key: "stack", ref: stackRef }, ...bubbleChildren) : null;
|
|
2673
|
+
const stack = editing ? React.createElement(
|
|
2674
|
+
"div",
|
|
2675
|
+
{ className: "dsh-recall-stack dsh-recall-stack-editing", key: "stack" },
|
|
2676
|
+
EditPanel({
|
|
2677
|
+
stage: edit,
|
|
2678
|
+
submitLabel: t(pluginConfig.editSubmitsImmediately ? "edit.submit" : "edit.submit.draftOnly"),
|
|
2679
|
+
meta: editMeta,
|
|
2680
|
+
attachments: editAttachments(),
|
|
2681
|
+
// chip 与官方同构:模型名与思考强度分两个 span(档位空 = provider default)
|
|
2682
|
+
modelLabel: modelState ? modelState.label : "",
|
|
2683
|
+
modelEffort: modelState ? modelState.effortLabel : "",
|
|
2684
|
+
modelOpen,
|
|
2685
|
+
modelBusy,
|
|
2686
|
+
modelNodes,
|
|
2687
|
+
onToggleModel: toggleModelMenu,
|
|
2688
|
+
onChange: (next) => setEdit((prev) => prev.stage === "editing" || prev.stage === "error" ? { stage: "editing", text: next, attachments: prev.attachments } : prev),
|
|
2689
|
+
onSubmit: submitEdit,
|
|
2690
|
+
onClose: closeEdit,
|
|
2691
|
+
onAddFiles: (files) => setEditFiles((prev) => prev.concat(files)),
|
|
2692
|
+
onRemoveAttachment: removeEditAttachment
|
|
2693
|
+
})
|
|
2694
|
+
) : bubble;
|
|
1689
2695
|
return React.createElement(
|
|
1690
2696
|
"div",
|
|
1691
|
-
{
|
|
1692
|
-
|
|
2697
|
+
{
|
|
2698
|
+
className: "dsh-recall-row",
|
|
2699
|
+
"data-time-hover-root": true,
|
|
2700
|
+
"data-editing": editing ? "true" : void 0,
|
|
2701
|
+
// 版本翻页器据此确认「当前是哪个会话」(官方槽只给 messageId)
|
|
2702
|
+
"data-recall-session": sessionId || void 0
|
|
2703
|
+
},
|
|
2704
|
+
stack,
|
|
1693
2705
|
React.createElement("div", { className: "dsh-recall-actions", key: "actions" }, ...actions),
|
|
1694
|
-
recallPanel(recall, closePanel, executeRecall, scope, setScope)
|
|
2706
|
+
recallPanel(recall, closePanel, () => executeRecall(), scope, setScope)
|
|
1695
2707
|
);
|
|
1696
2708
|
}
|
|
1697
2709
|
return { UserRecallNode };
|
|
@@ -1725,6 +2737,8 @@
|
|
|
1725
2737
|
refillDraft: v.refillDraft !== false,
|
|
1726
2738
|
snapshotEnabled: v.snapshotEnabled !== false,
|
|
1727
2739
|
archiveOriginal: v.archiveOriginal !== false,
|
|
2740
|
+
// issue #26:老 Host 的 values 无该字段时按默认 true(与 util 同口径)
|
|
2741
|
+
editSubmitsImmediately: v.editSubmitsImmediately !== false,
|
|
1728
2742
|
retentionDays: String(v.retentionDays == null ? "" : v.retentionDays),
|
|
1729
2743
|
locale: typeof v.locale === "string" && v.locale ? v.locale : "auto"
|
|
1730
2744
|
};
|
|
@@ -1749,7 +2763,7 @@
|
|
|
1749
2763
|
function save() {
|
|
1750
2764
|
if (state.busy || !draft || !baseline) return;
|
|
1751
2765
|
const patch = {};
|
|
1752
|
-
for (const key of ["gcSnaps", "gcHours", "maxFileBytes", "maxWorkspaceBytes", "scanBudgetMs", "maxSnapshotsPerWorkspace", "baseExcludes", "refillDraft", "snapshotEnabled", "archiveOriginal", "retentionDays", "locale"]) {
|
|
2766
|
+
for (const key of ["gcSnaps", "gcHours", "maxFileBytes", "maxWorkspaceBytes", "scanBudgetMs", "maxSnapshotsPerWorkspace", "baseExcludes", "refillDraft", "snapshotEnabled", "archiveOriginal", "editSubmitsImmediately", "retentionDays", "locale"]) {
|
|
1753
2767
|
const value = draft[key];
|
|
1754
2768
|
if (value !== void 0 && value !== baseline[key]) patch[key] = value;
|
|
1755
2769
|
}
|
|
@@ -1809,6 +2823,7 @@
|
|
|
1809
2823
|
if (patch.refillDraft !== void 0) clean.refillDraft = Boolean(patch.refillDraft);
|
|
1810
2824
|
if (patch.snapshotEnabled !== void 0) clean.snapshotEnabled = Boolean(patch.snapshotEnabled);
|
|
1811
2825
|
if (patch.archiveOriginal !== void 0) clean.archiveOriginal = Boolean(patch.archiveOriginal);
|
|
2826
|
+
if (patch.editSubmitsImmediately !== void 0) clean.editSubmitsImmediately = Boolean(patch.editSubmitsImmediately);
|
|
1812
2827
|
if (patch.retentionDays !== void 0) {
|
|
1813
2828
|
const n = parseInt(String(patch.retentionDays), 10);
|
|
1814
2829
|
if (!Number.isFinite(n) || n < 0) {
|
|
@@ -1966,6 +2981,7 @@
|
|
|
1966
2981
|
boolRow("snapshotEnabled", t("config.snapshotEnabled.label"), t("config.snapshotEnabled.hint")),
|
|
1967
2982
|
boolRow("refillDraft", t("config.refillDraft.label"), t("config.refillDraft.hint")),
|
|
1968
2983
|
boolRow("archiveOriginal", t("config.archiveOriginal.label"), t("config.archiveOriginal.hint")),
|
|
2984
|
+
boolRow("editSubmitsImmediately", t("config.editSubmitsImmediately.label"), t("config.editSubmitsImmediately.hint")),
|
|
1969
2985
|
React.createElement("div", { className: "dsh-recall-cfg-group" }, t("config.group.auto")),
|
|
1970
2986
|
// 单位后缀统一挂输入框右侧(与状态标签同基线),不再只藏在说明文字里;
|
|
1971
2987
|
// 传词表键而非字面量——单位词也是语言事实(条/小时 vs snapshots/hours)
|
|
@@ -2439,9 +3455,9 @@
|
|
|
2439
3455
|
}
|
|
2440
3456
|
function renderLeaf(it) {
|
|
2441
3457
|
const key = "snap-" + it.id;
|
|
2442
|
-
const
|
|
2443
|
-
const title =
|
|
2444
|
-
const label =
|
|
3458
|
+
const text2 = it.messageText;
|
|
3459
|
+
const title = text2 || it.id;
|
|
3460
|
+
const label = text2 ? clockText2(it.time) + " " + text2 : clockText2(it.time) + " " + it.id.slice(0, 12) + "\u2026";
|
|
2445
3461
|
return React.createElement(
|
|
2446
3462
|
"div",
|
|
2447
3463
|
{ className: "dsh-recall-tree-node", key },
|
|
@@ -2754,6 +3770,121 @@
|
|
|
2754
3770
|
return { RecallSettingsCard };
|
|
2755
3771
|
}
|
|
2756
3772
|
|
|
3773
|
+
// src/client/version-pager.ts
|
|
3774
|
+
function currentSessionId() {
|
|
3775
|
+
if (activeSession.id) return activeSession.id;
|
|
3776
|
+
if (typeof document === "undefined") return "";
|
|
3777
|
+
const rows = document.querySelectorAll("[data-recall-session]");
|
|
3778
|
+
const last = rows.length > 0 ? rows[rows.length - 1] : null;
|
|
3779
|
+
return last ? last.getAttribute("data-recall-session") || "" : "";
|
|
3780
|
+
}
|
|
3781
|
+
function isLastTurnTail(node) {
|
|
3782
|
+
if (!node || typeof document === "undefined") return false;
|
|
3783
|
+
const own = node.closest("[data-turn-tail]");
|
|
3784
|
+
if (!own) return false;
|
|
3785
|
+
const all = document.querySelectorAll("[data-turn-tail]");
|
|
3786
|
+
return all.length === 0 || all[all.length - 1] === own;
|
|
3787
|
+
}
|
|
3788
|
+
function scrollerOf(node) {
|
|
3789
|
+
let cur = node ? node.parentElement : null;
|
|
3790
|
+
while (cur) {
|
|
3791
|
+
if (cur instanceof HTMLElement && cur.scrollHeight > cur.clientHeight) {
|
|
3792
|
+
const style = typeof getComputedStyle === "function" ? getComputedStyle(cur) : null;
|
|
3793
|
+
if (style && /(auto|scroll)/.test(style.overflowY)) return cur;
|
|
3794
|
+
}
|
|
3795
|
+
cur = cur.parentElement;
|
|
3796
|
+
}
|
|
3797
|
+
return null;
|
|
3798
|
+
}
|
|
3799
|
+
function captureAnchor() {
|
|
3800
|
+
if (typeof document === "undefined") return null;
|
|
3801
|
+
const rows = document.querySelectorAll("[data-chat-anchor-key]");
|
|
3802
|
+
for (let i = 0; i < rows.length; i++) {
|
|
3803
|
+
const row = rows[i];
|
|
3804
|
+
const rect = row.getBoundingClientRect();
|
|
3805
|
+
if (rect.bottom > 0) {
|
|
3806
|
+
const key = row.getAttribute("data-chat-anchor-key") || "";
|
|
3807
|
+
return key ? { key, top: rect.top } : null;
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
return null;
|
|
3811
|
+
}
|
|
3812
|
+
function restoreAnchor(scroller, anchor) {
|
|
3813
|
+
if (!scroller || !anchor || typeof document === "undefined") return;
|
|
3814
|
+
const rows = document.querySelectorAll("[data-chat-anchor-key]");
|
|
3815
|
+
for (let i = 0; i < rows.length; i++) {
|
|
3816
|
+
const row = rows[i];
|
|
3817
|
+
if (row.getAttribute("data-chat-anchor-key") !== anchor.key) continue;
|
|
3818
|
+
const delta = row.getBoundingClientRect().top - anchor.top;
|
|
3819
|
+
if (Math.abs(delta) > 1) scroller.scrollTop += delta;
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
function buildVersionPager(React, util, workspacesSvc, uiWorkspaceSvc) {
|
|
3824
|
+
const { t, showThrottledToast } = util;
|
|
3825
|
+
function VersionPager(props) {
|
|
3826
|
+
const ref = React.useRef(null);
|
|
3827
|
+
const [family, setFamily] = React.useState(null);
|
|
3828
|
+
const [visible, setVisible] = React.useState(false);
|
|
3829
|
+
const [busy, setBusy] = React.useState(false);
|
|
3830
|
+
React.useEffect(() => {
|
|
3831
|
+
const measure = () => {
|
|
3832
|
+
setFamily(familyOf(props && props.sessionId || currentSessionId()));
|
|
3833
|
+
setVisible(isLastTurnTail(ref.current));
|
|
3834
|
+
};
|
|
3835
|
+
measure();
|
|
3836
|
+
const timer = setTimeout(measure, 60);
|
|
3837
|
+
return () => clearTimeout(timer);
|
|
3838
|
+
}, [props && props.messageId]);
|
|
3839
|
+
const show = Boolean(family && family.ids.length >= 2 && visible);
|
|
3840
|
+
if (!show || !family) {
|
|
3841
|
+
return React.createElement("div", { className: "dsh-recall-pager", ref });
|
|
3842
|
+
}
|
|
3843
|
+
const total = family.ids.length;
|
|
3844
|
+
const index = family.index;
|
|
3845
|
+
const go = (delta) => {
|
|
3846
|
+
if (busy) return;
|
|
3847
|
+
const target = family.ids[index + delta];
|
|
3848
|
+
if (!target) return;
|
|
3849
|
+
const current = activeSession.id || family.ids[index];
|
|
3850
|
+
if (!current || current === target) return;
|
|
3851
|
+
setBusy(true);
|
|
3852
|
+
const scroller = scrollerOf(ref.current);
|
|
3853
|
+
const anchor = captureAnchor();
|
|
3854
|
+
const done = () => setBusy(false);
|
|
3855
|
+
Promise.resolve().then(() => workspacesSvc && typeof workspacesSvc.archiveSession === "function" ? workspacesSvc.archiveSession(current) : void 0).then(() => uiWorkspaceSvc && typeof uiWorkspaceSvc.unarchiveSession === "function" ? uiWorkspaceSvc.unarchiveSession(target) : void 0).then(() => {
|
|
3856
|
+
if (uiWorkspaceSvc && typeof uiWorkspaceSvc.openSession === "function") uiWorkspaceSvc.openSession(target);
|
|
3857
|
+
setTimeout(() => restoreAnchor(scroller, anchor), 80);
|
|
3858
|
+
setTimeout(() => restoreAnchor(scroller, anchor), 320);
|
|
3859
|
+
}).catch((error) => {
|
|
3860
|
+
showThrottledToast(t("toast.pagerFailed", { error: String(error).slice(0, 120) }));
|
|
3861
|
+
}).then(done, done);
|
|
3862
|
+
};
|
|
3863
|
+
return React.createElement(
|
|
3864
|
+
"div",
|
|
3865
|
+
{ className: "dsh-recall-pager", ref, "data-busy": busy ? "true" : void 0 },
|
|
3866
|
+
React.createElement("button", {
|
|
3867
|
+
type: "button",
|
|
3868
|
+
className: "dsh-recall-pager-btn",
|
|
3869
|
+
"aria-label": t("pager.prev"),
|
|
3870
|
+
title: t("pager.prev"),
|
|
3871
|
+
disabled: busy || index <= 0,
|
|
3872
|
+
onClick: () => go(-1)
|
|
3873
|
+
}, "\u2039"),
|
|
3874
|
+
React.createElement("span", { className: "dsh-recall-pager-count", "aria-label": t("pager.label", { n: index + 1, total }) }, index + 1 + "/" + total),
|
|
3875
|
+
React.createElement("button", {
|
|
3876
|
+
type: "button",
|
|
3877
|
+
className: "dsh-recall-pager-btn",
|
|
3878
|
+
"aria-label": t("pager.next"),
|
|
3879
|
+
title: t("pager.next"),
|
|
3880
|
+
disabled: busy || index >= total - 1,
|
|
3881
|
+
onClick: () => go(1)
|
|
3882
|
+
}, "\u203A")
|
|
3883
|
+
);
|
|
3884
|
+
}
|
|
3885
|
+
return { VersionPager };
|
|
3886
|
+
}
|
|
3887
|
+
|
|
2757
3888
|
// src/client/app.ts
|
|
2758
3889
|
function nextShadowPriority(entries, key) {
|
|
2759
3890
|
let priority = -1;
|
|
@@ -2784,6 +3915,7 @@
|
|
|
2784
3915
|
const uiWorkspaceSvc = ctx.uiWorkspace;
|
|
2785
3916
|
const { UserRecallNode } = buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc, uiWorkspaceSvc);
|
|
2786
3917
|
const { RecallSettingsCard } = buildSettingsCards(React, util, sessionsSvc, workspacesSvc, uiWorkspaceSvc);
|
|
3918
|
+
const { VersionPager } = buildVersionPager(React, util, workspacesSvc, uiWorkspaceSvc);
|
|
2787
3919
|
for (const slotKey of ["user", "steering"]) {
|
|
2788
3920
|
try {
|
|
2789
3921
|
slots.inject("conversation.chat.node", () => {
|
|
@@ -2797,6 +3929,19 @@
|
|
|
2797
3929
|
appLog.error("slot register failed (" + slotKey + "):", error);
|
|
2798
3930
|
}
|
|
2799
3931
|
}
|
|
3932
|
+
try {
|
|
3933
|
+
slots.inject("conversation.chat.assistant-actions", () => slots.register(
|
|
3934
|
+
{
|
|
3935
|
+
name: "conversation.chat.assistant-actions",
|
|
3936
|
+
id: "recall-version-pager",
|
|
3937
|
+
order: 20,
|
|
3938
|
+
inject: (sessionId) => ({ sessionId })
|
|
3939
|
+
},
|
|
3940
|
+
VersionPager
|
|
3941
|
+
));
|
|
3942
|
+
} catch (error) {
|
|
3943
|
+
appLog.error("version pager register failed:", error);
|
|
3944
|
+
}
|
|
2800
3945
|
try {
|
|
2801
3946
|
slots.inject("settings.plugin.item", () => slots.register(
|
|
2802
3947
|
{ name: "settings.plugin.item", key: "dsh-recall" },
|