dsh-as-aistudio 0.4.2 → 0.4.4

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,22 @@
4
4
 
5
5
  ## 中文
6
6
 
7
+ **0.4.4** —— 复制失败的诊断(跟进 `dsh-edit-turn` 0.5.10)。
8
+
9
+ - **组件跟进**:`dsh-edit-turn` **0.5.10**(另三个不变:`dsh-rerun-turn` 0.2.6、`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
10
+
11
+ - 复制仍拿到改写前的原文时,浏览器**标签页标题**会显示 `[复制诊断] <原因>`,用来定位是哪一环没接管;接管成功时标题自动恢复。
12
+
13
+ **0.4.3** —— 编辑入口不再"等半天才出现"(`/state` 从 25 MB 降到几百 KB)。
14
+
15
+ - **组件跟进**:`dsh-edit-turn` **0.5.9**(另三个不变:`dsh-rerun-turn` 0.2.6、`dsh-delete-turn` 0.1.15、`dsh-markdown-bubble` 0.1.5)。
16
+
17
+ - **症状**:编辑笔要等很久才出现;而且在这之前按复制,拿到的是改写前的原文。
18
+
19
+ - **根因(两件事同一个原因)**:`dsh-edit-turn` 的 `/state` 把每条回复的**整轮**推理与正文全文都发了出去。实测一个会话 291 条回复 = **27.9 MB**,一次 `/state` 要 **13 秒**。这段时间里浏览器半部没有任何编辑条目 —— 笔不出现,复制也就不会被插件接管。
20
+
21
+ - **修法**:`/state` 的 `replies` 只发身份(`seq` / `turn` / `messageId` / `attachments` / `textLength` / `blockCount`),全文与分块走新的 `/reply` 接口,只在按复制或打开编辑器时取一次。
22
+
7
23
  **0.4.2** —— 复制拿到改写后的文本(改到平台写入器这一层)。
8
24
 
9
25
  - **组件跟进**:`dsh-edit-turn` **0.5.8**(另三个不变:`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.2",
3
+ "version": "0.4.4",
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.8'
46
+ const PLUGIN_VERSION = '0.5.10'
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
@@ -1967,6 +1998,25 @@ window.__ModuleLoader__.load({
1967
1998
  return created
1968
1999
  })()
1969
2000
  const rowCopyText = copyRegistry.text
2001
+
2002
+ // TEMP DIAG (删除时机:复制修好后). 复制按下没被接管时,把原因写进标签页标题:
2003
+ // 不污染剪贴板、不改页面内容,用户念一眼标题即可;成功接管时自动恢复原标题。
2004
+ const copyDiag = (page) => {
2005
+ try {
2006
+ if (page.__dshetTitle === undefined) page.__dshetTitle = document.title
2007
+ const base = String(page.__dshetTitle ?? '')
2008
+ const show = (reason) => {
2009
+ document.title = `[复制诊断] ${reason}${base === '' ? '' : ' · ' + base}`
2010
+ }
2011
+ const clear = () => {
2012
+ if (String(document.title).startsWith('[复制诊断]')) document.title = base
2013
+ }
2014
+ return { show, clear }
2015
+ } catch {
2016
+ return { show: () => {}, clear: () => {} }
2017
+ }
2018
+ }
2019
+ const titleDiag = typeof globalThis !== 'undefined' ? copyDiag(globalThis) : { show: () => {}, clear: () => {} }
1970
2020
  const rowHostText = copyRegistry.host
1971
2021
 
1972
2022
  /**
@@ -2098,6 +2148,11 @@ window.__ModuleLoader__.load({
2098
2148
  const native = clipboard.writeText.bind(clipboard)
2099
2149
  const wrapped = (value) => {
2100
2150
  const rewritten = rewriteCopiedText(String(value ?? ''))
2151
+ if (rewritten === null && rowCopyText.size > 0) {
2152
+ titleDiag.show(`写入没匹配上(登记 ${rowCopyText.size} 行,长度 ${String(value ?? '').length})`)
2153
+ } else if (rewritten !== null) {
2154
+ titleDiag.clear()
2155
+ }
2101
2156
  return rewritten === null ? native(value) : native(rewritten)
2102
2157
  }
2103
2158
  wrapped[CLIPBOARD_WRAPPED] = true
@@ -4825,37 +4880,55 @@ window.__ModuleLoader__.load({
4825
4880
  const open = (event) => {
4826
4881
  event.preventDefault()
4827
4882
  event.stopPropagation()
4828
- controller.open({
4829
- anchor: event.currentTarget,
4830
- seq: entry.seq,
4831
- mode: 'reply',
4832
- turn: entry.turn,
4833
- messageId: entry.messageId,
4834
- text: typeof entry.text === 'string' ? entry.text : '',
4835
- attachments: typeof entry.attachments === 'number' ? entry.attachments : 0,
4836
- ...(Array.isArray(entry.blocks) ? { blocks: entry.blocks, patch: true } : { blocks: [] }),
4837
- })
4883
+ // `currentTarget` is null once this handler returns, so it is read before
4884
+ // the await that may fetch the reply's wording.
4885
+ const anchor = event.currentTarget
4886
+ void (async () => {
4887
+ let body = null
4888
+ if (!Array.isArray(entry.blocks) && typeof entry.blockCount === 'number' && entry.blockCount > 0) {
4889
+ body = await controller.replyBody(entry.messageId)
4890
+ }
4891
+ const blocks = body !== null && Array.isArray(body.blocks)
4892
+ ? body.blocks
4893
+ : Array.isArray(entry.blocks) ? entry.blocks : []
4894
+ controller.open({
4895
+ anchor,
4896
+ seq: entry.seq,
4897
+ mode: 'reply',
4898
+ turn: entry.turn,
4899
+ messageId: entry.messageId,
4900
+ text: body !== null && typeof body.text === 'string'
4901
+ ? body.text
4902
+ : typeof entry.text === 'string' ? entry.text : '',
4903
+ attachments: typeof entry.attachments === 'number' ? entry.attachments : 0,
4904
+ ...(blocks.length > 0 ? { blocks, patch: true } : { blocks: [] }),
4905
+ })
4906
+ })()
4838
4907
  }
4839
4908
  const copy = (event) => {
4840
4909
  event.preventDefault()
4841
4910
  event.stopPropagation()
4842
- const text = replyCopyText(entry)
4843
- if (text === '') return
4844
- const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
4845
- ? primitives.writeClipboard
4846
- : typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function'
4847
- ? (value) => navigator.clipboard.writeText(value)
4848
- : null
4849
- if (write === null) return
4850
- Promise.resolve()
4851
- .then(() => write(text))
4852
- .then(
4853
- () => {
4854
- setCopied(true)
4855
- window.setTimeout(() => setCopied(false), 1200)
4856
- },
4857
- () => {},
4858
- )
4911
+ void (async () => {
4912
+ let text = replyCopyText(entry)
4913
+ if (text === '' && typeof entry.blockCount === 'number' && entry.blockCount > 0) {
4914
+ const body = await controller.replyBody(entry.messageId)
4915
+ if (body !== null) text = replyCopyText(body)
4916
+ }
4917
+ if (text === '') return
4918
+ const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
4919
+ ? primitives.writeClipboard
4920
+ : typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function'
4921
+ ? (value) => navigator.clipboard.writeText(value)
4922
+ : null
4923
+ if (write === null) return
4924
+ try {
4925
+ await write(text)
4926
+ } catch {
4927
+ return
4928
+ }
4929
+ setCopied(true)
4930
+ window.setTimeout(() => setCopied(false), 1200)
4931
+ })()
4859
4932
  }
4860
4933
  return jsxs('div', {
4861
4934
  className: 'dshet-tail',
@@ -4920,11 +4993,15 @@ window.__ModuleLoader__.load({
4920
4993
  // Our own buttons sit in the same bar: theirs is not this press.
4921
4994
  if (pluginOwned(button)) return
4922
4995
  const row = button.closest('[data-chat-flow-key]')
4923
- if (row === null) return
4996
+ if (row === null) { titleDiag.show('按钮不在行内'); return }
4924
4997
  const key = row.getAttribute('data-chat-flow-key')
4925
- if (typeof key !== 'string' || key === '') return
4998
+ if (typeof key !== 'string' || key === '') { titleDiag.show('行没有 key'); return }
4926
4999
  const text = rowCopyText.get(key)
4927
- if (typeof text !== 'string') return
5000
+ if (typeof text !== 'string') {
5001
+ titleDiag.show(`这一行没登记文本(登记 ${rowCopyText.size} 行)`)
5002
+ return
5003
+ }
5004
+ titleDiag.clear()
4928
5005
  const write = primitives !== null && primitives !== undefined && typeof primitives.writeClipboard === 'function'
4929
5006
  ? primitives.writeClipboard
4930
5007
  : null
@@ -9147,7 +9224,7 @@ window.__ModuleLoader__.load({
9147
9224
  const NS = 'dsh-as-aistudio'
9148
9225
 
9149
9226
  /** Keep in sync with package.json and src/components.js. */
9150
- const PLUGIN_VERSION = '0.4.2'
9227
+ const PLUGIN_VERSION = '0.4.4'
9151
9228
 
9152
9229
  /** Presence marker the live verifier reads. */
9153
9230
  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.2'
29
+ export const STUDIO_VERSION = '0.4.4'
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.8, sha256 5c6fa30edaffed61)
3
+ // Source: ../dsh-edit-turn/lib/index.js (v0.5.10, sha256 8bfd50c80995d96e)
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.8'
88
+ export const PLUGIN_VERSION = '0.5.10'
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.8, sha256 5e1e8cce1d168f6c)
3
+ // Source: ../dsh-edit-turn/lib/patch.js (v0.5.10, 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.8",
2
+ "dsh-edit-turn": "0.5.10",
3
3
  "dsh-rerun-turn": "0.2.6",
4
4
  "dsh-delete-turn": "0.1.15",
5
5
  "dsh-markdown-bubble": "0.1.5"