dsh-as-aistudio 0.3.0 → 0.4.1
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 +15 -0
- package/package.json +1 -1
- package/src/client.js +318 -12
- package/src/components.js +1 -1
- package/src/vendor/host/_platform.js +5 -8
- package/src/vendor/host/dsh-edit-turn.js +4 -71
- package/src/vendor/host/dsh-edit-turn.patch.js +1 -1
- package/src/vendor/host/dsh-rerun-turn.js +37 -5
- package/src/vendor/versions.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
## 中文
|
|
6
6
|
|
|
7
|
+
**0.4.1** —— 编辑过的消息,复制按钮被宿主重画之后仍然复制改写后的文本。
|
|
8
|
+
|
|
9
|
+
- **组件跟进**:`dsh-edit-turn` **0.5.7**(另三个不变:`dsh-rerun-turn` 0.2.6、`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
|
|
10
|
+
|
|
11
|
+
- **复制拿到的是改写前的原文**:编辑一条消息后,界面上显示的是改写后的文本,但点那一排的复制按钮,粘出来的是原文。原因是这次按下原本由**挂在宿主那个按钮节点上**的捕获期监听回答,而动作条是宿主 React 画的——任何一次重渲染都可能换掉那个按钮节点,监听就跟着旧节点一起消失,按下于是落到宿主自己的处理上(它复制"这一行被画出来时的那条消息")。现在改由**文档级捕获期监听按行**回答:与按钮是不是同一个节点无关,捕获期在 React 根节点之前,宿主自己的处理看不到这次按下。本包已把修好的 `dsh-edit-turn` 0.5.7 重新 vendor 进来并重建了浏览器 bundle。
|
|
12
|
+
|
|
13
|
+
**0.4.0** —— 少要一个平台包;四个组件跟进到最新。
|
|
14
|
+
|
|
15
|
+
- **不再要求 `dsh-tools`**:本包内置的宿主半部以前要 `@deepseek-ai/dsh-tools`,唯一原因是 `dsh-edit-turn` 向模型暴露了一个工具(`edit_turn_targets`)。那个工具已经删掉——编辑是靠 UI 面板做的,模型不需要知道这件事——于是整族插件都不再声明模型可见的工具,宿主半部的解析只需要 `dsh-session`。少一个平台包,就少一处跟宿主版本绑定的地方。
|
|
16
|
+
|
|
17
|
+
- **组件跟进**:`dsh-edit-turn` **0.5.6**、`dsh-rerun-turn` **0.2.6**;另两个不变(`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
|
|
18
|
+
|
|
19
|
+
- `dsh-edit-turn` 0.5.5 把模型工具拿掉;0.5.6 给会话日志裁剪工具加了"先验后写"(那个工具在 `tools/` 下、不随 npm 包发布,本包也不含它,只是版本号跟着走)。
|
|
20
|
+
- `dsh-rerun-turn` 0.2.6 修 `sourceEventSeqs`:写入前摊平嵌套数组、去重、排序,并且**只接受真正的数字**——之前写出的形状(嵌套数组、缺项、倒序区间)会被宿主的校验器整条拒收,**整份会话日志都加载不了**。这是实测出来的:一个 29,497 事件的会话里有三条这样的记录,宿主三层校验全拒。
|
|
21
|
+
|
|
7
22
|
**0.3.0** —— 设置页搬到插件行自己的配置箭头;组件设置卡片直接复用组件自己的那一个。
|
|
8
23
|
|
|
9
24
|
- **设置页换了位置**:以前是「设置 → 插件 → AI Studio」里多一个标签页,和别的插件都不一样。现在注册到 `plugins.row.config`,键是 `dsh-as-aistudio#dsh-as-aistudio`(`<包名>#<行 id>` 的约定),于是**本包那一行**右侧多了配置箭头 —— 和这个 profile 里其它插件完全一致。点进去是整页:四个组件的实时状态,加下面的「组件设置」。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-as-aistudio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Google-AI-Studio-style turn controls for DeepSeek Harness, in one install: edit a prompt and re-run it, re-run a reply in place, delete a turn from the model context, and render sent prompts as Markdown. Self-contained: the plugin list shows this one row, and the four components are vendored inside it - installing the studio installs nothing else.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/client.js
CHANGED
|
@@ -43,7 +43,7 @@ window.__ModuleLoader__.load({
|
|
|
43
43
|
const RERUN_PREFIX = '/dsh-rerun-turn'
|
|
44
44
|
|
|
45
45
|
/** Keep in sync with package.json and lib/index.js. */
|
|
46
|
-
const PLUGIN_VERSION = '0.5.
|
|
46
|
+
const PLUGIN_VERSION = '0.5.7'
|
|
47
47
|
|
|
48
48
|
// --- copy -----------------------------------------------------------------
|
|
49
49
|
|
|
@@ -58,6 +58,8 @@ window.__ModuleLoader__.load({
|
|
|
58
58
|
'copy': '复制',
|
|
59
59
|
'copied': '复制成功',
|
|
60
60
|
'action.copied': '复制成功',
|
|
61
|
+
'action.copy': '复制这条回答',
|
|
62
|
+
'action.tail-actions': '这一轮的动作',
|
|
61
63
|
'editor.title': '编辑这条消息',
|
|
62
64
|
'editor.title-reply': '编辑这条回答',
|
|
63
65
|
'editor.placeholder': '改成你希望自己当时说的内容。保存后就地替换这条消息,后面的对话保持不变。',
|
|
@@ -145,6 +147,8 @@ window.__ModuleLoader__.load({
|
|
|
145
147
|
'copy': 'Copy',
|
|
146
148
|
'copied': 'Copied',
|
|
147
149
|
'action.copied': 'Copied',
|
|
150
|
+
'action.copy': 'Copy this reply',
|
|
151
|
+
'action.tail-actions': 'Actions for this turn',
|
|
148
152
|
'editor.title': 'Edit this message',
|
|
149
153
|
'editor.title-reply': 'Edit this reply',
|
|
150
154
|
'editor.placeholder': 'Write what you want to have said. Saving replaces this message in place; the conversation after it stays as it is.',
|
|
@@ -422,6 +426,18 @@ window.__ModuleLoader__.load({
|
|
|
422
426
|
// variable resolves on the action bar (and everywhere above it); the
|
|
423
427
|
// fallback is the value it resolves to in the default theme.
|
|
424
428
|
'.dshet-action{width:28px;height:28px;padding:6px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:28px;background:transparent;color:var(--dsw-alias-label-tertiary,#81858c);cursor:pointer;transition:background-color .12s,color .12s}',
|
|
429
|
+
// The fallback strip for a turn with no closing body.
|
|
430
|
+
//
|
|
431
|
+
// The gate is the host's OWN marker: TurnTailNodeView renders
|
|
432
|
+
// `data-actions-reveal` on the tail container exactly on the branch that
|
|
433
|
+
// also draws the official action row, and never on the branch that has
|
|
434
|
+
// none. So one rule hides this strip wherever the official one exists -
|
|
435
|
+
// the two can never be drawn together, and no observer is needed.
|
|
436
|
+
'.dshet-tail{display:inline-flex;align-items:center;gap:2px;margin-left:4px}',
|
|
437
|
+
'[data-turn-tail][data-actions-reveal] .dshet-tail{display:none}',
|
|
438
|
+
'.dshet-tail[data-reveal=always]{opacity:1}',
|
|
439
|
+
'@media (hover:hover){.dshet-tail[data-reveal=hover]{opacity:0;transition:opacity 80ms}[data-turn-tail]:hover .dshet-tail[data-reveal=hover],[data-turn-tail]:focus-within .dshet-tail[data-reveal=hover]{opacity:1}}',
|
|
440
|
+
'.dshet-tail-action[data-copied=1]{color:var(--dshet-accent,#4d6bfe)}',
|
|
425
441
|
'.dshet-action:hover:not(:disabled){background:var(--dshet-chip);box-shadow:inset 0 0 0 1px var(--dshet-line);color:var(--dsw-alias-label-primary,var(--dshet-ink))}',
|
|
426
442
|
'.dshet-action:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,var(--dshet-accent));outline-offset:2px}',
|
|
427
443
|
'.dshet-action:disabled{cursor:default;opacity:.4}',
|
|
@@ -577,12 +593,18 @@ window.__ModuleLoader__.load({
|
|
|
577
593
|
].join('')
|
|
578
594
|
|
|
579
595
|
const TAG_ID = 'dsh-edit-turn/edit-turn.css'
|
|
580
|
-
if (typeof document !== 'undefined'
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
tag
|
|
585
|
-
|
|
596
|
+
if (typeof document !== 'undefined') {
|
|
597
|
+
// Reused, not skipped: an upgrade over a live page finds the tag the
|
|
598
|
+
// PREVIOUS build injected, and leaving it alone kept the new rules out of
|
|
599
|
+
// the page entirely.
|
|
600
|
+
let tag = document.querySelector('style[data-plugin-css=' + JSON.stringify(TAG_ID) + ']')
|
|
601
|
+
if (tag === null) {
|
|
602
|
+
tag = document.createElement('style')
|
|
603
|
+
tag.dataset.plugin = NS
|
|
604
|
+
tag.dataset.pluginCss = TAG_ID
|
|
605
|
+
document.head.appendChild(tag)
|
|
606
|
+
}
|
|
607
|
+
if (tag.textContent !== CSS) tag.textContent = CSS
|
|
586
608
|
}
|
|
587
609
|
|
|
588
610
|
// --- icon -----------------------------------------------------------------
|
|
@@ -1913,6 +1935,17 @@ window.__ModuleLoader__.load({
|
|
|
1913
1935
|
const COPY_TIMER = 'dshetCopyTimer'
|
|
1914
1936
|
/** The host draws its own check for a second; this is the same second. */
|
|
1915
1937
|
const COPY_FEEDBACK_MS = 1000
|
|
1938
|
+
/**
|
|
1939
|
+
* Row key -> the text that row's copy button must hand over.
|
|
1940
|
+
*
|
|
1941
|
+
* The host's copy button copies the message the row was drawn for, so on a row
|
|
1942
|
+
* this plugin rewrote it would copy the wording the user just replaced. The
|
|
1943
|
+
* press is answered from a document-level listener keyed by ROW rather than by
|
|
1944
|
+
* button node: the host re-renders its action bar, and a listener planted on
|
|
1945
|
+
* the button it drew goes away with that node (the button it draws next is a
|
|
1946
|
+
* different one).
|
|
1947
|
+
*/
|
|
1948
|
+
const rowCopyText = new Map()
|
|
1916
1949
|
|
|
1917
1950
|
/**
|
|
1918
1951
|
* A button another plugin put in this bar.
|
|
@@ -1977,6 +2010,13 @@ window.__ModuleLoader__.load({
|
|
|
1977
2010
|
* takeover that cannot copy is worse than none.
|
|
1978
2011
|
*/
|
|
1979
2012
|
function takeOverRowCopy(row, text, t) {
|
|
2013
|
+
// Registered before the button lookup on purpose: the handover has to work
|
|
2014
|
+
// even when this pass cannot find the button to mark (the next one may, and
|
|
2015
|
+
// the document-level press answers from this table either way).
|
|
2016
|
+
{
|
|
2017
|
+
const key = row.getAttribute('data-chat-flow-key')
|
|
2018
|
+
if (typeof key === 'string' && key !== '') rowCopyText.set(key, text)
|
|
2019
|
+
}
|
|
1980
2020
|
const button = nativeCopyButton(row, t)
|
|
1981
2021
|
if (button === null) return
|
|
1982
2022
|
const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
|
|
@@ -2105,6 +2145,8 @@ window.__ModuleLoader__.load({
|
|
|
2105
2145
|
|
|
2106
2146
|
/** The copy handover of a row, wherever the last pass left it. */
|
|
2107
2147
|
function releaseRowCopy(row) {
|
|
2148
|
+
const key = row.getAttribute('data-chat-flow-key')
|
|
2149
|
+
if (typeof key === 'string' && key !== '') rowCopyText.delete(key)
|
|
2108
2150
|
const button = nativeCopyButton(row)
|
|
2109
2151
|
if (button !== null) releaseCopyButton(button)
|
|
2110
2152
|
}
|
|
@@ -3695,9 +3737,12 @@ window.__ModuleLoader__.load({
|
|
|
3695
3737
|
error.className = 'dshet-error'
|
|
3696
3738
|
error.setAttribute('role', 'status')
|
|
3697
3739
|
const detail = typeof view.failureDetail === 'string' && view.failureDetail !== '' ? view.failureDetail : null
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3740
|
+
// The host's own sentence is always shown: a code-only message leaves the
|
|
3741
|
+
// reader with nothing to act on, and it is what made a real 400 impossible
|
|
3742
|
+
// to diagnose.
|
|
3743
|
+
error.textContent = detail === null
|
|
3744
|
+
? errorText(t, failureCode)
|
|
3745
|
+
: `${errorText(t, failureCode)}(${detail.slice(0, 300)})`
|
|
3701
3746
|
box.appendChild(error)
|
|
3702
3747
|
}
|
|
3703
3748
|
|
|
@@ -4531,6 +4576,211 @@ window.__ModuleLoader__.load({
|
|
|
4531
4576
|
})
|
|
4532
4577
|
}
|
|
4533
4578
|
|
|
4579
|
+
// --- the fallback strip ---------------------------------------------------
|
|
4580
|
+
//
|
|
4581
|
+
// TurnTailNodeView, when a turn's model reply has no closing body
|
|
4582
|
+
// (`closing === null`), returns the turnTail slot alone and never renders
|
|
4583
|
+
// `conversation.chat.assistant-actions`. Every action on that row goes with
|
|
4584
|
+
// it - this plugin's pencil, and the platform's own copy, branch, feedback
|
|
4585
|
+
// and usage buttons too. Nothing a plugin registers inside a strip that is
|
|
4586
|
+
// not rendered can bring those back, so the fallback lives in the one slot
|
|
4587
|
+
// that branch DOES render.
|
|
4588
|
+
//
|
|
4589
|
+
// It is not a second bar for every turn: the host puts `data-actions-reveal`
|
|
4590
|
+
// on the tail container only on the branch that draws its own strip, and one
|
|
4591
|
+
// CSS rule hides this one wherever that attribute is present.
|
|
4592
|
+
|
|
4593
|
+
/**
|
|
4594
|
+
* The reply this turn's fallback strip acts on.
|
|
4595
|
+
*
|
|
4596
|
+
* A turn runs several steps and the strip belongs at the end of the turn,
|
|
4597
|
+
* so the LAST rewriteable reply of that turn wins - the same row the host
|
|
4598
|
+
* would have hung its own strip on.
|
|
4599
|
+
*
|
|
4600
|
+
* @param view - the controller snapshot (see readReplyView).
|
|
4601
|
+
* @param turn - the TurnLocation the slot hands over.
|
|
4602
|
+
* @returns the reply entry, or undefined when this turn has none.
|
|
4603
|
+
*/
|
|
4604
|
+
function replyForTurn(view, turn) {
|
|
4605
|
+
if (view === null || view === undefined || typeof turn !== 'number') return undefined
|
|
4606
|
+
const replies = view.replies
|
|
4607
|
+
if (replies === null || replies === undefined || typeof replies.values !== 'function') return undefined
|
|
4608
|
+
if (view.hidden === null || view.hidden === undefined || typeof view.hidden.has !== 'function') return undefined
|
|
4609
|
+
let best
|
|
4610
|
+
for (const reply of replies.values()) {
|
|
4611
|
+
if (reply === null || typeof reply !== 'object') continue
|
|
4612
|
+
if (typeof reply.seq !== 'number' || reply.turn !== turn) continue
|
|
4613
|
+
if (view.hidden.has(reply.seq)) continue
|
|
4614
|
+
if (best === undefined || reply.seq > best.seq) best = reply
|
|
4615
|
+
}
|
|
4616
|
+
return best
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4619
|
+
/**
|
|
4620
|
+
* What a copy press on the fallback strip writes.
|
|
4621
|
+
*
|
|
4622
|
+
* The model's own answer when the turn has one, and its reasoning when the
|
|
4623
|
+
* whole reply went into thinking - the case this strip exists for. Blocks
|
|
4624
|
+
* are joined the way they read, blank ones dropped so a trailing empty
|
|
4625
|
+
* block does not become a trailing newline.
|
|
4626
|
+
*
|
|
4627
|
+
* @param entry - one reply entry.
|
|
4628
|
+
* @returns the text to copy; empty when there is nothing readable.
|
|
4629
|
+
*/
|
|
4630
|
+
function replyCopyText(entry) {
|
|
4631
|
+
const blocks = entry !== null && entry !== undefined && Array.isArray(entry.blocks) ? entry.blocks : []
|
|
4632
|
+
const gather = (kind) =>
|
|
4633
|
+
blocks
|
|
4634
|
+
.filter((block) => block !== null && typeof block === 'object' && block.kind === kind)
|
|
4635
|
+
.map((block) => (typeof block.text === 'string' ? block.text : ''))
|
|
4636
|
+
.filter((text) => text.trim() !== '')
|
|
4637
|
+
.join('\n\n')
|
|
4638
|
+
const answer = gather('text')
|
|
4639
|
+
return answer !== '' ? answer : gather('reasoning')
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
function CopyIcon() {
|
|
4643
|
+
return jsx('svg', {
|
|
4644
|
+
width: 16,
|
|
4645
|
+
height: 16,
|
|
4646
|
+
viewBox: '0 0 16 16',
|
|
4647
|
+
fill: 'none',
|
|
4648
|
+
'aria-hidden': true,
|
|
4649
|
+
children: [
|
|
4650
|
+
jsx('rect', { x: 5.2, y: 5.2, width: 8, height: 8, rx: 1.6, stroke: 'currentColor', strokeWidth: 1.2 }, 0),
|
|
4651
|
+
jsx('path', { d: 'M10.8 5.2V3.6A1.6 1.6 0 0 0 9.2 2H3.6A1.6 1.6 0 0 0 2 3.6v5.6a1.6 1.6 0 0 0 1.6 1.6h1.6', stroke: 'currentColor', strokeWidth: 1.2, strokeLinecap: 'round' }, 1),
|
|
4652
|
+
],
|
|
4653
|
+
})
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
function CheckIcon() {
|
|
4657
|
+
return jsx('svg', {
|
|
4658
|
+
width: 16,
|
|
4659
|
+
height: 16,
|
|
4660
|
+
viewBox: '0 0 16 16',
|
|
4661
|
+
fill: 'none',
|
|
4662
|
+
'aria-hidden': true,
|
|
4663
|
+
children: jsx('path', { d: 'M3.2 8.4l3.2 3.2 6.4-6.4', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round' }, 0),
|
|
4664
|
+
})
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
/**
|
|
4668
|
+
* The fallback strip for a turn the host draws no action row for.
|
|
4669
|
+
*
|
|
4670
|
+
* @param props - the slot's owner props ({ turn, seq, openFile }) plus the
|
|
4671
|
+
* injected controller, the standard `useChat` and this plugin's `t`.
|
|
4672
|
+
*/
|
|
4673
|
+
function TailActionEntry(input) {
|
|
4674
|
+
const props = input !== null && typeof input === 'object' ? input : {}
|
|
4675
|
+
const turn = props.turn
|
|
4676
|
+
const controller = props.controller
|
|
4677
|
+
const t = typeof props.t === 'function' ? props.t : null
|
|
4678
|
+
const useChat = props.useChat
|
|
4679
|
+
const [view, setView] = react.useState(() => {
|
|
4680
|
+
try {
|
|
4681
|
+
return readReplyView(controller)
|
|
4682
|
+
} catch {
|
|
4683
|
+
return null
|
|
4684
|
+
}
|
|
4685
|
+
})
|
|
4686
|
+
const [copied, setCopied] = react.useState(false)
|
|
4687
|
+
react.useEffect(() => {
|
|
4688
|
+
let unsubscribe = null
|
|
4689
|
+
try {
|
|
4690
|
+
setView(readReplyView(controller))
|
|
4691
|
+
if (controller !== null && controller !== undefined && typeof controller.subscribe === 'function') {
|
|
4692
|
+
unsubscribe = controller.subscribe(() => setView(readReplyView(controller)))
|
|
4693
|
+
}
|
|
4694
|
+
} catch {
|
|
4695
|
+
setView(null)
|
|
4696
|
+
}
|
|
4697
|
+
return () => {
|
|
4698
|
+
if (typeof unsubscribe === 'function') unsubscribe()
|
|
4699
|
+
}
|
|
4700
|
+
}, [controller])
|
|
4701
|
+
// The host reveals the latest turn's row always and older ones on hover.
|
|
4702
|
+
// The same rule, from the same source it uses (the last turn in the
|
|
4703
|
+
// timeline), so the strip does not behave differently from the row it
|
|
4704
|
+
// stands in for.
|
|
4705
|
+
const lastTurn = typeof useChat === 'function'
|
|
4706
|
+
? useChat((state) => {
|
|
4707
|
+
if (state === null || typeof state !== 'object') return undefined
|
|
4708
|
+
const timeline = state.timeline
|
|
4709
|
+
const order = timeline === null || timeline === undefined ? null : timeline.turnOrder
|
|
4710
|
+
if (!Array.isArray(order) || order.length === 0) return undefined
|
|
4711
|
+
return order[order.length - 1]
|
|
4712
|
+
})
|
|
4713
|
+
: undefined
|
|
4714
|
+
const entry = replyForTurn(view, turn)
|
|
4715
|
+
// Nothing this plugin can act on: the strip stays out of the way rather
|
|
4716
|
+
// than drawing an empty row.
|
|
4717
|
+
if (entry === undefined) return null
|
|
4718
|
+
const label = t === null ? '' : t('action.edit-reply')
|
|
4719
|
+
const copyLabel = t === null ? '' : t(copied ? 'action.copied' : 'action.copy')
|
|
4720
|
+
const open = (event) => {
|
|
4721
|
+
event.preventDefault()
|
|
4722
|
+
event.stopPropagation()
|
|
4723
|
+
controller.open({
|
|
4724
|
+
anchor: event.currentTarget,
|
|
4725
|
+
seq: entry.seq,
|
|
4726
|
+
mode: 'reply',
|
|
4727
|
+
turn: entry.turn,
|
|
4728
|
+
messageId: entry.messageId,
|
|
4729
|
+
text: typeof entry.text === 'string' ? entry.text : '',
|
|
4730
|
+
attachments: typeof entry.attachments === 'number' ? entry.attachments : 0,
|
|
4731
|
+
...(Array.isArray(entry.blocks) ? { blocks: entry.blocks, patch: true } : { blocks: [] }),
|
|
4732
|
+
})
|
|
4733
|
+
}
|
|
4734
|
+
const copy = (event) => {
|
|
4735
|
+
event.preventDefault()
|
|
4736
|
+
event.stopPropagation()
|
|
4737
|
+
const text = replyCopyText(entry)
|
|
4738
|
+
if (text === '') return
|
|
4739
|
+
const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
|
|
4740
|
+
? primitives.writeClipboard
|
|
4741
|
+
: typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function'
|
|
4742
|
+
? (value) => navigator.clipboard.writeText(value)
|
|
4743
|
+
: null
|
|
4744
|
+
if (write === null) return
|
|
4745
|
+
Promise.resolve()
|
|
4746
|
+
.then(() => write(text))
|
|
4747
|
+
.then(
|
|
4748
|
+
() => {
|
|
4749
|
+
setCopied(true)
|
|
4750
|
+
window.setTimeout(() => setCopied(false), 1200)
|
|
4751
|
+
},
|
|
4752
|
+
() => {},
|
|
4753
|
+
)
|
|
4754
|
+
}
|
|
4755
|
+
return jsxs('div', {
|
|
4756
|
+
className: 'dshet-tail',
|
|
4757
|
+
'data-reveal': lastTurn === turn ? 'always' : 'hover',
|
|
4758
|
+
role: 'toolbar',
|
|
4759
|
+
'aria-label': t === null ? undefined : t('action.tail-actions'),
|
|
4760
|
+
children: [
|
|
4761
|
+
jsx('button', {
|
|
4762
|
+
type: 'button',
|
|
4763
|
+
className: 'dshet-action dshet-tail-action',
|
|
4764
|
+
'aria-label': label,
|
|
4765
|
+
title: label,
|
|
4766
|
+
onClick: open,
|
|
4767
|
+
key: 'edit',
|
|
4768
|
+
children: jsx(PencilIcon, {}),
|
|
4769
|
+
}),
|
|
4770
|
+
jsx('button', {
|
|
4771
|
+
type: 'button',
|
|
4772
|
+
className: 'dshet-action dshet-tail-action',
|
|
4773
|
+
'data-copied': copied ? '1' : undefined,
|
|
4774
|
+
'aria-label': copyLabel,
|
|
4775
|
+
title: copyLabel,
|
|
4776
|
+
onClick: copy,
|
|
4777
|
+
key: 'copy',
|
|
4778
|
+
children: copied ? jsx(CheckIcon, {}) : jsx(CopyIcon, {}),
|
|
4779
|
+
}),
|
|
4780
|
+
],
|
|
4781
|
+
})
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4534
4784
|
// --- react entries --------------------------------------------------------
|
|
4535
4785
|
|
|
4536
4786
|
function OverlayEntry({ useChat, useEditTurn, controller, t }) {
|
|
@@ -4545,6 +4795,45 @@ window.__ModuleLoader__.load({
|
|
|
4545
4795
|
// left". Blur cannot be that signal: a host re-render replaces the node
|
|
4546
4796
|
// (blur without a related target) and a save temporarily disables it -
|
|
4547
4797
|
// both used to drop the caret for good.
|
|
4798
|
+
// The press that matters: the host's own copy button on a row this plugin
|
|
4799
|
+
// rewrote. Capture phase, so the host's handler (which listens at the React
|
|
4800
|
+
// root, below the document) never sees the press; by row key, so a button
|
|
4801
|
+
// the host re-rendered is still answered.
|
|
4802
|
+
react.useEffect(() => {
|
|
4803
|
+
if (typeof document.addEventListener !== 'function' || typeof document.removeEventListener !== 'function') {
|
|
4804
|
+
return undefined
|
|
4805
|
+
}
|
|
4806
|
+
const onCopyPress = (event) => {
|
|
4807
|
+
const target = event.target
|
|
4808
|
+
if (target === null || target === undefined || typeof target.closest !== 'function') return
|
|
4809
|
+
const button = target.closest('button')
|
|
4810
|
+
if (button === null) return
|
|
4811
|
+
// Our own buttons sit in the same bar: theirs is not this press.
|
|
4812
|
+
if (pluginOwned(button)) return
|
|
4813
|
+
const row = button.closest('[data-chat-flow-key]')
|
|
4814
|
+
if (row === null) return
|
|
4815
|
+
const key = row.getAttribute('data-chat-flow-key')
|
|
4816
|
+
if (typeof key !== 'string' || key === '') return
|
|
4817
|
+
const text = rowCopyText.get(key)
|
|
4818
|
+
if (typeof text !== 'string') return
|
|
4819
|
+
const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
|
|
4820
|
+
? primitives.writeClipboard
|
|
4821
|
+
: null
|
|
4822
|
+
if (write === null) return
|
|
4823
|
+
if (typeof event.preventDefault === 'function') event.preventDefault()
|
|
4824
|
+
if (typeof event.stopPropagation === 'function') event.stopPropagation()
|
|
4825
|
+
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation()
|
|
4826
|
+
Promise.resolve(write(text)).then(
|
|
4827
|
+
(ok) => {
|
|
4828
|
+
if (ok === true) flashRowCopy(button, typeof t === 'function' ? t('action.copied') : null)
|
|
4829
|
+
},
|
|
4830
|
+
() => {},
|
|
4831
|
+
)
|
|
4832
|
+
}
|
|
4833
|
+
document.addEventListener('click', onCopyPress, true)
|
|
4834
|
+
return () => document.removeEventListener('click', onCopyPress, true)
|
|
4835
|
+
}, [t])
|
|
4836
|
+
|
|
4548
4837
|
react.useEffect(() => {
|
|
4549
4838
|
if (typeof document.addEventListener !== 'function' || typeof document.removeEventListener !== 'function') {
|
|
4550
4839
|
return undefined
|
|
@@ -5012,6 +5301,23 @@ window.__ModuleLoader__.load({
|
|
|
5012
5301
|
// The row's configure arrow in the plugin manager. Without this the
|
|
5013
5302
|
// component page is a read-only overview (the card cannot be opened); the
|
|
5014
5303
|
// key is the manager's `<bundle package>#<row id>` contract.
|
|
5304
|
+
// The fallback strip, for turns the host draws no action row for (see
|
|
5305
|
+
// TailActionEntry). Order 40 keeps it after the shipped contributors
|
|
5306
|
+
// (schedule sits at 20). It is hidden by CSS wherever the host's own strip
|
|
5307
|
+
// exists, so ordering only matters on the turns it appears at all.
|
|
5308
|
+
ctx.slots.inject('conversation.chat.turnTail', () =>
|
|
5309
|
+
ctx.slots.register(
|
|
5310
|
+
{
|
|
5311
|
+
name: 'conversation.chat.turnTail',
|
|
5312
|
+
id: 'edit-turn-tail',
|
|
5313
|
+
order: 40,
|
|
5314
|
+
locale: NS,
|
|
5315
|
+
inject: (sessionId) => ({ controller: controllerFor(sessionId) }),
|
|
5316
|
+
},
|
|
5317
|
+
TailActionEntry,
|
|
5318
|
+
),
|
|
5319
|
+
)
|
|
5320
|
+
|
|
5015
5321
|
ctx.slots.inject('plugins.row.config', () =>
|
|
5016
5322
|
ctx.slots.register(
|
|
5017
5323
|
{
|
|
@@ -5062,7 +5368,7 @@ window.__ModuleLoader__.load({
|
|
|
5062
5368
|
const ROUTE_PREFIX = '/dsh-rerun-turn'
|
|
5063
5369
|
|
|
5064
5370
|
/** Keep in sync with package.json and lib/index.js. */
|
|
5065
|
-
const PLUGIN_VERSION = '0.2.
|
|
5371
|
+
const PLUGIN_VERSION = '0.2.6'
|
|
5066
5372
|
|
|
5067
5373
|
// --- copy -----------------------------------------------------------------
|
|
5068
5374
|
|
|
@@ -8732,7 +9038,7 @@ window.__ModuleLoader__.load({
|
|
|
8732
9038
|
const NS = 'dsh-as-aistudio'
|
|
8733
9039
|
|
|
8734
9040
|
/** Keep in sync with package.json and src/components.js. */
|
|
8735
|
-
const PLUGIN_VERSION = '0.
|
|
9041
|
+
const PLUGIN_VERSION = '0.4.1'
|
|
8736
9042
|
|
|
8737
9043
|
/** Presence marker the live verifier reads. */
|
|
8738
9044
|
const DEBUG_KEY = '__DSH_AS_AISTUDIO__'
|
package/src/components.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
export const STUDIO_ID = 'dsh-as-aistudio'
|
|
27
27
|
|
|
28
28
|
/** Keep in sync with package.json and src/client.js. */
|
|
29
|
-
export const STUDIO_VERSION = '0.
|
|
29
|
+
export const STUDIO_VERSION = '0.4.1'
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* The AI Studio action strip this studio composes, in the order the user meets
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// A component copied into this package must NOT get its own copy of
|
|
4
4
|
// `@deepseek-ai/dsh-session`: two module instances mean two registries, and the
|
|
5
5
|
// second copy's `appendPluginRecord` cannot touch a Session the host created
|
|
6
|
-
// (the brand check inside it throws).
|
|
7
|
-
//
|
|
8
|
-
// expects.
|
|
6
|
+
// (the brand check inside it throws). So the copies import THIS file, and this
|
|
7
|
+
// file resolves that one package from the running harness.
|
|
9
8
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// (`@deepseek-ai/dsh-tools` used to be here as well, for the agent tool that
|
|
10
|
+
// dsh-edit-turn registered. That tool is gone - the family exposes no model-facing
|
|
11
|
+
// tools - so the studio no longer needs the package at all.) The harness lives in the `dsh` installation, so the base
|
|
12
12
|
// is found by walking up from this file to the nearest node_modules that has it,
|
|
13
13
|
// with the profile's own tree as the second try. A failure here is a failure of
|
|
14
14
|
// the whole component row and says exactly which package was missing - silence
|
|
@@ -125,8 +125,5 @@ async function importPlatformOrThrow(spec) {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/** `@deepseek-ai/dsh-tools`, resolved from the running harness. */
|
|
129
|
-
export const dshTools = await importPlatformOrThrow('@deepseek-ai/dsh-tools')
|
|
130
|
-
|
|
131
128
|
/** `@deepseek-ai/dsh-session`, resolved from the running harness. */
|
|
132
129
|
export const dshSession = await importPlatformOrThrow('@deepseek-ai/dsh-session')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-edit-turn host half (dsh-edit-turn.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-edit-turn/lib/index.js (v0.5.
|
|
3
|
+
// Source: ../dsh-edit-turn/lib/index.js (v0.5.7, sha256 32dc3ed4d66bb063)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -46,9 +46,8 @@
|
|
|
46
46
|
// is why an `assistant/message` carrier is impossible (it forbids
|
|
47
47
|
// `sourceEventSeqs` outright).
|
|
48
48
|
//
|
|
49
|
-
// The module resolves `@deepseek-ai/schemastery` defensively.
|
|
50
|
-
//
|
|
51
|
-
// package is a regular dependency: when it cannot be resolved - a pruned
|
|
49
|
+
// The module resolves `@deepseek-ai/schemastery` defensively. The schema package
|
|
50
|
+
// is a regular dependency: when it cannot be resolved - a pruned
|
|
52
51
|
// package cache, an installation that skipped dependencies - a static import
|
|
53
52
|
// takes the whole plugin down and the feature simply disappears from the UI
|
|
54
53
|
// with nothing but a line on the host's stdout to explain it. A guarded require
|
|
@@ -57,7 +56,6 @@
|
|
|
57
56
|
import { randomUUID } from 'node:crypto'
|
|
58
57
|
import { readFile } from 'node:fs/promises'
|
|
59
58
|
import { createRequire } from 'node:module'
|
|
60
|
-
import { dshTools } from './_platform.js'
|
|
61
59
|
import { dshSession as sessionApi } from './_platform.js'
|
|
62
60
|
import {
|
|
63
61
|
PATCH_TYPE,
|
|
@@ -70,9 +68,6 @@ import {
|
|
|
70
68
|
projectedBlocks,
|
|
71
69
|
} from './dsh-edit-turn.patch.js'
|
|
72
70
|
|
|
73
|
-
const { defineTool } = dshTools
|
|
74
|
-
|
|
75
|
-
|
|
76
71
|
const nodeRequire = createRequire(import.meta.url)
|
|
77
72
|
const schemaPackage = (() => {
|
|
78
73
|
try {
|
|
@@ -90,7 +85,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
|
|
|
90
85
|
export const name = PLUGIN_ID
|
|
91
86
|
|
|
92
87
|
/** Keep in sync with package.json and lib/client.js. */
|
|
93
|
-
export const PLUGIN_VERSION = '0.5.
|
|
88
|
+
export const PLUGIN_VERSION = '0.5.7'
|
|
94
89
|
|
|
95
90
|
// A ring of the last few requests, answering "did the browser half even ask, and
|
|
96
91
|
// what did it get?" without needing its console. Read-only diagnostics: the
|
|
@@ -2345,62 +2340,6 @@ function sessionIdFromExec(exec) {
|
|
|
2345
2340
|
return undefined
|
|
2346
2341
|
}
|
|
2347
2342
|
|
|
2348
|
-
function registerTool(ctx, config) {
|
|
2349
|
-
const tools = ctx.get('tools')
|
|
2350
|
-
if (!tools || typeof tools.register !== 'function') return
|
|
2351
|
-
tools.register(
|
|
2352
|
-
defineTool({
|
|
2353
|
-
name: 'edit_turn_targets',
|
|
2354
|
-
description:
|
|
2355
|
-
'List the user turns and model replies of a DeepSeek Harness session that the edit-turn plugin can rewrite, with each message\'s current text and, for a reply, every editable block (output text and reasoning). A prompt is replaced where it stands; a reply block is patched in place when the host supports projections, otherwise the reply correction is appended and the later turns are replayed behind it. Read-only: it changes nothing.',
|
|
2356
|
-
parameters: {
|
|
2357
|
-
sessionId: {
|
|
2358
|
-
type: 'string',
|
|
2359
|
-
description: 'Session id to inspect. Defaults to the session this agent is running in.',
|
|
2360
|
-
},
|
|
2361
|
-
limit: { type: 'number', description: 'Maximum number of turns to return (default 20).' },
|
|
2362
|
-
},
|
|
2363
|
-
output: {
|
|
2364
|
-
schema: { type: 'string' },
|
|
2365
|
-
render: (_args, value) => [{ type: 'text', text: value }],
|
|
2366
|
-
},
|
|
2367
|
-
async execute(args, exec) {
|
|
2368
|
-
const sessionId = typeof args.sessionId === 'string' && args.sessionId !== '' ? args.sessionId : sessionIdFromExec(exec)
|
|
2369
|
-
if (sessionId === undefined) {
|
|
2370
|
-
throw new Error('no sessionId was supplied and the calling agent has no session')
|
|
2371
|
-
}
|
|
2372
|
-
const events = await readEvents(ctx, sessionId)
|
|
2373
|
-
if (!events) throw new Error(`no session log for ${sessionId}`)
|
|
2374
|
-
const nodes = surfaceOf(ctx, sessionId, events)
|
|
2375
|
-
const patches = patchesOf(events)
|
|
2376
|
-
const turns = editableTurns(events, nodes)
|
|
2377
|
-
const replies = editableReplies(events, nodes, { patches, patch: config && config.patch === true })
|
|
2378
|
-
const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.floor(args.limit) : 20
|
|
2379
|
-
const shown = turns.slice(-limit)
|
|
2380
|
-
const shownReplies = replies.slice(-limit)
|
|
2381
|
-
const lines = [
|
|
2382
|
-
`session ${sessionId}: ${turns.length} editable turn(s), ${replies.length} editable reply/replies, ${isBusy(events) ? 'BUSY' : 'idle'}`,
|
|
2383
|
-
]
|
|
2384
|
-
for (const turn of shown) {
|
|
2385
|
-
const preview = turn.text.length > 160 ? `${turn.text.slice(0, 160)}...` : turn.text
|
|
2386
|
-
lines.push(`- turn ${turn.turn} (seq ${turn.seq}, ${turn.attachments} attachment(s)): ${preview}`)
|
|
2387
|
-
}
|
|
2388
|
-
if (turns.length > shown.length) lines.push(`(${turns.length - shown.length} earlier turn(s) omitted)`)
|
|
2389
|
-
for (const reply of shownReplies) {
|
|
2390
|
-
const preview = reply.text.length > 160 ? `${reply.text.slice(0, 160)}...` : reply.text
|
|
2391
|
-
const blocks = Array.isArray(reply.blocks)
|
|
2392
|
-
? ` [${reply.blocks.map((block) => `${block.index}:${block.kind}${block.patched ? '*' : ''}`).join(', ')}]`
|
|
2393
|
-
: ''
|
|
2394
|
-
lines.push(`- reply (seq ${reply.seq}, ${reply.attachments} non-text part(s))${blocks}: ${preview}`)
|
|
2395
|
-
}
|
|
2396
|
-
if (replies.length > shownReplies.length) lines.push(`(${replies.length - shownReplies.length} earlier reply/replies omitted)`)
|
|
2397
|
-
lines.push('editing a turn replaces its wording in place (no model call); editing a reply block patches it in place when the host supports plugin-record projections, otherwise it replaces the reply and replays the tail. Re-running a turn is dsh-rerun-turn\'s button.')
|
|
2398
|
-
return lines.join('\n')
|
|
2399
|
-
},
|
|
2400
|
-
}),
|
|
2401
|
-
)
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
2343
|
// --- http --------------------------------------------------------------------
|
|
2405
2344
|
|
|
2406
2345
|
function isLoopbackAddress(address) {
|
|
@@ -2802,12 +2741,6 @@ export function apply(ctx, config) {
|
|
|
2802
2741
|
ctx.inject(['webServer'], (sub) => registerRoutes(sub.webServer, sub))
|
|
2803
2742
|
}
|
|
2804
2743
|
|
|
2805
|
-
const tools = ctx.get('tools')
|
|
2806
|
-
if (tools) {
|
|
2807
|
-
registerTool(ctx, resolved)
|
|
2808
|
-
} else {
|
|
2809
|
-
ctx.inject(['tools'], (sub) => registerTool(sub, resolved))
|
|
2810
|
-
}
|
|
2811
2744
|
}
|
|
2812
2745
|
|
|
2813
2746
|
export default apply
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-edit-turn host half (dsh-edit-turn.patch.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-edit-turn/lib/patch.js (v0.5.
|
|
3
|
+
// Source: ../dsh-edit-turn/lib/patch.js (v0.5.7, sha256 5e1e8cce1d168f6c)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-rerun-turn host half (dsh-rerun-turn.js), VENDORED by tools/vendor.mjs - DO NOT EDIT.
|
|
2
2
|
//
|
|
3
|
-
// Source: ../dsh-rerun-turn/lib/index.js (v0.2.
|
|
3
|
+
// Source: ../dsh-rerun-turn/lib/index.js (v0.2.6, sha256 3e20b1a0a07cf213)
|
|
4
4
|
// Regenerate with `npm run vendor`; test/sync.test.js fails when this is stale.
|
|
5
5
|
//
|
|
6
6
|
// src/shell.js imports the copy named after the component, which is why the
|
|
@@ -90,7 +90,7 @@ export const PLUGIN_ID = 'dsh-rerun-turn'
|
|
|
90
90
|
export const name = PLUGIN_ID
|
|
91
91
|
|
|
92
92
|
/** Keep in sync with package.json and lib/client.js. */
|
|
93
|
-
export const PLUGIN_VERSION = '0.2.
|
|
93
|
+
export const PLUGIN_VERSION = '0.2.6'
|
|
94
94
|
|
|
95
95
|
const ROUTE_PREFIX = '/dsh-rerun-turn'
|
|
96
96
|
// A session id is an OPAQUE string to DSH: the host's own schema is
|
|
@@ -1182,6 +1182,35 @@ export const ADAPTER_DROPS_EMPTY_USER_CONTENT = probeAdapterDropsEmptyUserConten
|
|
|
1182
1182
|
* injectable so the shape is testable without this machine's adapter cache.
|
|
1183
1183
|
* @returns a one-element `{ type, data }` list for `session.append`.
|
|
1184
1184
|
*/
|
|
1185
|
+
/**
|
|
1186
|
+
* Normalize a shadowed-seq list to the shape the host's contract demands.
|
|
1187
|
+
*
|
|
1188
|
+
* `assertSourceEventReferences` requires a non-empty array of non-negative safe
|
|
1189
|
+
* integers, no duplicates, every entry earlier than the citing event. A list that
|
|
1190
|
+
* is nested (a range written as a pair), holds a non-number, or repeats a seq makes
|
|
1191
|
+
* the whole event unadoptable: the host's strict readers (`adoptSessionEvent`,
|
|
1192
|
+
* `snapshotSessionEvent`, `foldSurface`) reject it outright, so a log carrying one
|
|
1193
|
+
* can only be opened by the lenient path.
|
|
1194
|
+
*
|
|
1195
|
+
* @param shadowed - candidate list from a plan or a recovered record.
|
|
1196
|
+
* @returns a flat, ascending, de-duplicated list of seqs.
|
|
1197
|
+
*/
|
|
1198
|
+
export function normalizeShadowed(shadowed) {
|
|
1199
|
+
const flat = []
|
|
1200
|
+
const visit = (value) => {
|
|
1201
|
+
if (Array.isArray(value)) {
|
|
1202
|
+
for (const inner of value) visit(inner)
|
|
1203
|
+
return
|
|
1204
|
+
}
|
|
1205
|
+
// 只认真正的数字:`Number(null)` 是 0,而 0 在 0 基日志里是个合法 seq,
|
|
1206
|
+
// 用 Number() 强转等于往引用列表里塞一条假引用。
|
|
1207
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) return
|
|
1208
|
+
flat.push(value)
|
|
1209
|
+
}
|
|
1210
|
+
visit(shadowed)
|
|
1211
|
+
return [...new Set(flat)].sort((a, b) => a - b)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1185
1214
|
export function buildShadowWrites(
|
|
1186
1215
|
plan,
|
|
1187
1216
|
rerunId,
|
|
@@ -1192,7 +1221,8 @@ export function buildShadowWrites(
|
|
|
1192
1221
|
{
|
|
1193
1222
|
type: 'user/message',
|
|
1194
1223
|
surfaceOp: { op: 'replace', startSeq: plan.startSeq, endSeq: plan.endSeq },
|
|
1195
|
-
|
|
1224
|
+
// 写入前规范化:嵌套数组/重复项/非整数都会让宿主的严格校验器拒绝整条事件。
|
|
1225
|
+
sourceEventSeqs: normalizeShadowed(plan.shadowed),
|
|
1196
1226
|
data: {
|
|
1197
1227
|
id: randomUUID(),
|
|
1198
1228
|
role: 'user',
|
|
@@ -1978,8 +2008,10 @@ async function performReplay(ctx, sessionId, rerunId, config) {
|
|
|
1978
2008
|
logTo: record.logTo ?? record.endSeq,
|
|
1979
2009
|
promptSeq: record.promptSeq ?? record.startSeq,
|
|
1980
2010
|
// The replay copies exactly these surface nodes (and nothing the log
|
|
1981
|
-
// happens to hold between them), so the shadowed list must travel
|
|
1982
|
-
|
|
2011
|
+
// happens to hold between them), so the shadowed list must travel - and it
|
|
2012
|
+
// travels normalized, because a record read back from an older log may hold
|
|
2013
|
+
// the nested shape this plugin used to write.
|
|
2014
|
+
shadowed: normalizeShadowed(record.shadowed),
|
|
1983
2015
|
}
|
|
1984
2016
|
const startSeq = session.seq
|
|
1985
2017
|
const writes = buildReplayWrites(events, plan, baseTurn, rerunId, startSeq, !promptLanded)
|
package/src/vendor/versions.json
CHANGED