dsh-as-aistudio 0.4.1 → 0.4.2

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 CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  ## 中文
6
6
 
7
+ **0.4.2** —— 复制拿到改写后的文本(改到平台写入器这一层)。
8
+
9
+ - **组件跟进**:`dsh-edit-turn` **0.5.8**(另三个不变:`dsh-rerun-turn` 0.2.6、`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
10
+
11
+ - 0.4.1 里跟进的 0.5.7 没修好:它把"回答复制按下"从按钮节点挪到文档级捕获监听,实测仍复制出原文,说明这次按下没有经过那条路。0.5.8 换到**所有复制都要经过的唯一关口**——`navigator.clipboard.writeText`(宿主自己的 `writeClipboard` 原语内部就是调它):写入的文本如果正是某条被改写消息的原文,就换成改写后的文本,其余写入一字不动。登记表放在页级(`globalThis` 上的 Symbol),因为实时重载后旧 bundle 装的包装器仍在写入路径上。
12
+
7
13
  **0.4.1** —— 编辑过的消息,复制按钮被宿主重画之后仍然复制改写后的文本。
8
14
 
9
15
  - **组件跟进**:`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.1",
3
+ "version": "0.4.2",
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.7'
46
+ const PLUGIN_VERSION = '0.5.8'
47
47
 
48
48
  // --- copy -----------------------------------------------------------------
49
49
 
@@ -1935,6 +1935,10 @@ window.__ModuleLoader__.load({
1935
1935
  const COPY_TIMER = 'dshetCopyTimer'
1936
1936
  /** The host draws its own check for a second; this is the same second. */
1937
1937
  const COPY_FEEDBACK_MS = 1000
1938
+ /** 标记:这一页的剪贴板写入已经被本半部包过一次(重载会复用同一个 page)。 */
1939
+ const CLIPBOARD_WRAPPED = Symbol.for('dshetClipboardWrapped')
1940
+ /** 页级登记表:重载后新 bundle 与旧包装器共用同一份。 */
1941
+ const COPY_REGISTRY = Symbol.for('dshetCopyRegistry')
1938
1942
  /**
1939
1943
  * Row key -> the text that row's copy button must hand over.
1940
1944
  *
@@ -1945,7 +1949,25 @@ window.__ModuleLoader__.load({
1945
1949
  * the button it drew goes away with that node (the button it draws next is a
1946
1950
  * different one).
1947
1951
  */
1948
- const rowCopyText = new Map()
1952
+ /**
1953
+ * Row key -> the text that row's copy must hand over, and the wording the host
1954
+ * would hand over instead.
1955
+ *
1956
+ * Both live on the PAGE rather than in this module: a live reload builds a new
1957
+ * bundle with new maps, and the clipboard wrapper the previous bundle installed
1958
+ * is still the one in the write path (wrapping is idempotent, by design). Two
1959
+ * maps would leave that wrapper reading a registry nobody fills.
1960
+ */
1961
+ const copyRegistry = (() => {
1962
+ const page = typeof globalThis !== 'undefined' ? globalThis : {}
1963
+ const existing = page[COPY_REGISTRY]
1964
+ if (existing !== undefined && existing !== null && existing.text instanceof Map) return existing
1965
+ const created = { text: new Map(), host: new Map() }
1966
+ page[COPY_REGISTRY] = created
1967
+ return created
1968
+ })()
1969
+ const rowCopyText = copyRegistry.text
1970
+ const rowHostText = copyRegistry.host
1949
1971
 
1950
1972
  /**
1951
1973
  * A button another plugin put in this bar.
@@ -2009,13 +2031,93 @@ window.__ModuleLoader__.load({
2009
2031
  * host's own clipboard writer the button keeps the host's behaviour too: a
2010
2032
  * takeover that cannot copy is worse than none.
2011
2033
  */
2034
+ /**
2035
+ * The wording the host's own copy button would hand over for this row.
2036
+ *
2037
+ * It is the message the row was drawn for - the pre-edit text - and it is read
2038
+ * off the row itself, because that is where it still is: this plugin hides the
2039
+ * row's own bubble and stands its revision in front of it.
2040
+ *
2041
+ * @param {Element} row - the row.
2042
+ * @returns {string|null} the host's text, or null when it cannot be read.
2043
+ */
2044
+ function hostCopyTextOf(row) {
2045
+ try {
2046
+ const bubble = ownRevisionBubble(row)
2047
+ let text = ''
2048
+ const children = row.childNodes
2049
+ if (children === undefined || children === null) return null
2050
+ for (const child of children) {
2051
+ if (bubble !== null && child === bubble) continue
2052
+ if (typeof child.textContent === 'string') text += child.textContent
2053
+ }
2054
+ const normalized = text.replace(/\s+/g, ' ').trim()
2055
+ return normalized === '' ? null : normalized
2056
+ } catch {
2057
+ return null
2058
+ }
2059
+ }
2060
+
2061
+ /**
2062
+ * The wording that must actually land on the clipboard.
2063
+ *
2064
+ * The host's copy button copies the message its row was drawn for, so on a row
2065
+ * this plugin rewrote it hands over the pre-edit text. The press itself is
2066
+ * answered earlier when this half can find the button, but the action bar is the
2067
+ * host's own React and it may draw a button this half never marked - so the last
2068
+ * say belongs to the write itself, which no button and no handler can bypass.
2069
+ *
2070
+ * @param {string} text - what the platform is about to write.
2071
+ * @returns {string|null} the text to write instead, or null to leave it alone.
2072
+ */
2073
+ function rewriteCopiedText(text) {
2074
+ const wanted = text.replace(/\s+/g, ' ').trim()
2075
+ if (wanted === '') return null
2076
+ for (const [key, edited] of rowCopyText) {
2077
+ if (typeof edited !== 'string') continue
2078
+ if (edited.replace(/\s+/g, ' ').trim() === wanted) return null
2079
+ const hostText = rowHostText.get(key)
2080
+ if (typeof hostText === 'string' && (hostText === wanted || hostText.includes(wanted))) return edited
2081
+ }
2082
+ return null
2083
+ }
2084
+
2085
+ /**
2086
+ * Wrap the platform's clipboard writer, once per page.
2087
+ *
2088
+ * `navigator.clipboard.writeText` is the single choke point every copy goes
2089
+ * through - the host's own `writeClipboard` primitive calls it, and so does a
2090
+ * browser's native copy of a selection.
2091
+ *
2092
+ * @returns {() => void} undo.
2093
+ */
2094
+ function installClipboardRewrite() {
2095
+ const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : undefined
2096
+ if (clipboard === undefined || clipboard === null || typeof clipboard.writeText !== 'function') return () => {}
2097
+ if (clipboard.writeText[CLIPBOARD_WRAPPED] === true) return () => {}
2098
+ const native = clipboard.writeText.bind(clipboard)
2099
+ const wrapped = (value) => {
2100
+ const rewritten = rewriteCopiedText(String(value ?? ''))
2101
+ return rewritten === null ? native(value) : native(rewritten)
2102
+ }
2103
+ wrapped[CLIPBOARD_WRAPPED] = true
2104
+ clipboard.writeText = wrapped
2105
+ return () => {
2106
+ if (clipboard.writeText === wrapped) clipboard.writeText = native
2107
+ }
2108
+ }
2109
+
2012
2110
  function takeOverRowCopy(row, text, t) {
2013
2111
  // Registered before the button lookup on purpose: the handover has to work
2014
2112
  // even when this pass cannot find the button to mark (the next one may, and
2015
2113
  // the document-level press answers from this table either way).
2016
2114
  {
2017
2115
  const key = row.getAttribute('data-chat-flow-key')
2018
- if (typeof key === 'string' && key !== '') rowCopyText.set(key, text)
2116
+ if (typeof key === 'string' && key !== '') {
2117
+ rowCopyText.set(key, text)
2118
+ const hostText = hostCopyTextOf(row)
2119
+ if (hostText !== null) rowHostText.set(key, hostText)
2120
+ }
2019
2121
  }
2020
2122
  const button = nativeCopyButton(row, t)
2021
2123
  if (button === null) return
@@ -2146,7 +2248,10 @@ window.__ModuleLoader__.load({
2146
2248
  /** The copy handover of a row, wherever the last pass left it. */
2147
2249
  function releaseRowCopy(row) {
2148
2250
  const key = row.getAttribute('data-chat-flow-key')
2149
- if (typeof key === 'string' && key !== '') rowCopyText.delete(key)
2251
+ if (typeof key === 'string' && key !== '') {
2252
+ rowCopyText.delete(key)
2253
+ rowHostText.delete(key)
2254
+ }
2150
2255
  const button = nativeCopyButton(row)
2151
2256
  if (button !== null) releaseCopyButton(button)
2152
2257
  }
@@ -4791,6 +4896,10 @@ window.__ModuleLoader__.load({
4791
4896
  controller.load()
4792
4897
  }, [controller])
4793
4898
 
4899
+ // The last say on what lands: whatever route the press takes, the write
4900
+ // itself goes through here.
4901
+ react.useEffect(() => installClipboardRewrite(), [])
4902
+
4794
4903
  // Pressing outside the editor is the one signal that means "the user
4795
4904
  // left". Blur cannot be that signal: a host re-render replaces the node
4796
4905
  // (blur without a related target) and a save temporarily disables it -
@@ -9038,7 +9147,7 @@ window.__ModuleLoader__.load({
9038
9147
  const NS = 'dsh-as-aistudio'
9039
9148
 
9040
9149
  /** Keep in sync with package.json and src/components.js. */
9041
- const PLUGIN_VERSION = '0.4.1'
9150
+ const PLUGIN_VERSION = '0.4.2'
9042
9151
 
9043
9152
  /** Presence marker the live verifier reads. */
9044
9153
  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.1'
29
+ export const STUDIO_VERSION = '0.4.2'
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.7, sha256 32dc3ed4d66bb063)
3
+ // Source: ../dsh-edit-turn/lib/index.js (v0.5.8, sha256 5c6fa30edaffed61)
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.7'
88
+ export const PLUGIN_VERSION = '0.5.8'
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
@@ -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.7, sha256 5e1e8cce1d168f6c)
3
+ // Source: ../dsh-edit-turn/lib/patch.js (v0.5.8, 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,5 +1,5 @@
1
1
  {
2
- "dsh-edit-turn": "0.5.7",
2
+ "dsh-edit-turn": "0.5.8",
3
3
  "dsh-rerun-turn": "0.2.6",
4
4
  "dsh-delete-turn": "0.1.15",
5
5
  "dsh-markdown-bubble": "0.1.5"