dsh-as-aistudio 0.4.1 → 0.4.3
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 +16 -0
- package/package.json +1 -1
- package/src/client.js +190 -32
- package/src/components.js +1 -1
- package/src/vendor/host/dsh-edit-turn.js +74 -3
- package/src/vendor/host/dsh-edit-turn.patch.js +1 -1
- package/src/vendor/versions.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
## 中文
|
|
6
6
|
|
|
7
|
+
**0.4.3** —— 编辑入口不再"等半天才出现"(`/state` 从 25 MB 降到几百 KB)。
|
|
8
|
+
|
|
9
|
+
- **组件跟进**:`dsh-edit-turn` **0.5.9**(另三个不变:`dsh-rerun-turn` 0.2.6、`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
|
|
10
|
+
|
|
11
|
+
- **症状**:编辑笔要等很久才出现;而且在这之前按复制,拿到的是改写前的原文。
|
|
12
|
+
|
|
13
|
+
- **根因(两件事同一个原因)**:`dsh-edit-turn` 的 `/state` 把每条回复的**整轮**推理与正文全文都发了出去。实测一个会话 291 条回复 = **27.9 MB**,一次 `/state` 要 **13 秒**。这段时间里浏览器半部没有任何编辑条目 —— 笔不出现,复制也就不会被插件接管。
|
|
14
|
+
|
|
15
|
+
- **修法**:`/state` 的 `replies` 只发身份(`seq` / `turn` / `messageId` / `attachments` / `textLength` / `blockCount`),全文与分块走新的 `/reply` 接口,只在按复制或打开编辑器时取一次。
|
|
16
|
+
|
|
17
|
+
**0.4.2** —— 复制拿到改写后的文本(改到平台写入器这一层)。
|
|
18
|
+
|
|
19
|
+
- **组件跟进**:`dsh-edit-turn` **0.5.8**(另三个不变:`dsh-rerun-turn` 0.2.6、`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
|
|
20
|
+
|
|
21
|
+
- 0.4.1 里跟进的 0.5.7 没修好:它把"回答复制按下"从按钮节点挪到文档级捕获监听,实测仍复制出原文,说明这次按下没有经过那条路。0.5.8 换到**所有复制都要经过的唯一关口**——`navigator.clipboard.writeText`(宿主自己的 `writeClipboard` 原语内部就是调它):写入的文本如果正是某条被改写消息的原文,就换成改写后的文本,其余写入一字不动。登记表放在页级(`globalThis` 上的 Symbol),因为实时重载后旧 bundle 装的包装器仍在写入路径上。
|
|
22
|
+
|
|
7
23
|
**0.4.1** —— 编辑过的消息,复制按钮被宿主重画之后仍然复制改写后的文本。
|
|
8
24
|
|
|
9
25
|
- **组件跟进**:`dsh-edit-turn` **0.5.7**(另三个不变:`dsh-rerun-turn` 0.2.6、`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-as-aistudio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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.9'
|
|
47
47
|
|
|
48
48
|
// --- copy -----------------------------------------------------------------
|
|
49
49
|
|
|
@@ -139,6 +139,7 @@ window.__ModuleLoader__.load({
|
|
|
139
139
|
'error.attachment-too-large': '这个文件太大,无法作为附件。',
|
|
140
140
|
'error.too-large': '这次修改太大,服务器没有接收。',
|
|
141
141
|
'error.replay-partial': '已保存并改写了这条回答,但后面的轮次没能全部重放回来,请重新打开会话确认。',
|
|
142
|
+
'error.reply-not-found': '这条回答已经不可编辑了(可能已被改写或不在当前上下文里)。',
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
const en = {
|
|
@@ -228,6 +229,7 @@ window.__ModuleLoader__.load({
|
|
|
228
229
|
'error.attachment-too-large': 'That file is too large to attach.',
|
|
229
230
|
'error.too-large': 'That revision was too large for the host to accept.',
|
|
230
231
|
'error.replay-partial': 'Saved and corrected, but the turns after it could not all be replayed; reopen the session to check.',
|
|
232
|
+
'error.reply-not-found': 'That reply is no longer editable (it may have been rewritten, or left the current context).',
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
// Every error code the host can return travels through here. A code is
|
|
@@ -643,6 +645,9 @@ window.__ModuleLoader__.load({
|
|
|
643
645
|
this.sessionId = sessionId
|
|
644
646
|
this.listeners = new Set()
|
|
645
647
|
this.inflight = null
|
|
648
|
+
// One reply's full wording, fetched on demand and kept: `/state` ships
|
|
649
|
+
// identities only (a reply carries its whole turn's text).
|
|
650
|
+
this.replyBodies = new Map()
|
|
646
651
|
this.animateOnce = false
|
|
647
652
|
this.rowCount = -1
|
|
648
653
|
this.editor = null
|
|
@@ -747,6 +752,32 @@ window.__ModuleLoader__.load({
|
|
|
747
752
|
return value
|
|
748
753
|
}
|
|
749
754
|
|
|
755
|
+
/**
|
|
756
|
+
* One reply's full wording and blocks, on demand.
|
|
757
|
+
*
|
|
758
|
+
* `/state` publishes identities only: a reply carries its whole turn's
|
|
759
|
+
* reasoning and answer text, and a long session's list ran to 27.9 MB -
|
|
760
|
+
* 13 seconds for one refresh, which is why the edit entry appeared so late.
|
|
761
|
+
*
|
|
762
|
+
* @param {string} messageId - the reply's message id.
|
|
763
|
+
* @returns {Promise<object|null>} `{ text, blocks, attachments }`, or null.
|
|
764
|
+
*/
|
|
765
|
+
replyBody(messageId) {
|
|
766
|
+
if (typeof messageId !== 'string' || messageId === '') return Promise.resolve(null)
|
|
767
|
+
const cached = this.replyBodies.get(messageId)
|
|
768
|
+
if (cached !== undefined) return cached
|
|
769
|
+
const url = `${ROUTE_PREFIX}/reply?sessionId=${encodeURIComponent(this.sessionId)}&messageId=${encodeURIComponent(messageId)}`
|
|
770
|
+
const pending = fetch(url, { headers: { accept: 'application/json' } })
|
|
771
|
+
.then(async (res) => {
|
|
772
|
+
const data = await res.json().catch(() => ({}))
|
|
773
|
+
if (!res.ok || !data.ok) throw new Error(data && data.error ? String(data.error) : `HTTP ${res.status}`)
|
|
774
|
+
return { text: data.text, blocks: Array.isArray(data.blocks) ? data.blocks : [], attachments: data.attachments }
|
|
775
|
+
})
|
|
776
|
+
.catch(() => null)
|
|
777
|
+
this.replyBodies.set(messageId, pending)
|
|
778
|
+
return pending
|
|
779
|
+
}
|
|
780
|
+
|
|
750
781
|
load(force) {
|
|
751
782
|
// A refresh asked for while one is already running has to land *after*
|
|
752
783
|
// it: handing back the in-flight response would answer with a snapshot
|
|
@@ -1935,6 +1966,10 @@ window.__ModuleLoader__.load({
|
|
|
1935
1966
|
const COPY_TIMER = 'dshetCopyTimer'
|
|
1936
1967
|
/** The host draws its own check for a second; this is the same second. */
|
|
1937
1968
|
const COPY_FEEDBACK_MS = 1000
|
|
1969
|
+
/** 标记:这一页的剪贴板写入已经被本半部包过一次(重载会复用同一个 page)。 */
|
|
1970
|
+
const CLIPBOARD_WRAPPED = Symbol.for('dshetClipboardWrapped')
|
|
1971
|
+
/** 页级登记表:重载后新 bundle 与旧包装器共用同一份。 */
|
|
1972
|
+
const COPY_REGISTRY = Symbol.for('dshetCopyRegistry')
|
|
1938
1973
|
/**
|
|
1939
1974
|
* Row key -> the text that row's copy button must hand over.
|
|
1940
1975
|
*
|
|
@@ -1945,7 +1980,25 @@ window.__ModuleLoader__.load({
|
|
|
1945
1980
|
* the button it drew goes away with that node (the button it draws next is a
|
|
1946
1981
|
* different one).
|
|
1947
1982
|
*/
|
|
1948
|
-
|
|
1983
|
+
/**
|
|
1984
|
+
* Row key -> the text that row's copy must hand over, and the wording the host
|
|
1985
|
+
* would hand over instead.
|
|
1986
|
+
*
|
|
1987
|
+
* Both live on the PAGE rather than in this module: a live reload builds a new
|
|
1988
|
+
* bundle with new maps, and the clipboard wrapper the previous bundle installed
|
|
1989
|
+
* is still the one in the write path (wrapping is idempotent, by design). Two
|
|
1990
|
+
* maps would leave that wrapper reading a registry nobody fills.
|
|
1991
|
+
*/
|
|
1992
|
+
const copyRegistry = (() => {
|
|
1993
|
+
const page = typeof globalThis !== 'undefined' ? globalThis : {}
|
|
1994
|
+
const existing = page[COPY_REGISTRY]
|
|
1995
|
+
if (existing !== undefined && existing !== null && existing.text instanceof Map) return existing
|
|
1996
|
+
const created = { text: new Map(), host: new Map() }
|
|
1997
|
+
page[COPY_REGISTRY] = created
|
|
1998
|
+
return created
|
|
1999
|
+
})()
|
|
2000
|
+
const rowCopyText = copyRegistry.text
|
|
2001
|
+
const rowHostText = copyRegistry.host
|
|
1949
2002
|
|
|
1950
2003
|
/**
|
|
1951
2004
|
* A button another plugin put in this bar.
|
|
@@ -2009,13 +2062,93 @@ window.__ModuleLoader__.load({
|
|
|
2009
2062
|
* host's own clipboard writer the button keeps the host's behaviour too: a
|
|
2010
2063
|
* takeover that cannot copy is worse than none.
|
|
2011
2064
|
*/
|
|
2065
|
+
/**
|
|
2066
|
+
* The wording the host's own copy button would hand over for this row.
|
|
2067
|
+
*
|
|
2068
|
+
* It is the message the row was drawn for - the pre-edit text - and it is read
|
|
2069
|
+
* off the row itself, because that is where it still is: this plugin hides the
|
|
2070
|
+
* row's own bubble and stands its revision in front of it.
|
|
2071
|
+
*
|
|
2072
|
+
* @param {Element} row - the row.
|
|
2073
|
+
* @returns {string|null} the host's text, or null when it cannot be read.
|
|
2074
|
+
*/
|
|
2075
|
+
function hostCopyTextOf(row) {
|
|
2076
|
+
try {
|
|
2077
|
+
const bubble = ownRevisionBubble(row)
|
|
2078
|
+
let text = ''
|
|
2079
|
+
const children = row.childNodes
|
|
2080
|
+
if (children === undefined || children === null) return null
|
|
2081
|
+
for (const child of children) {
|
|
2082
|
+
if (bubble !== null && child === bubble) continue
|
|
2083
|
+
if (typeof child.textContent === 'string') text += child.textContent
|
|
2084
|
+
}
|
|
2085
|
+
const normalized = text.replace(/\s+/g, ' ').trim()
|
|
2086
|
+
return normalized === '' ? null : normalized
|
|
2087
|
+
} catch {
|
|
2088
|
+
return null
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* The wording that must actually land on the clipboard.
|
|
2094
|
+
*
|
|
2095
|
+
* The host's copy button copies the message its row was drawn for, so on a row
|
|
2096
|
+
* this plugin rewrote it hands over the pre-edit text. The press itself is
|
|
2097
|
+
* answered earlier when this half can find the button, but the action bar is the
|
|
2098
|
+
* host's own React and it may draw a button this half never marked - so the last
|
|
2099
|
+
* say belongs to the write itself, which no button and no handler can bypass.
|
|
2100
|
+
*
|
|
2101
|
+
* @param {string} text - what the platform is about to write.
|
|
2102
|
+
* @returns {string|null} the text to write instead, or null to leave it alone.
|
|
2103
|
+
*/
|
|
2104
|
+
function rewriteCopiedText(text) {
|
|
2105
|
+
const wanted = text.replace(/\s+/g, ' ').trim()
|
|
2106
|
+
if (wanted === '') return null
|
|
2107
|
+
for (const [key, edited] of rowCopyText) {
|
|
2108
|
+
if (typeof edited !== 'string') continue
|
|
2109
|
+
if (edited.replace(/\s+/g, ' ').trim() === wanted) return null
|
|
2110
|
+
const hostText = rowHostText.get(key)
|
|
2111
|
+
if (typeof hostText === 'string' && (hostText === wanted || hostText.includes(wanted))) return edited
|
|
2112
|
+
}
|
|
2113
|
+
return null
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/**
|
|
2117
|
+
* Wrap the platform's clipboard writer, once per page.
|
|
2118
|
+
*
|
|
2119
|
+
* `navigator.clipboard.writeText` is the single choke point every copy goes
|
|
2120
|
+
* through - the host's own `writeClipboard` primitive calls it, and so does a
|
|
2121
|
+
* browser's native copy of a selection.
|
|
2122
|
+
*
|
|
2123
|
+
* @returns {() => void} undo.
|
|
2124
|
+
*/
|
|
2125
|
+
function installClipboardRewrite() {
|
|
2126
|
+
const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : undefined
|
|
2127
|
+
if (clipboard === undefined || clipboard === null || typeof clipboard.writeText !== 'function') return () => {}
|
|
2128
|
+
if (clipboard.writeText[CLIPBOARD_WRAPPED] === true) return () => {}
|
|
2129
|
+
const native = clipboard.writeText.bind(clipboard)
|
|
2130
|
+
const wrapped = (value) => {
|
|
2131
|
+
const rewritten = rewriteCopiedText(String(value ?? ''))
|
|
2132
|
+
return rewritten === null ? native(value) : native(rewritten)
|
|
2133
|
+
}
|
|
2134
|
+
wrapped[CLIPBOARD_WRAPPED] = true
|
|
2135
|
+
clipboard.writeText = wrapped
|
|
2136
|
+
return () => {
|
|
2137
|
+
if (clipboard.writeText === wrapped) clipboard.writeText = native
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2012
2141
|
function takeOverRowCopy(row, text, t) {
|
|
2013
2142
|
// Registered before the button lookup on purpose: the handover has to work
|
|
2014
2143
|
// even when this pass cannot find the button to mark (the next one may, and
|
|
2015
2144
|
// the document-level press answers from this table either way).
|
|
2016
2145
|
{
|
|
2017
2146
|
const key = row.getAttribute('data-chat-flow-key')
|
|
2018
|
-
if (typeof key === 'string' && key !== '')
|
|
2147
|
+
if (typeof key === 'string' && key !== '') {
|
|
2148
|
+
rowCopyText.set(key, text)
|
|
2149
|
+
const hostText = hostCopyTextOf(row)
|
|
2150
|
+
if (hostText !== null) rowHostText.set(key, hostText)
|
|
2151
|
+
}
|
|
2019
2152
|
}
|
|
2020
2153
|
const button = nativeCopyButton(row, t)
|
|
2021
2154
|
if (button === null) return
|
|
@@ -2146,7 +2279,10 @@ window.__ModuleLoader__.load({
|
|
|
2146
2279
|
/** The copy handover of a row, wherever the last pass left it. */
|
|
2147
2280
|
function releaseRowCopy(row) {
|
|
2148
2281
|
const key = row.getAttribute('data-chat-flow-key')
|
|
2149
|
-
if (typeof key === 'string' && key !== '')
|
|
2282
|
+
if (typeof key === 'string' && key !== '') {
|
|
2283
|
+
rowCopyText.delete(key)
|
|
2284
|
+
rowHostText.delete(key)
|
|
2285
|
+
}
|
|
2150
2286
|
const button = nativeCopyButton(row)
|
|
2151
2287
|
if (button !== null) releaseCopyButton(button)
|
|
2152
2288
|
}
|
|
@@ -4720,37 +4856,55 @@ window.__ModuleLoader__.load({
|
|
|
4720
4856
|
const open = (event) => {
|
|
4721
4857
|
event.preventDefault()
|
|
4722
4858
|
event.stopPropagation()
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4859
|
+
// `currentTarget` is null once this handler returns, so it is read before
|
|
4860
|
+
// the await that may fetch the reply's wording.
|
|
4861
|
+
const anchor = event.currentTarget
|
|
4862
|
+
void (async () => {
|
|
4863
|
+
let body = null
|
|
4864
|
+
if (!Array.isArray(entry.blocks) && typeof entry.blockCount === 'number' && entry.blockCount > 0) {
|
|
4865
|
+
body = await controller.replyBody(entry.messageId)
|
|
4866
|
+
}
|
|
4867
|
+
const blocks = body !== null && Array.isArray(body.blocks)
|
|
4868
|
+
? body.blocks
|
|
4869
|
+
: Array.isArray(entry.blocks) ? entry.blocks : []
|
|
4870
|
+
controller.open({
|
|
4871
|
+
anchor,
|
|
4872
|
+
seq: entry.seq,
|
|
4873
|
+
mode: 'reply',
|
|
4874
|
+
turn: entry.turn,
|
|
4875
|
+
messageId: entry.messageId,
|
|
4876
|
+
text: body !== null && typeof body.text === 'string'
|
|
4877
|
+
? body.text
|
|
4878
|
+
: typeof entry.text === 'string' ? entry.text : '',
|
|
4879
|
+
attachments: typeof entry.attachments === 'number' ? entry.attachments : 0,
|
|
4880
|
+
...(blocks.length > 0 ? { blocks, patch: true } : { blocks: [] }),
|
|
4881
|
+
})
|
|
4882
|
+
})()
|
|
4733
4883
|
}
|
|
4734
4884
|
const copy = (event) => {
|
|
4735
4885
|
event.preventDefault()
|
|
4736
4886
|
event.stopPropagation()
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4887
|
+
void (async () => {
|
|
4888
|
+
let text = replyCopyText(entry)
|
|
4889
|
+
if (text === '' && typeof entry.blockCount === 'number' && entry.blockCount > 0) {
|
|
4890
|
+
const body = await controller.replyBody(entry.messageId)
|
|
4891
|
+
if (body !== null) text = replyCopyText(body)
|
|
4892
|
+
}
|
|
4893
|
+
if (text === '') return
|
|
4894
|
+
const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
|
|
4895
|
+
? primitives.writeClipboard
|
|
4896
|
+
: typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function'
|
|
4897
|
+
? (value) => navigator.clipboard.writeText(value)
|
|
4898
|
+
: null
|
|
4899
|
+
if (write === null) return
|
|
4900
|
+
try {
|
|
4901
|
+
await write(text)
|
|
4902
|
+
} catch {
|
|
4903
|
+
return
|
|
4904
|
+
}
|
|
4905
|
+
setCopied(true)
|
|
4906
|
+
window.setTimeout(() => setCopied(false), 1200)
|
|
4907
|
+
})()
|
|
4754
4908
|
}
|
|
4755
4909
|
return jsxs('div', {
|
|
4756
4910
|
className: 'dshet-tail',
|
|
@@ -4791,6 +4945,10 @@ window.__ModuleLoader__.load({
|
|
|
4791
4945
|
controller.load()
|
|
4792
4946
|
}, [controller])
|
|
4793
4947
|
|
|
4948
|
+
// The last say on what lands: whatever route the press takes, the write
|
|
4949
|
+
// itself goes through here.
|
|
4950
|
+
react.useEffect(() => installClipboardRewrite(), [])
|
|
4951
|
+
|
|
4794
4952
|
// Pressing outside the editor is the one signal that means "the user
|
|
4795
4953
|
// left". Blur cannot be that signal: a host re-render replaces the node
|
|
4796
4954
|
// (blur without a related target) and a save temporarily disables it -
|
|
@@ -9038,7 +9196,7 @@ window.__ModuleLoader__.load({
|
|
|
9038
9196
|
const NS = 'dsh-as-aistudio'
|
|
9039
9197
|
|
|
9040
9198
|
/** Keep in sync with package.json and src/components.js. */
|
|
9041
|
-
const PLUGIN_VERSION = '0.4.
|
|
9199
|
+
const PLUGIN_VERSION = '0.4.3'
|
|
9042
9200
|
|
|
9043
9201
|
/** Presence marker the live verifier reads. */
|
|
9044
9202
|
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.4.
|
|
29
|
+
export const STUDIO_VERSION = '0.4.3'
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* The AI Studio action strip this studio composes, in the order the user meets
|
|
@@ -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.9, sha256 a007714c89acfae5)
|
|
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
|
|
@@ -85,7 +85,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
|
|
|
85
85
|
export const name = PLUGIN_ID
|
|
86
86
|
|
|
87
87
|
/** Keep in sync with package.json and lib/client.js. */
|
|
88
|
-
export const PLUGIN_VERSION = '0.5.
|
|
88
|
+
export const PLUGIN_VERSION = '0.5.9'
|
|
89
89
|
|
|
90
90
|
// A ring of the last few requests, answering "did the browser half even ask, and
|
|
91
91
|
// what did it get?" without needing its console. Read-only diagnostics: the
|
|
@@ -1693,6 +1693,50 @@ export async function attachmentBytes(ctx, sessionId, seq, index) {
|
|
|
1693
1693
|
|
|
1694
1694
|
// --- operations --------------------------------------------------------------
|
|
1695
1695
|
|
|
1696
|
+
/**
|
|
1697
|
+
* A reply's identity, without its words.
|
|
1698
|
+
*
|
|
1699
|
+
* One reply carries every reasoning and answer block of its **turn**, so a long
|
|
1700
|
+
* session's reply list is tens of megabytes: measured 291 replies = 27.9 MB, and
|
|
1701
|
+
* a single `/state` took 13 seconds - long enough that the browser showed no edit
|
|
1702
|
+
* entry, and slow enough to look like the plugin had not loaded at all. The words
|
|
1703
|
+
* are fetched when the user actually wants them (`/reply`).
|
|
1704
|
+
*
|
|
1705
|
+
* @param {object} entry - one entry from `editableReplies`.
|
|
1706
|
+
* @returns {object} the identity-only projection.
|
|
1707
|
+
*/
|
|
1708
|
+
export function lightReply(entry) {
|
|
1709
|
+
return {
|
|
1710
|
+
seq: entry.seq,
|
|
1711
|
+
turn: entry.turn,
|
|
1712
|
+
messageId: entry.messageId,
|
|
1713
|
+
attachments: entry.attachments,
|
|
1714
|
+
textLength: typeof entry.text === 'string' ? entry.text.length : 0,
|
|
1715
|
+
blockCount: Array.isArray(entry.blocks) ? entry.blocks.length : 0,
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* One reply's full wording and blocks, for a copy press or an opened editor.
|
|
1721
|
+
*
|
|
1722
|
+
* @param ctx - plugin context.
|
|
1723
|
+
* @param {string} sessionId - the session.
|
|
1724
|
+
* @param {object} config - resolved config.
|
|
1725
|
+
* @param {string} messageId - the reply's message id.
|
|
1726
|
+
* @returns {Promise<object|null>} `{ text, blocks, attachments }`, or null.
|
|
1727
|
+
*/
|
|
1728
|
+
async function replyOf(ctx, sessionId, config, messageId) {
|
|
1729
|
+
const events = await readEvents(ctx, sessionId)
|
|
1730
|
+
if (!events) return null
|
|
1731
|
+
const folded = foldSurface(events)
|
|
1732
|
+
const entry = editableReplies(events, folded.nodes, {
|
|
1733
|
+
patches: patchesOf(events),
|
|
1734
|
+
patch: config.patch === true,
|
|
1735
|
+
}).find((candidate) => candidate.messageId === messageId)
|
|
1736
|
+
if (entry === undefined) return null
|
|
1737
|
+
return { text: entry.text, blocks: entry.blocks, attachments: entry.attachments }
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1696
1740
|
async function stateOf(ctx, sessionId, config) {
|
|
1697
1741
|
const events = await readEvents(ctx, sessionId)
|
|
1698
1742
|
if (!events) {
|
|
@@ -1741,7 +1785,7 @@ async function stateOf(ctx, sessionId, config) {
|
|
|
1741
1785
|
// editor promising to keep blocks the host would have to drop.
|
|
1742
1786
|
const store = resolveAttachmentStore(ctx)
|
|
1743
1787
|
const turns = editableTurns(events, folded.nodes, { preview: canPreviewAttachments(store) })
|
|
1744
|
-
const replies = editableReplies(events, folded.nodes, { patches, patch: config.patch === true })
|
|
1788
|
+
const replies = editableReplies(events, folded.nodes, { patches, patch: config.patch === true }).map(lightReply)
|
|
1745
1789
|
noteRequest({
|
|
1746
1790
|
kind: 'state',
|
|
1747
1791
|
sessionId,
|
|
@@ -2594,6 +2638,33 @@ export function apply(ctx, config) {
|
|
|
2594
2638
|
}),
|
|
2595
2639
|
)
|
|
2596
2640
|
|
|
2641
|
+
// One reply's wording and blocks, on demand: `/state` publishes identities
|
|
2642
|
+
// only, because a reply can carry a whole turn's worth of text.
|
|
2643
|
+
fiber.effect(() =>
|
|
2644
|
+
webServer.register({
|
|
2645
|
+
kind: 'exact',
|
|
2646
|
+
path: `${ROUTE_PREFIX}/reply`,
|
|
2647
|
+
handler: async (req, res) => {
|
|
2648
|
+
if (!guard(req, res)) return
|
|
2649
|
+
if (req.method !== 'GET') {
|
|
2650
|
+
sendJson(res, 405, { ok: false, code: 'method', error: 'GET only' })
|
|
2651
|
+
return
|
|
2652
|
+
}
|
|
2653
|
+
try {
|
|
2654
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1')
|
|
2655
|
+
const sessionId = requireSessionId(url.searchParams.get('sessionId') ?? '')
|
|
2656
|
+
const messageId = String(url.searchParams.get('messageId') ?? '').trim()
|
|
2657
|
+
if (messageId === '') throw new HttpError(400, 'invalid', 'messageId required')
|
|
2658
|
+
const body = await replyOf(ctx, sessionId, resolved, messageId)
|
|
2659
|
+
if (body === null) throw new HttpError(404, 'reply-not-found', 'this reply is not editable')
|
|
2660
|
+
sendJson(res, 200, { ok: true, messageId, ...body })
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
failure(res, error)
|
|
2663
|
+
}
|
|
2664
|
+
},
|
|
2665
|
+
}),
|
|
2666
|
+
)
|
|
2667
|
+
|
|
2597
2668
|
// The bytes behind one block, for the editor's chips. Read-only, loopback
|
|
2598
2669
|
// and same-origin like everything else here, and answered only for a block
|
|
2599
2670
|
// the session's own log cites.
|
|
@@ -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.9, 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
|
package/src/vendor/versions.json
CHANGED