dsh-reference-anything 0.2.1 → 0.2.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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  One `@` to reference them all.
10
10
 
11
- **English** · [简体中文](./README_zh-CN.md) · [News](#news) · [Roadmap](#future-roadmap) · [Installation](#installation) · [Usage](#usage) · [Report Bug][github-issues-link]
11
+ **English** · [简体中文](./README_zh-CN.md) · [News](#news) · [Roadmap](#roadmap) · [Installation](#installation) · [Usage](#usage) · [Report Bug][github-issues-link]
12
12
 
13
13
  <!-- SHIELD GROUP -->
14
14
 
@@ -24,11 +24,15 @@ One `@` to reference them all.
24
24
 
25
25
  Within the unified `@` menu of DeepSeek Harness (DSH), reference commands, Skills, workspace files/folders, DSH sessions, and historical conversations from ChatGPT, Claude, Gemini, DeepSeek, Grok, and Kimi.
26
26
 
27
- This plugin uses OpenCLI to access historical conversations through AI chat sessions that are already logged in. Only conversation titles are stored locally; when the full content is needed, the agent fetches it from the remote platform on demand.
27
+ This plugin uses OpenCLI to access historical conversations through AI chat sessions that are already logged in. By default, only conversation titles are stored locally, and the agent fetches remote content on demand. An optional offline-mirror mode stores the latest complete conversation bodies locally.
28
+
29
+ > [!NOTE]
30
+ > DSH is currently in Beta, so its underlying capabilities and interfaces may change as it evolves. This plugin will adapt alongside those changes. Because of some current DSH limitations, parts of the implementation may not yet be ideal; we will continue to follow DSH updates and improve the plugin over time. See the relevant sections below for specific limitations and usage notes.
28
31
 
29
32
  ## News
30
33
 
31
- - **2026-08-18 · v0.2.0** — A redesigned Conversations settings page with local session statistics, paginated management, Provider/Profile selection, and sync status checks.
34
+ - **2026-08-19 · v0.2.3** — Added automatic version checks and in-settings updates, Pill/Raw text input rendering modes, and reusable background browser sessions for more reliable OpenCLI synchronization and input interactions.
35
+ - **2026-08-18 · v0.2.0** — A redesigned Reference Anything settings page with local session statistics, paginated management, Provider/Profile selection, and sync status checks.
32
36
  - **2026-08-18** — Introduced on-demand read protocol: references default to safe pointers, and the agent reads the body and attachments only after authorization.
33
37
  - **2026-08-17** — Unified ChatGPT, Claude, Gemini, DeepSeek, Grok, and Kimi under the DSH `@` menu.
34
38
 
@@ -57,33 +61,38 @@ dsh plugin --profile web add dsh-reference-anything
57
61
  For development, the repository can still be installed from a local path:
58
62
 
59
63
  ```powershell
60
- dsh plugin --profile web add D:\dsh-reference-anything
64
+ # Run from the repository root
65
+ dsh plugin --profile web add .
61
66
  ```
62
67
 
63
- After installing the DSH plugin, open `Settings → Conversations → Availability check` in DSH Web and click **Install all**. It discovers OpenCLI, installs or upgrades it through npm when missing or outdated, installs the OpenCLI adapters bundled in the npm package, starts or refreshes Browser Bridge, and immediately opens the OpenCLI Browser Bridge page in the [Chrome Web Store](https://chromewebstore.google.com/detail/opencli/ildkmabpimmkaediidaifkhjpohdnifk). Confirm the extension installation, then return and click **Check again**. Any remaining failed check displays its own recovery action.
68
+ After installing the DSH plugin, open `Settings → Reference Anything → Availability check` in DSH Web (restart DSH first if this settings entry is not yet visible), then click **One-click setup**. It discovers OpenCLI; installs or upgrades it globally through npm when it is missing or older than `1.8.6`; installs the bundled adapters for all six platforms; starts or refreshes Browser Bridge; and opens the OpenCLI Browser Bridge page in the [Chrome Web Store](https://chromewebstore.google.com/detail/opencli/ildkmabpimmkaediidaifkhjpohdnifk). Confirm the extension installation, then return and click **Recheck setup**. Any remaining failed check displays its own recovery action.
64
69
 
65
- You can also complete the steps manually:
70
+ You can also install OpenCLI and the conversation adapter manually, then start Browser Bridge:
66
71
 
67
72
  ```powershell
68
- opencli plugin install file:///D:/dsh-reference-anything/opencli-plugin
73
+ npm install --global "@jackwener/opencli@>=1.8.6"
74
+ opencli plugin install file:///C:/path/to/dsh-reference-anything/opencli-plugin
75
+ opencli daemon restart
69
76
  ```
70
77
 
71
- Browser extensions cannot be silently installed from a webpage; confirm the installation in the Chrome Web Store or use “Load unpacked” from [OpenCLI Releases](https://github.com/jackwener/opencli/releases). If the browser blocks the store popup, the settings page keeps a normal fallback link. When multiple browser profiles are connected, select and apply one directly in the failed check. Global npm installation remains subject to OS permissions; failures retain their original diagnostic in the settings page.
78
+ Replace `C:/path/to/dsh-reference-anything` with the repository location. Browser extensions cannot be silently installed from a webpage; confirm the installation in the Chrome Web Store, or download an extension package from [OpenCLI Releases](https://github.com/jackwener/opencli/releases) and use “Load unpacked.” If the browser blocks the store popup, the settings page keeps a normal fallback link. When multiple browser profiles are connected, select and apply one directly in the failed check. Global npm installation remains subject to OS permissions; failures retain their original diagnostic in the settings page.
72
79
 
73
80
  ## Usage
74
81
 
75
- 1. Open `Settings → Conversations` in DSH Web.
76
- 2. Check the status of the OpenCLI CLI, daemon, Browser Bridge, and adapters.
77
- 3. Select a Chrome Profile and click `Sync all`, or sync a single Provider.
82
+ 1. Open `Settings → Reference Anything` in DSH Web.
83
+ 2. Under **Availability check**, confirm that OpenCLI, Browser Bridge, the browser extension, and the conversation adapter are ready.
84
+ 3. Under **External conversation sync settings**, choose a connected browser Profile, history storage mode, and sync mode. Then click **Sync enabled sources now**, or sync an individual Provider from its card.
78
85
  4. Type `@` in the input box and choose from the `Files and folders`, `DSH sessions`, or `External conversations` groups.
79
86
  5. Type a keyword to filter candidates, for example `@cache-design`.
80
87
 
88
+ The default **Read bodies on demand** mode stores only the title index locally and uses the browser when an agent reads a reference. Choose **Store full bodies locally** for offline reading and full-text search; this mode keeps only the latest version of each conversation. The settings page also lets you enable or disable each `@` group, reorder groups, set their maximum result counts (six per group by default), and switch between Pill and Raw text input rendering. The plugin checks npm for updates when it loads; restart DSH after installing an update from the settings page.
89
+
81
90
  > [!WARNING]
82
91
  > To protect your account and conversation data, external conversations are imported and synchronized through OpenCLI using your existing logged-in browser session. A browser window may temporarily open during use or synchronization, and it may display OpenCLI debugging information. This is expected—please do not be alarmed or close the window manually; wait for the operation to finish.
83
92
 
84
93
  ### Search
85
94
 
86
- The `@` menu contains five groups: `Commands`, `Skills`, `Files and folders`, `DSH sessions`, and `External conversations`. The first two appear only when `@` is at the beginning of the draft. Each group shows up to five results when the query is empty, and up to eight after a query is entered.
95
+ The `@` menu contains five groups: `Commands`, `Skills`, `Files and folders`, `DSH sessions`, and `External conversations`. The first two appear only when `@` is at the beginning of the draft. Each group shows up to six results by default. Under `Settings → Reference Anything → General`, you can enable or disable groups, reorder them, and set each limit from 1 to 50.
87
96
 
88
97
  #### @Commands — DSH native commands
89
98
 
@@ -124,7 +133,7 @@ Supports historical conversations from ChatGPT, Claude, Gemini, DeepSeek, Grok,
124
133
 
125
134
  **Search capabilities:**
126
135
  - **Title match:** fuzzy search on conversation titles
127
- - **Content search:** if title matches are insufficient, the conversation body is searched and matching excerpts are shown in the candidate list
136
+ - **Content search:** available only in **Store full bodies locally** mode; if title matches are insufficient, synchronized bodies are searched and matching excerpts are shown in the candidate list
128
137
  - **Provider and account isolation:** history is maintained separately by Provider and account scope
129
138
  - `@` search uses the account scope cached by the latest sync and never probes the browser; after a sync observes an account switch, it exposes only that account while older rows remain available in conversation management for cleanup
130
139
 
@@ -135,6 +144,9 @@ Supports historical conversations from ChatGPT, Claude, Gemini, DeepSeek, Grok,
135
144
 
136
145
  Opening the source URL happens only in the UI; the URL is never injected into model context. The initial reference contains only a safe pointer; if the model needs the body, it calls `reference_read` on demand.
137
146
 
147
+ > [!NOTE]
148
+ > Due to DSH's current underlying length limit for References, external-conversation references temporarily use a compatibility layer implemented by this plugin instead of relying entirely on DSH's native reference presentation. As a result, some interactions may behave differently or encounter issues in certain scenarios. We have raised this limitation in DSH Discussions; when a future DSH release provides the necessary support, we will update the plugin promptly and migrate to a more native implementation.
149
+
138
150
  ---
139
151
 
140
152
  **General notes:**
package/lib/client.js CHANGED
@@ -17074,6 +17074,7 @@ function adoptAdaptiveComposerHeight() {
17074
17074
  document.querySelectorAll("[data-dsh-ref-height-ruler]").forEach((node) => node.remove());
17075
17075
  };
17076
17076
  }
17077
+ var caretVisualAffinity = /* @__PURE__ */ new WeakMap();
17077
17078
  function logicalOffsetFromPoint(input, backdrop, clientX, clientY) {
17078
17079
  const previousVisibility = input.style.getPropertyValue("visibility");
17079
17080
  const previousVisibilityPriority = input.style.getPropertyPriority("visibility");
@@ -17100,24 +17101,85 @@ function logicalOffsetFromPoint(input, backdrop, clientX, clientY) {
17100
17101
  const lineHeight = Number.parseFloat(style.lineHeight);
17101
17102
  const fontSize = Number.parseFloat(style.fontSize);
17102
17103
  const resolvedLineHeight = lineHeight > 0 ? lineHeight : fontSize > 0 ? fontSize * 1.2 : 20;
17103
- if (clamped !== void 0 && preciseNode) return clamped;
17104
- return nearestVisualLogicalOffset(backdrop, input.value.length, clientX, clientY, resolvedLineHeight) ?? clamped;
17104
+ if (clamped !== void 0 && preciseNode) {
17105
+ rememberCaretAffinityAtPoint(input, backdrop, clamped, clientX, clientY);
17106
+ return clamped;
17107
+ }
17108
+ if (clamped !== void 0 && node === backdrop && logicalEndpointMatchesPoint(backdrop, clamped, input.value.length, clientX, clientY, resolvedLineHeight)) {
17109
+ rememberCaretAffinityAtPoint(input, backdrop, clamped, clientX, clientY);
17110
+ return clamped;
17111
+ }
17112
+ const fallback2 = nearestVisualLogicalOffset(backdrop, input.value.length, clientX, clientY, resolvedLineHeight) ?? clamped;
17113
+ if (fallback2 !== void 0) rememberCaretAffinityAtPoint(input, backdrop, fallback2, clientX, clientY);
17114
+ return fallback2;
17105
17115
  }
17106
17116
  function restoreInlineProperty(style, name, value, priority) {
17107
17117
  if (value === "") style.removeProperty(name);
17108
17118
  else style.setProperty(name, value, priority);
17109
17119
  }
17110
17120
  function nearestVisualLogicalOffset(root, maxLogical, clientX, clientY, lineHeight) {
17111
- const boundaries = visualLogicalBoundaries(root, maxLogical, lineHeight);
17112
- if (boundaries.length === 0) return void 0;
17113
17121
  const style = getComputedStyle(root);
17114
17122
  const paddingTop = Number.parseFloat(style.paddingTop) || 0;
17115
17123
  const contentTop = root.getBoundingClientRect().top + paddingTop;
17116
17124
  const targetLine = Math.floor((clientY - contentTop) / lineHeight);
17117
17125
  const lineOf = (boundary) => Math.round((boundary.top - contentTop) / lineHeight);
17118
- let nearestLineDistance = Number.POSITIVE_INFINITY;
17119
- for (const boundary of boundaries) nearestLineDistance = Math.min(nearestLineDistance, Math.abs(lineOf(boundary) - targetLine));
17120
- const onLine = boundaries.filter((boundary) => Math.abs(lineOf(boundary) - targetLine) === nearestLineDistance);
17126
+ const cache = /* @__PURE__ */ new Map();
17127
+ const boundaryAt = (offset, affinity = "forward") => {
17128
+ const at = Math.min(Math.max(offset, 0), maxLogical);
17129
+ const key = `${at}:${affinity}`;
17130
+ if (cache.has(key)) return cache.get(key);
17131
+ const rect = visualCaretRectAtLogicalOffset(root, at, affinity);
17132
+ const boundary = rect === void 0 ? void 0 : { offset: at, left: rect.left, top: rect.top, height: rect.height ?? lineHeight };
17133
+ cache.set(key, boundary);
17134
+ return boundary;
17135
+ };
17136
+ const first = boundaryAt(0);
17137
+ const last = boundaryAt(maxLogical);
17138
+ if (first === void 0 || last === void 0) return void 0;
17139
+ const firstLine = lineOf(first);
17140
+ const lastLine = lineOf(last);
17141
+ const wantedLine = Math.min(Math.max(targetLine, Math.min(firstLine, lastLine)), Math.max(firstLine, lastLine));
17142
+ const firstOffsetOnOrAfterLine = (line) => {
17143
+ let low = 0;
17144
+ let high = maxLogical;
17145
+ while (low < high) {
17146
+ const middle = Math.floor((low + high) / 2);
17147
+ const boundary = boundaryAt(middle);
17148
+ if (boundary === void 0 || lineOf(boundary) >= line) high = middle;
17149
+ else low = middle + 1;
17150
+ }
17151
+ return low;
17152
+ };
17153
+ const start = firstOffsetOnOrAfterLine(wantedLine);
17154
+ const after = firstOffsetOnOrAfterLine(wantedLine + 1);
17155
+ const end = after === start && lineOf(last) === wantedLine ? maxLogical : Math.max(start, after - 1);
17156
+ const candidates = /* @__PURE__ */ new Set([start, end]);
17157
+ const span = end - start;
17158
+ if (span <= 512) {
17159
+ for (let offset = start; offset <= end; offset++) candidates.add(offset);
17160
+ } else {
17161
+ const rtl = style.direction === "rtl";
17162
+ let low = start;
17163
+ let high = end;
17164
+ while (low <= high) {
17165
+ const middle = Math.floor((low + high) / 2);
17166
+ const boundary = boundaryAt(middle);
17167
+ if (boundary === void 0) {
17168
+ high = middle - 1;
17169
+ continue;
17170
+ }
17171
+ candidates.add(middle);
17172
+ const beforePoint = rtl ? boundary.left > clientX : boundary.left < clientX;
17173
+ if (beforePoint) low = middle + 1;
17174
+ else high = middle - 1;
17175
+ }
17176
+ for (const offset of [low - 2, low - 1, low, low + 1, low + 2]) {
17177
+ if (offset >= start && offset <= end) candidates.add(offset);
17178
+ }
17179
+ }
17180
+ candidates.add(after);
17181
+ const onLine = Array.from(candidates).flatMap((offset) => [boundaryAt(offset, "backward"), boundaryAt(offset, "forward")]).filter((boundary) => boundary !== void 0 && lineOf(boundary) === wantedLine);
17182
+ if (onLine.length === 0) return Math.abs(targetLine - firstLine) <= Math.abs(targetLine - lastLine) ? first.offset : last.offset;
17121
17183
  return onLine.reduce((best, boundary) => {
17122
17184
  const distance = Math.abs(boundary.left - clientX);
17123
17185
  const bestDistance = Math.abs(best.left - clientX);
@@ -17125,51 +17187,27 @@ function nearestVisualLogicalOffset(root, maxLogical, clientX, clientY, lineHeig
17125
17187
  return boundary.offset > best.offset ? boundary : best;
17126
17188
  }).offset;
17127
17189
  }
17128
- function visualLogicalBoundaries(root, maxLogical, lineHeight) {
17129
- const boundaries = [];
17130
- const direction = getComputedStyle(root).direction;
17131
- let logical = 0;
17132
- const add = (offset, left, top, height) => {
17133
- if (offset < 0 || offset > maxLogical || !Number.isFinite(left) || !Number.isFinite(top)) return;
17134
- boundaries.push({ offset, left, top, height: height > 0 ? height : lineHeight });
17135
- };
17136
- const visit = (parent) => {
17137
- for (const child of Array.from(parent.childNodes)) {
17138
- if (logical > maxLogical) return;
17139
- if (child instanceof Element && child.matches('[data-decoration="chip"]')) {
17140
- const rect = child.getBoundingClientRect();
17141
- const start = direction === "rtl" ? rect.right : rect.left;
17142
- const end = direction === "rtl" ? rect.left : rect.right;
17143
- add(logical, start, rect.top, rect.height);
17144
- logical += 1;
17145
- add(logical, end, rect.top, rect.height);
17146
- continue;
17147
- }
17148
- if (child instanceof Text) {
17149
- const text = child.data;
17150
- let local = 0;
17151
- for (const symbol2 of text) {
17152
- const next = local + symbol2.length;
17153
- const range = document.createRange();
17154
- range.setStart(child, local);
17155
- range.setEnd(child, next);
17156
- const rect = range.getBoundingClientRect();
17157
- if (usableCaretRect(rect)) {
17158
- const start = direction === "rtl" ? rect.right : rect.left;
17159
- const end = direction === "rtl" ? rect.left : rect.right;
17160
- add(logical + local, start, rect.top, rect.height);
17161
- add(logical + next, end, rect.top, rect.height);
17162
- }
17163
- local = next;
17164
- }
17165
- logical += text.length;
17166
- continue;
17167
- }
17168
- visit(child);
17169
- }
17170
- };
17171
- visit(root);
17172
- return boundaries;
17190
+ function logicalEndpointMatchesPoint(root, offset, maxLogical, clientX, clientY, lineHeight) {
17191
+ if (offset !== 0 && offset !== maxLogical) return false;
17192
+ const rtl = getComputedStyle(root).direction === "rtl";
17193
+ return ["backward", "forward"].some((affinity) => {
17194
+ const boundary = visualCaretRectAtLogicalOffset(root, offset, affinity);
17195
+ if (boundary === void 0) return false;
17196
+ const sameLine = clientY >= boundary.top - lineHeight / 2 && clientY <= boundary.top + lineHeight * 1.5;
17197
+ if (!sameLine) return false;
17198
+ if (offset === 0) return rtl ? clientX >= boundary.left - 1 : clientX <= boundary.left + 1;
17199
+ return rtl ? clientX <= boundary.left + 1 : clientX >= boundary.left - 1;
17200
+ });
17201
+ }
17202
+ function rememberCaretAffinityAtPoint(input, root, offset, clientX, clientY) {
17203
+ const choices = ["backward", "forward"].map((affinity) => ({ affinity, rect: visualCaretRectAtLogicalOffset(root, offset, affinity) })).filter((choice) => choice.rect !== void 0);
17204
+ if (choices.length === 0) return;
17205
+ const best = choices.reduce((nearest, choice) => {
17206
+ const distance = Math.abs(choice.rect.top + (choice.rect.height ?? 0) / 2 - clientY) * 1e3 + Math.abs(choice.rect.left - clientX);
17207
+ const nearestDistance = Math.abs(nearest.rect.top + (nearest.rect.height ?? 0) / 2 - clientY) * 1e3 + Math.abs(nearest.rect.left - clientX);
17208
+ return distance < nearestDistance ? choice : nearest;
17209
+ });
17210
+ caretVisualAffinity.set(input, best.affinity);
17173
17211
  }
17174
17212
  function logicalOffsetAtDomPoint(root, node, nodeOffset, clientX) {
17175
17213
  if (!root.contains(node) && node !== root) return void 0;
@@ -17194,56 +17232,120 @@ function logicalOffsetAtDomPoint(root, node, nodeOffset, clientX) {
17194
17232
  }
17195
17233
  function adoptAdaptiveChipHitTesting() {
17196
17234
  let press;
17197
- const apply2 = (input, anchor, target) => {
17198
- input.focus({ preventScroll: true });
17199
- input.setSelectionRange(Math.min(anchor, target), Math.max(anchor, target), target < anchor ? "backward" : "forward");
17235
+ let clickSequence;
17236
+ const applyRange = (input, start, end, direction) => {
17237
+ input.setSelectionRange(start, end, direction);
17200
17238
  input.dispatchEvent(new Event("select", { bubbles: true }));
17201
17239
  };
17240
+ const applyTarget = (current, target, clientY) => {
17241
+ if (current.unit === "character") {
17242
+ applyRange(current.input, Math.min(current.anchor, target), Math.max(current.anchor, target), target < current.anchor ? "backward" : "forward");
17243
+ return;
17244
+ }
17245
+ const targetRange = current.unit === "word" ? wordRangeAtLogicalOffset(current.input.value, target) : visualLineRangeAtPoint(current.input, current.backdrop, clientY);
17246
+ if (targetRange === void 0) return;
17247
+ if (targetRange.end <= current.anchorStart) applyRange(current.input, targetRange.start, current.anchorEnd, "backward");
17248
+ else applyRange(current.input, current.anchorStart, targetRange.end, "forward");
17249
+ };
17250
+ const scrollDelta = (current) => {
17251
+ const scrollport = current.input.closest("[data-input-scroll]");
17252
+ if (!(scrollport instanceof HTMLElement) || scrollport.scrollHeight <= scrollport.clientHeight) return 0;
17253
+ const viewport = scrollport.getBoundingClientRect();
17254
+ const edge = Math.min(32, viewport.height / 4);
17255
+ if (!(edge > 0)) return 0;
17256
+ if (current.clientY < viewport.top + edge) return -Math.max(2, Math.min(24, (viewport.top + edge - current.clientY) / 2));
17257
+ if (current.clientY > viewport.bottom - edge) return Math.max(2, Math.min(24, (current.clientY - (viewport.bottom - edge)) / 2));
17258
+ return 0;
17259
+ };
17260
+ const scheduleDragScroll = (current) => {
17261
+ if (current.scrollFrame !== 0 || scrollDelta(current) === 0) return;
17262
+ current.scrollFrame = requestAnimationFrame(() => {
17263
+ current.scrollFrame = 0;
17264
+ if (press !== current) return;
17265
+ const scrollport = current.input.closest("[data-input-scroll]");
17266
+ if (!(scrollport instanceof HTMLElement)) return;
17267
+ const before = scrollport.scrollTop;
17268
+ scrollport.scrollTop += scrollDelta(current);
17269
+ if (scrollport.scrollTop !== before) {
17270
+ const target = logicalOffsetFromPoint(current.input, current.backdrop, current.clientX, current.clientY);
17271
+ if (target !== void 0) applyTarget(current, target, current.clientY);
17272
+ scheduleDragScroll(current);
17273
+ }
17274
+ });
17275
+ };
17276
+ const finishPress = () => {
17277
+ const current = press;
17278
+ press = void 0;
17279
+ if (current === void 0) return void 0;
17280
+ cancelAnimationFrame(current.scrollFrame);
17281
+ if (Math.hypot(current.clientX - current.startX, current.clientY - current.startY) > 5) clickSequence = void 0;
17282
+ try {
17283
+ if (current.input.hasPointerCapture?.(current.pointerId)) current.input.releasePointerCapture(current.pointerId);
17284
+ } catch {
17285
+ }
17286
+ return current;
17287
+ };
17202
17288
  const onPointerDown = (event) => {
17203
17289
  const input = event.target;
17204
- if (event.button !== 0 || !(input instanceof HTMLTextAreaElement)) return;
17290
+ if (event.button !== 0 || event.pointerType === "touch" || !(input instanceof HTMLTextAreaElement)) return;
17205
17291
  const backdrop = input.closest("[data-composer-card]")?.querySelector("[data-input-backdrop]:not([data-dsh-ref-height-ruler])");
17206
17292
  if (!(backdrop instanceof HTMLElement) || backdrop.querySelector('[data-decoration="chip"]') === null) return;
17207
17293
  const target = logicalOffsetFromPoint(input, backdrop, event.clientX, event.clientY);
17208
17294
  if (target === void 0) return;
17209
17295
  const anchor = event.shiftKey ? input.selectionDirection === "backward" ? input.selectionEnd : input.selectionStart : target;
17210
- press = { input, backdrop, anchor };
17211
- apply2(input, anchor, target);
17296
+ const previousClick = clickSequence;
17297
+ const continuedClick = previousClick !== void 0 && previousClick.input === input && event.timeStamp - previousClick.time <= 500 && Math.hypot(event.clientX - previousClick.x, event.clientY - previousClick.y) <= 5;
17298
+ const sequenceCount = continuedClick ? previousClick.count + 1 : 1;
17299
+ clickSequence = { input, x: event.clientX, y: event.clientY, time: event.timeStamp, count: sequenceCount };
17300
+ const clickCount = Math.max(sequenceCount, event.detail);
17301
+ const unit = clickCount >= 3 ? "line" : clickCount === 2 ? "word" : "character";
17302
+ const unitRange = unit === "word" ? wordRangeAtLogicalOffset(input.value, target) : unit === "line" ? visualLineRangeAtPoint(input, backdrop, event.clientY) : void 0;
17303
+ const anchorStart = unitRange?.start ?? anchor;
17304
+ const anchorEnd = unitRange?.end ?? anchor;
17305
+ finishPress();
17306
+ input.focus({ preventScroll: true });
17307
+ press = { input, backdrop, anchor, anchorStart, anchorEnd, unit, clientX: event.clientX, clientY: event.clientY, startX: event.clientX, startY: event.clientY, pointerId: event.pointerId, scrollFrame: 0 };
17308
+ try {
17309
+ input.setPointerCapture?.(event.pointerId);
17310
+ } catch {
17311
+ }
17312
+ if (press !== void 0) applyTarget(press, target, event.clientY);
17212
17313
  event.preventDefault();
17213
17314
  };
17214
17315
  const onPointerMove = (event) => {
17215
17316
  const current = press;
17216
17317
  if (current === void 0) return;
17318
+ current.clientX = event.clientX;
17319
+ current.clientY = event.clientY;
17320
+ if (Math.hypot(current.clientX - current.startX, current.clientY - current.startY) > 5) clickSequence = void 0;
17217
17321
  const target = logicalOffsetFromPoint(current.input, current.backdrop, event.clientX, event.clientY);
17218
17322
  if (target === void 0) return;
17219
- apply2(current.input, current.anchor, target);
17323
+ applyTarget(current, target, event.clientY);
17324
+ scheduleDragScroll(current);
17220
17325
  event.preventDefault();
17221
17326
  };
17222
17327
  const onPointerUp = (event) => {
17223
- const current = press;
17224
- press = void 0;
17328
+ const current = finishPress();
17225
17329
  if (current === void 0) return;
17226
17330
  const target = logicalOffsetFromPoint(current.input, current.backdrop, event.clientX, event.clientY);
17227
- if (target !== void 0) apply2(current.input, current.anchor, target);
17331
+ if (target !== void 0) applyTarget(current, target, event.clientY);
17228
17332
  event.preventDefault();
17229
17333
  };
17230
17334
  const onPointerCancel = () => {
17231
- press = void 0;
17335
+ finishPress();
17232
17336
  };
17233
17337
  const onDoubleClick = (event) => {
17234
17338
  const input = event.target;
17235
17339
  if (!(input instanceof HTMLTextAreaElement)) return;
17236
17340
  const backdrop = input.closest("[data-composer-card]")?.querySelector("[data-input-backdrop]:not([data-dsh-ref-height-ruler])");
17237
17341
  if (!(backdrop instanceof HTMLElement) || backdrop.querySelector('[data-decoration="chip"]') === null) return;
17238
- const target = logicalOffsetFromPoint(input, backdrop, event.clientX, event.clientY);
17239
- if (target === void 0) return;
17240
- const selected = wordRangeAtLogicalOffset(input.value, target);
17241
- const applyWord = () => {
17242
- input.setSelectionRange(selected.start, selected.end);
17342
+ const selected = { start: input.selectionStart, end: input.selectionEnd, direction: input.selectionDirection };
17343
+ const restoreWordSelection = () => {
17344
+ input.setSelectionRange(selected.start, selected.end, selected.direction);
17243
17345
  input.dispatchEvent(new Event("select", { bubbles: true }));
17244
17346
  };
17245
- applyWord();
17246
- queueMicrotask(applyWord);
17347
+ restoreWordSelection();
17348
+ queueMicrotask(restoreWordSelection);
17247
17349
  event.preventDefault();
17248
17350
  event.stopImmediatePropagation();
17249
17351
  };
@@ -17252,18 +17354,13 @@ function adoptAdaptiveChipHitTesting() {
17252
17354
  const input = event.target;
17253
17355
  const backdrop = input.closest("[data-composer-card]")?.querySelector("[data-input-backdrop]:not([data-dsh-ref-height-ruler])");
17254
17356
  if (!(backdrop instanceof HTMLElement) || backdrop.querySelector('[data-decoration="chip"]') === null) return;
17255
- const style = getComputedStyle(input);
17256
- const left = backdrop.getBoundingClientRect().left + (Number.parseFloat(style.paddingLeft) || 0) + 1;
17257
- const right = backdrop.getBoundingClientRect().right - (Number.parseFloat(style.paddingRight) || 0) - 1;
17258
- const start = logicalOffsetFromPoint(input, backdrop, left, event.clientY);
17259
- const end = logicalOffsetFromPoint(input, backdrop, right, event.clientY);
17260
- if (start === void 0 || end === void 0) return;
17261
- const applyLine = () => {
17262
- input.setSelectionRange(Math.min(start, end), Math.max(start, end));
17357
+ const selected = { start: input.selectionStart, end: input.selectionEnd, direction: input.selectionDirection };
17358
+ const restoreLineSelection = () => {
17359
+ input.setSelectionRange(selected.start, selected.end, selected.direction);
17263
17360
  input.dispatchEvent(new Event("select", { bubbles: true }));
17264
17361
  };
17265
- applyLine();
17266
- queueMicrotask(applyLine);
17362
+ restoreLineSelection();
17363
+ queueMicrotask(restoreLineSelection);
17267
17364
  event.preventDefault();
17268
17365
  event.stopImmediatePropagation();
17269
17366
  };
@@ -17271,16 +17368,21 @@ function adoptAdaptiveChipHitTesting() {
17271
17368
  document.addEventListener("pointermove", onPointerMove, true);
17272
17369
  document.addEventListener("pointerup", onPointerUp, true);
17273
17370
  document.addEventListener("pointercancel", onPointerCancel, true);
17371
+ document.addEventListener("lostpointercapture", onPointerCancel, true);
17274
17372
  document.addEventListener("dblclick", onDoubleClick, true);
17275
17373
  document.addEventListener("click", onClick, true);
17374
+ window.addEventListener("blur", onPointerCancel);
17276
17375
  return () => {
17277
- press = void 0;
17376
+ finishPress();
17377
+ clickSequence = void 0;
17278
17378
  document.removeEventListener("pointerdown", onPointerDown, true);
17279
17379
  document.removeEventListener("pointermove", onPointerMove, true);
17280
17380
  document.removeEventListener("pointerup", onPointerUp, true);
17281
17381
  document.removeEventListener("pointercancel", onPointerCancel, true);
17382
+ document.removeEventListener("lostpointercapture", onPointerCancel, true);
17282
17383
  document.removeEventListener("dblclick", onDoubleClick, true);
17283
17384
  document.removeEventListener("click", onClick, true);
17385
+ window.removeEventListener("blur", onPointerCancel);
17284
17386
  };
17285
17387
  }
17286
17388
  function wordRangeAtLogicalOffset(value, offset) {
@@ -17289,6 +17391,13 @@ function wordRangeAtLogicalOffset(value, offset) {
17289
17391
  if (chip >= 0) return { start: chip, end: chip + 1 };
17290
17392
  const pivot = at === value.length && at > 0 ? at - 1 : at;
17291
17393
  if (pivot < 0 || pivot >= value.length) return { start: at, end: at };
17394
+ if (typeof Intl.Segmenter === "function") {
17395
+ for (const part of new Intl.Segmenter(void 0, { granularity: "word" }).segment(value)) {
17396
+ const start2 = part.index;
17397
+ const end2 = start2 + part.segment.length;
17398
+ if (pivot >= start2 && pivot < end2) return { start: start2, end: end2 };
17399
+ }
17400
+ }
17292
17401
  const kind = (char) => /[\p{L}\p{N}\p{M}_]/u.test(char) ? "word" : /\s/u.test(char) ? "space" : "punctuation";
17293
17402
  const selectedKind = kind(value[pivot] ?? "");
17294
17403
  let start = pivot;
@@ -17297,16 +17406,43 @@ function wordRangeAtLogicalOffset(value, offset) {
17297
17406
  while (end < value.length && kind(value[end] ?? "") === selectedKind && value[end] !== "\uFFFC") end++;
17298
17407
  return { start, end };
17299
17408
  }
17409
+ function visualLineRangeAtPoint(input, backdrop, clientY) {
17410
+ const style = getComputedStyle(input);
17411
+ const rect = backdrop.getBoundingClientRect();
17412
+ const left = rect.left + (Number.parseFloat(style.paddingLeft) || 0) + 1;
17413
+ const right = rect.right - (Number.parseFloat(style.paddingRight) || 0) - 1;
17414
+ const parsedLineHeight = Number.parseFloat(style.lineHeight);
17415
+ const fontSize = Number.parseFloat(style.fontSize);
17416
+ const lineHeight = parsedLineHeight > 0 ? parsedLineHeight : fontSize > 0 ? fontSize * 1.2 : 20;
17417
+ const first = nearestVisualLogicalOffset(backdrop, input.value.length, left, clientY, lineHeight) ?? logicalOffsetFromPoint(input, backdrop, left, clientY);
17418
+ const last = nearestVisualLogicalOffset(backdrop, input.value.length, right, clientY, lineHeight) ?? logicalOffsetFromPoint(input, backdrop, right, clientY);
17419
+ if (first === void 0 || last === void 0) return void 0;
17420
+ const start = Math.min(first, last);
17421
+ let end = Math.max(first, last);
17422
+ if (input.value[end] === "\n") end++;
17423
+ return { start, end };
17424
+ }
17300
17425
  function adoptAdaptiveChipKeyboardNavigation() {
17301
17426
  let preferredX;
17427
+ let preferredFocus;
17302
17428
  const reset = () => {
17303
17429
  preferredX = void 0;
17430
+ preferredFocus = void 0;
17304
17431
  };
17305
17432
  const onKeyDown = (event) => {
17306
17433
  const vertical = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "PageUp" || event.key === "PageDown";
17307
17434
  const lineEdge = event.key === "Home" || event.key === "End";
17308
17435
  if (!vertical && !lineEdge) {
17309
17436
  reset();
17437
+ if ((event.key === "ArrowLeft" || event.key === "ArrowRight") && event.target instanceof HTMLTextAreaElement) {
17438
+ const input2 = event.target;
17439
+ const chip = input2.closest("[data-composer-card]")?.querySelector('[data-decoration="chip"]');
17440
+ if (chip instanceof Element) {
17441
+ queueMicrotask(() => {
17442
+ caretVisualAffinity.set(input2, event.key === "ArrowLeft" ? "backward" : "forward");
17443
+ });
17444
+ }
17445
+ }
17310
17446
  return;
17311
17447
  }
17312
17448
  if (event.defaultPrevented || event.isComposing || event.keyCode === 229 || event.altKey || event.ctrlKey || event.metaKey) return;
@@ -17314,8 +17450,9 @@ function adoptAdaptiveChipKeyboardNavigation() {
17314
17450
  const input = event.target;
17315
17451
  const backdrop = input.closest("[data-composer-card]")?.querySelector("[data-input-backdrop]:not([data-dsh-ref-height-ruler])");
17316
17452
  if (!(backdrop instanceof HTMLElement) || backdrop.querySelector('[data-decoration="chip"]') === null) return;
17317
- const focus = input.selectionDirection === "backward" ? input.selectionStart : input.selectionEnd;
17318
- let anchorRect = visualCaretRectAtLogicalOffset(backdrop, focus);
17453
+ const pageKey = event.key === "PageUp" || event.key === "PageDown";
17454
+ const focus = pageKey && !event.shiftKey && input.selectionStart !== input.selectionEnd ? event.key === "PageUp" ? input.selectionStart : input.selectionEnd : input.selectionDirection === "backward" ? input.selectionStart : input.selectionEnd;
17455
+ let anchorRect = visualCaretRectAtLogicalOffset(backdrop, focus, caretVisualAffinity.get(input));
17319
17456
  if (anchorRect === void 0) return;
17320
17457
  const style = getComputedStyle(input);
17321
17458
  const lineHeight = Number.parseFloat(style.lineHeight);
@@ -17327,6 +17464,7 @@ function adoptAdaptiveChipKeyboardNavigation() {
17327
17464
  let targetX = anchorRect.left;
17328
17465
  let targetY = anchorRect.top + lineHeight / 2;
17329
17466
  if (vertical) {
17467
+ if (preferredFocus?.input !== input || preferredFocus.offset !== focus || preferredFocus.value !== input.value) preferredX = void 0;
17330
17468
  preferredX ??= anchorRect.left;
17331
17469
  targetX = preferredX;
17332
17470
  const direction = event.key === "ArrowUp" || event.key === "PageUp" ? -1 : 1;
@@ -17336,7 +17474,7 @@ function adoptAdaptiveChipKeyboardNavigation() {
17336
17474
  const viewport = scrollport.getBoundingClientRect();
17337
17475
  if (targetY < viewport.top || targetY > viewport.bottom) {
17338
17476
  scrollport.scrollTop += targetY < viewport.top ? targetY - viewport.top : targetY - viewport.bottom;
17339
- anchorRect = visualCaretRectAtLogicalOffset(backdrop, focus) ?? anchorRect;
17477
+ anchorRect = visualCaretRectAtLogicalOffset(backdrop, focus, caretVisualAffinity.get(input)) ?? anchorRect;
17340
17478
  targetY = anchorRect.top + lineHeight / 2 + direction * distance;
17341
17479
  }
17342
17480
  }
@@ -17353,15 +17491,12 @@ function adoptAdaptiveChipKeyboardNavigation() {
17353
17491
  const selectionAnchor = event.shiftKey ? input.selectionDirection === "backward" ? input.selectionEnd : input.selectionStart : target;
17354
17492
  input.setSelectionRange(Math.min(selectionAnchor, target), Math.max(selectionAnchor, target), target < selectionAnchor ? "backward" : "forward");
17355
17493
  input.dispatchEvent(new Event("select", { bubbles: true }));
17494
+ if (vertical) preferredFocus = { input, offset: target, value: input.value };
17356
17495
  event.preventDefault();
17357
17496
  };
17358
17497
  document.addEventListener("keydown", onKeyDown);
17359
- document.addEventListener("input", reset, true);
17360
- document.addEventListener("pointerdown", reset, true);
17361
17498
  return () => {
17362
17499
  document.removeEventListener("keydown", onKeyDown);
17363
- document.removeEventListener("input", reset, true);
17364
- document.removeEventListener("pointerdown", reset, true);
17365
17500
  };
17366
17501
  }
17367
17502
  function logicalOffsetBeforeChip(root, target) {
@@ -17528,7 +17663,7 @@ function adoptAdaptiveChipCaret() {
17528
17663
  const card = input.closest("[data-composer-card]");
17529
17664
  const backdrop = card?.querySelector("[data-input-backdrop]:not([data-dsh-ref-height-ruler])");
17530
17665
  if (!(backdrop instanceof HTMLElement) || backdrop.querySelector('[data-decoration="chip"]') === null) return hide();
17531
- const rect = visualCaretRectAtLogicalOffset(backdrop, input.selectionEnd);
17666
+ const rect = visualCaretRectAtLogicalOffset(backdrop, input.selectionEnd, caretVisualAffinity.get(input));
17532
17667
  if (rect === void 0 || !Number.isFinite(rect.left) || !Number.isFinite(rect.top)) return hide();
17533
17668
  input.classList.remove("dsh_ref_native_caret_hidden");
17534
17669
  const style = getComputedStyle(input);
@@ -17583,6 +17718,12 @@ function adoptAdaptiveChipCaret() {
17583
17718
  const onActive = () => {
17584
17719
  schedule(true, true);
17585
17720
  };
17721
+ const onSelect = () => {
17722
+ restartBlink = true;
17723
+ revealPending = true;
17724
+ cancelAnimationFrame(frame);
17725
+ update();
17726
+ };
17586
17727
  const onCompositionStart = () => {
17587
17728
  composing = true;
17588
17729
  schedule(true, true);
@@ -17593,6 +17734,7 @@ function adoptAdaptiveChipCaret() {
17593
17734
  };
17594
17735
  for (const event of passiveEvents) document.addEventListener(event, onPassive, true);
17595
17736
  for (const event of activeEvents) document.addEventListener(event, onActive, true);
17737
+ document.addEventListener("select", onSelect, true);
17596
17738
  document.addEventListener("compositionstart", onCompositionStart, true);
17597
17739
  document.addEventListener("compositionend", onCompositionEnd, true);
17598
17740
  schedule();
@@ -17601,6 +17743,7 @@ function adoptAdaptiveChipCaret() {
17601
17743
  observer.disconnect();
17602
17744
  for (const event of passiveEvents) document.removeEventListener(event, onPassive, true);
17603
17745
  for (const event of activeEvents) document.removeEventListener(event, onActive, true);
17746
+ document.removeEventListener("select", onSelect, true);
17604
17747
  document.removeEventListener("compositionstart", onCompositionStart, true);
17605
17748
  document.removeEventListener("compositionend", onCompositionEnd, true);
17606
17749
  hide();
@@ -17663,59 +17806,103 @@ function projectPickerIcon(node, text, marker, kind) {
17663
17806
  function usableCaretRect(rect) {
17664
17807
  return rect.height > 0 || rect.top !== 0 || rect.left !== 0;
17665
17808
  }
17666
- function visualCaretRectAtLogicalOffset(root, target) {
17809
+ function measuredRangeRect(range) {
17810
+ const measure = range.getBoundingClientRect;
17811
+ return typeof measure === "function" ? measure.call(range) : void 0;
17812
+ }
17813
+ function visualCaretRectAtLogicalOffset(root, target, affinity = "forward") {
17667
17814
  const range = rangeAtLogicalOffset(root, target);
17668
17815
  if (range === void 0) return void 0;
17669
- const measured = range.getBoundingClientRect();
17670
- const boundary = caretBoundaryRectAtLogicalOffset(root, target);
17671
- return usableCaretRect(measured) ? { left: measured.left, top: boundary?.top ?? measured.top, source: boundary?.source ?? "text", height: boundary?.height } : boundary;
17816
+ const boundaries = caretBoundaryRectsAtLogicalOffset(root, target);
17817
+ const boundary = affinity === "backward" ? boundaries.before ?? boundaries.after : boundaries.after ?? boundaries.before;
17818
+ if (boundary !== void 0) return boundary;
17819
+ const measured = measuredRangeRect(range);
17820
+ if (measured !== void 0 && usableCaretRect(measured)) return { left: measured.left, top: measured.top, source: "text", height: measured.height };
17821
+ return trailingEmptyLineCaretRect(root, target);
17672
17822
  }
17673
17823
  function visualCaretTop(backdropTop, paddingTop, anchorTop, lineHeight, caretHeight) {
17674
17824
  const contentTop = backdropTop + paddingTop;
17675
17825
  const lineIndex = Math.max(0, Math.round((anchorTop - contentTop) / lineHeight));
17676
17826
  return contentTop + lineIndex * lineHeight + Math.max(0, (lineHeight - caretHeight) / 2);
17677
17827
  }
17678
- function caretBoundaryRectAtLogicalOffset(root, target) {
17828
+ function caretBoundaryRectsAtLogicalOffset(root, target) {
17679
17829
  let logical = 0;
17680
- let trailingChip;
17830
+ let before;
17831
+ let after;
17681
17832
  const visit = (parent) => {
17682
17833
  for (const child of Array.from(parent.childNodes)) {
17683
17834
  if (child instanceof Element && child.matches('[data-decoration="chip"]')) {
17684
17835
  const chip = child.getBoundingClientRect();
17685
- if (target === logical) return { left: chip.left, top: chip.top, height: chip.height, source: "chip" };
17836
+ if (target === logical) after ??= { left: chip.left, top: chip.top, height: chip.height, source: "chip" };
17686
17837
  logical += 1;
17687
- if (target === logical) trailingChip = { left: chip.right, top: chip.top, height: chip.height, source: "chip" };
17838
+ if (target === logical) before = { left: chip.right, top: chip.top, height: chip.height, source: "chip" };
17688
17839
  continue;
17689
17840
  }
17690
17841
  if (child instanceof Text) {
17691
17842
  const start = logical;
17692
17843
  const end = start + child.data.length;
17693
17844
  if (target >= start && target <= end && child.data.length > 0) {
17694
- const range = document.createRange();
17695
17845
  if (target < end) {
17846
+ const range = document.createRange();
17696
17847
  const at = Math.max(0, target - start);
17697
17848
  range.setStart(child, at);
17698
17849
  range.setEnd(child, at + 1);
17699
- const rect = range.getBoundingClientRect();
17700
- if (usableCaretRect(rect)) return { left: rect.left, top: rect.top, height: rect.height, source: "text" };
17850
+ const rect = measuredRangeRect(range);
17851
+ if (rect !== void 0 && usableCaretRect(rect)) after ??= { left: rect.left, top: rect.top, height: rect.height, source: "text" };
17701
17852
  }
17702
17853
  if (target > start) {
17854
+ const range = document.createRange();
17703
17855
  const at = Math.min(child.data.length, target - start);
17704
17856
  range.setStart(child, at - 1);
17705
17857
  range.setEnd(child, at);
17706
- const rect = range.getBoundingClientRect();
17707
- if (usableCaretRect(rect)) return { left: rect.right, top: rect.top, height: rect.height, source: "text" };
17858
+ const rect = measuredRangeRect(range);
17859
+ if (rect !== void 0 && usableCaretRect(rect)) before = { left: rect.right, top: rect.top, height: rect.height, source: "text" };
17708
17860
  }
17709
17861
  }
17710
17862
  logical = end;
17711
17863
  continue;
17712
17864
  }
17713
- const found = visit(child);
17714
- if (found !== void 0) return found;
17865
+ visit(child);
17866
+ }
17867
+ };
17868
+ visit(root);
17869
+ return { before, after };
17870
+ }
17871
+ function trailingEmptyLineCaretRect(root, target) {
17872
+ let logical = 0;
17873
+ let previous = "";
17874
+ const visit = (parent) => {
17875
+ for (const child of Array.from(parent.childNodes)) {
17876
+ if (child instanceof Element && child.matches('[data-decoration="chip"]')) {
17877
+ if (logical < target) previous = "\uFFFC";
17878
+ logical += 1;
17879
+ continue;
17880
+ }
17881
+ if (child instanceof Text) {
17882
+ const end = logical + child.data.length;
17883
+ if (target > logical && target <= end) previous = child.data[target - logical - 1] ?? previous;
17884
+ logical = end;
17885
+ continue;
17886
+ }
17887
+ visit(child);
17715
17888
  }
17716
- return trailingChip;
17717
17889
  };
17718
- return visit(root);
17890
+ visit(root);
17891
+ if (target !== logical || previous !== "\n") return void 0;
17892
+ const style = getComputedStyle(root);
17893
+ const lineHeight = Number.parseFloat(style.lineHeight);
17894
+ const fontSize = Number.parseFloat(style.fontSize);
17895
+ const height = lineHeight > 0 ? lineHeight : fontSize > 0 ? fontSize * 1.2 : 20;
17896
+ const paddingLeft = Number.parseFloat(style.paddingLeft) || 0;
17897
+ const paddingRight = Number.parseFloat(style.paddingRight) || 0;
17898
+ const paddingBottom = Number.parseFloat(style.paddingBottom) || 0;
17899
+ const rect = root.getBoundingClientRect();
17900
+ return {
17901
+ left: style.direction === "rtl" ? rect.right - paddingRight : rect.left + paddingLeft,
17902
+ top: Math.max(rect.top, rect.bottom - paddingBottom - height),
17903
+ height,
17904
+ source: "text"
17905
+ };
17719
17906
  }
17720
17907
  function rangeAtLogicalOffset(root, target) {
17721
17908
  const range = document.createRange();
@@ -17765,6 +17952,27 @@ function rangeBetweenLogicalOffsets(root, start, end) {
17765
17952
  return range;
17766
17953
  }
17767
17954
 
17955
+ // src/client/notice.ts
17956
+ var NOTICE_DURATION_MS = 5e3;
17957
+ function createAutoDismissNotice(read, write, durationMs = NOTICE_DURATION_MS) {
17958
+ let timer;
17959
+ return {
17960
+ show(notice, patch = {}) {
17961
+ if (timer) clearTimeout(timer);
17962
+ write({ ...read(), ...patch, notice });
17963
+ timer = setTimeout(() => {
17964
+ const current = read();
17965
+ if (current.notice === notice) write({ ...current, notice: void 0 });
17966
+ timer = void 0;
17967
+ }, durationMs);
17968
+ },
17969
+ dispose() {
17970
+ if (timer) clearTimeout(timer);
17971
+ timer = void 0;
17972
+ }
17973
+ };
17974
+ }
17975
+
17768
17976
  // src/client/index.ts
17769
17977
  var inject = ["inputTriggers", "remote", "remote.commands", "slots", "connection", "locale"];
17770
17978
  function apply(ctx) {
@@ -17784,9 +17992,11 @@ function apply(ctx) {
17784
17992
  });
17785
17993
  let currentJob = "";
17786
17994
  let poll;
17787
- let settingsNoticeTimer;
17788
17995
  let refreshGeneration = 0;
17789
17996
  const t = ctx.locale.bind(REFERENCE_ANYTHING_NS);
17997
+ const notices = createAutoDismissNotice(() => scope.getSnapshot(), (value) => {
17998
+ scope.set(value);
17999
+ });
17790
18000
  let applySources;
17791
18001
  ctx.effect(() => ctx.locale.register(REFERENCE_ANYTHING_NS, { zh, en }), "reference-anything.client.dictionaries");
17792
18002
  ctx.effect(() => adoptMenuGroupTitleProjection(t), "reference-anything.client.menu-group-localization");
@@ -17917,12 +18127,8 @@ function apply(ctx) {
17917
18127
  try {
17918
18128
  const update = unwrap(await activeRemote.updateStatus());
17919
18129
  if (remote === activeRemote) {
17920
- const current = scope.getSnapshot();
17921
- scope.set({
17922
- ...current,
17923
- update,
17924
- notice: update.updateAvailable ? t("notice.updateAvailable", { version: update.latestVersion }) : current.notice
17925
- });
18130
+ if (update.updateAvailable) notices.show(t("notice.updateAvailable", { version: update.latestVersion }), { update });
18131
+ else scope.set({ ...scope.getSnapshot(), update });
17926
18132
  }
17927
18133
  } catch {
17928
18134
  }
@@ -17983,7 +18189,7 @@ function apply(ctx) {
17983
18189
  await Promise.all([refreshStats(), refreshStorage()]);
17984
18190
  const current = scope.getSnapshot().browse;
17985
18191
  if (current) await browse(current.query, current.provider, 0);
17986
- scope.set({ ...scope.getSnapshot(), notice: t("notice.cleared", { count }), error: void 0 });
18192
+ notices.show(t("notice.cleared", { count }), { error: void 0 });
17987
18193
  } catch (error51) {
17988
18194
  scope.set({ ...scope.getSnapshot(), error: message(error51) });
17989
18195
  }
@@ -17995,7 +18201,7 @@ function apply(ctx) {
17995
18201
  await Promise.all([refreshStats(), refreshStorage()]);
17996
18202
  const current = scope.getSnapshot().browse;
17997
18203
  if (current) await browse(current.query, current.provider, 0);
17998
- scope.set({ ...scope.getSnapshot(), notice: t("notice.cleared", { count }), error: void 0 });
18204
+ notices.show(t("notice.cleared", { count }), { error: void 0 });
17999
18205
  } catch (error51) {
18000
18206
  scope.set({ ...scope.getSnapshot(), error: message(error51) });
18001
18207
  }
@@ -18017,14 +18223,7 @@ function apply(ctx) {
18017
18223
  try {
18018
18224
  const value = unwrap(await remote.settingsUpdate(settings));
18019
18225
  applySources?.(value.picker, value.inputRenderMode);
18020
- const notice = t("notice.settingsSaved");
18021
- if (settingsNoticeTimer) clearTimeout(settingsNoticeTimer);
18022
- scope.set({ ...scope.getSnapshot(), settings: value, error: void 0, notice });
18023
- settingsNoticeTimer = setTimeout(() => {
18024
- const current = scope.getSnapshot();
18025
- if (current.notice === notice) scope.set({ ...current, notice: void 0 });
18026
- settingsNoticeTimer = void 0;
18027
- }, 2400);
18226
+ notices.show(t("notice.settingsSaved"), { settings: value, error: void 0 });
18028
18227
  } catch (error51) {
18029
18228
  scope.set({ ...scope.getSnapshot(), error: message(error51), notice: void 0 });
18030
18229
  }
@@ -18039,7 +18238,7 @@ function apply(ctx) {
18039
18238
  await Promise.all([refreshStats(), refreshStorage()]);
18040
18239
  const current = scope.getSnapshot().browse;
18041
18240
  if (current) await browse(current.query, current.provider, 0);
18042
- scope.set({ ...scope.getSnapshot(), notice: t("notice.cleared", { count }), error: void 0 });
18241
+ notices.show(t("notice.cleared", { count }), { error: void 0 });
18043
18242
  } catch (error51) {
18044
18243
  scope.set({ ...scope.getSnapshot(), error: message(error51) });
18045
18244
  }
@@ -18051,13 +18250,13 @@ function apply(ctx) {
18051
18250
  await Promise.all([refreshStats(), refreshStorage()]);
18052
18251
  const current = scope.getSnapshot().browse;
18053
18252
  if (current) await browse(current.query, current.provider, 0);
18054
- scope.set({ ...scope.getSnapshot(), notice: t("notice.cleared", { count }), error: void 0 });
18253
+ notices.show(t("notice.cleared", { count }), { error: void 0 });
18055
18254
  } catch (error51) {
18056
18255
  scope.set({ ...scope.getSnapshot(), error: message(error51) });
18057
18256
  }
18058
18257
  };
18059
18258
  ctx.effect(() => () => {
18060
- if (settingsNoticeTimer) clearTimeout(settingsNoticeTimer);
18259
+ notices.dispose();
18061
18260
  }, "reference-anything.client.settings-notice");
18062
18261
  const startSync = async (providers, mode) => {
18063
18262
  if (!remote) return;
@@ -18130,7 +18329,7 @@ function apply(ctx) {
18130
18329
  try {
18131
18330
  if (!remote) return;
18132
18331
  const update = unwrap(await remote.checkUpdate());
18133
- scope.set({ ...scope.getSnapshot(), update, error: void 0, notice: update.updateAvailable ? void 0 : t("notice.upToDate", { version: update.currentVersion }) });
18332
+ notices.show(update.updateAvailable ? t("notice.updateAvailable", { version: update.latestVersion }) : t("notice.upToDate", { version: update.currentVersion }), { update, error: void 0 });
18134
18333
  } catch (error51) {
18135
18334
  scope.set({ ...scope.getSnapshot(), error: message(error51), notice: void 0 });
18136
18335
  }
@@ -18146,12 +18345,7 @@ function apply(ctx) {
18146
18345
  updateAvailable: false,
18147
18346
  checkedAt: Date.now()
18148
18347
  };
18149
- scope.set({
18150
- ...scope.getSnapshot(),
18151
- update,
18152
- error: void 0,
18153
- notice: result.restartRequired ? t("notice.updateInstalled", { version: result.version }) : t("notice.upToDate", { version: previous?.currentVersion || result.version })
18154
- });
18348
+ notices.show(result.restartRequired ? t("notice.updateInstalled", { version: result.version }) : t("notice.upToDate", { version: previous?.currentVersion || result.version }), { update, error: void 0 });
18155
18349
  } catch (error51) {
18156
18350
  scope.set({ ...scope.getSnapshot(), error: message(error51), notice: void 0 });
18157
18351
  }
@@ -18182,11 +18376,11 @@ function apply(ctx) {
18182
18376
  });
18183
18377
  if (!health?.version || !health.opencliCompatible) throw new Error(t("settings.opencliStillUnavailable"));
18184
18378
  if (setupReady(health)) {
18185
- scope.set({ ...scope.getSnapshot(), error: void 0, notice: t("notice.setupComplete"), setupStep: "complete" });
18379
+ notices.show(t("notice.setupComplete"), { error: void 0, setupStep: "complete" });
18186
18380
  } else if (health?.extensionConnected && !health.connectivityOk) {
18187
18381
  scope.set({ ...scope.getSnapshot(), error: t("settings.connectivityStillUnavailable"), notice: void 0, setupStep: void 0 });
18188
18382
  } else if (!health?.extensionConnected) {
18189
- scope.set({ ...scope.getSnapshot(), error: void 0, notice: extensionPageOpened ? t("notice.oneClickSetup") : t("notice.extensionStoreBlocked"), setupStep: "extension" });
18383
+ notices.show(extensionPageOpened ? t("notice.oneClickSetup") : t("notice.extensionStoreBlocked"), { error: void 0, setupStep: "extension" });
18190
18384
  } else {
18191
18385
  scope.set({ ...scope.getSnapshot(), error: t("settings.setupIncomplete"), notice: void 0, setupStep: void 0 });
18192
18386
  }
@@ -18202,7 +18396,7 @@ function apply(ctx) {
18202
18396
  const settings = scope.getSnapshot().settings;
18203
18397
  unwrap(await remote.settingsUpdate({ ...settings, opencliPath: discovery.executable }));
18204
18398
  await refresh();
18205
- scope.set({ ...scope.getSnapshot(), error: void 0, notice: t("notice.opencliFound", { version: discovery.version }) });
18399
+ notices.show(t("notice.opencliFound", { version: discovery.version }), { error: void 0 });
18206
18400
  } catch (error51) {
18207
18401
  scope.set({ ...scope.getSnapshot(), error: message(error51), notice: void 0 });
18208
18402
  }
@@ -18214,7 +18408,7 @@ function apply(ctx) {
18214
18408
  await refresh();
18215
18409
  const health = scope.getSnapshot().health;
18216
18410
  if (!health?.version || !health.opencliCompatible) throw new Error(t("settings.opencliStillUnavailable"));
18217
- scope.set({ ...scope.getSnapshot(), error: void 0, notice: t("notice.opencliInstalled", { version: discovery.version }) });
18411
+ notices.show(t("notice.opencliInstalled", { version: discovery.version }), { error: void 0 });
18218
18412
  } catch (error51) {
18219
18413
  scope.set({ ...scope.getSnapshot(), error: message(error51), notice: void 0 });
18220
18414
  }
@@ -18227,7 +18421,7 @@ function apply(ctx) {
18227
18421
  await refresh();
18228
18422
  const health = scope.getSnapshot().health;
18229
18423
  if (!health?.extensionConnected) throw new Error(t("settings.profileStillUnavailable"));
18230
- scope.set({ ...scope.getSnapshot(), error: void 0, notice: t("notice.profileSelected") });
18424
+ notices.show(t("notice.profileSelected"), { error: void 0 });
18231
18425
  } catch (error51) {
18232
18426
  scope.set({ ...scope.getSnapshot(), error: message(error51), notice: void 0 });
18233
18427
  }
@@ -18239,7 +18433,7 @@ function apply(ctx) {
18239
18433
  await refresh();
18240
18434
  const health = scope.getSnapshot().health;
18241
18435
  if (!health?.pluginInstalled || !health.adapterCompatible) throw new Error(t("settings.adapterStillUnavailable"));
18242
- scope.set({ ...scope.getSnapshot(), error: void 0, notice: t("notice.adapterInstalled") });
18436
+ notices.show(t("notice.adapterInstalled"), { error: void 0 });
18243
18437
  } catch (error51) {
18244
18438
  scope.set({ ...scope.getSnapshot(), error: message(error51), notice: void 0 });
18245
18439
  }
@@ -18251,7 +18445,7 @@ function apply(ctx) {
18251
18445
  await refresh();
18252
18446
  const health = scope.getSnapshot().health;
18253
18447
  if (!health?.daemonRunning || health.daemonStale) throw new Error(t("settings.daemonStillUnavailable"));
18254
- scope.set({ ...scope.getSnapshot(), error: void 0, notice: t("notice.daemonRestarted") });
18448
+ notices.show(t("notice.daemonRestarted"), { error: void 0 });
18255
18449
  } catch (error51) {
18256
18450
  scope.set({ ...scope.getSnapshot(), error: message(error51), notice: void 0 });
18257
18451
  }
package/lib/update.js CHANGED
@@ -91,7 +91,10 @@ export class PackageUpdateManager {
91
91
  export async function fetchLatestVersion(signal) {
92
92
  const timeout = AbortSignal.timeout(CHECK_TIMEOUT_MS);
93
93
  const response = await fetch(NPM_LATEST_URL, {
94
- headers: { accept: 'application/vnd.npm.install-v1+json' },
94
+ // The registry's package metadata endpoint accepts the install-v1 media
95
+ // type, but its `/latest` dist-tag endpoint responds 406 to that same
96
+ // header. Request ordinary JSON because this endpoint returns one manifest.
97
+ headers: { accept: 'application/json' },
95
98
  signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
96
99
  });
97
100
  if (!response.ok)
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "dsh-reference-anything",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Reference locally mirrored ChatGPT, Claude, Gemini, DeepSeek, Grok, and Kimi conversations from DeepSeek Harness",
5
- "license": "MIT",
6
- "publishConfig": {
7
- "access": "public"
8
- },
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
9
  "type": "module",
10
10
  "repository": {
11
11
  "type": "git",
@@ -53,9 +53,9 @@
53
53
  "opencli-plugin/*.js",
54
54
  "opencli-plugin/opencli-plugin.json",
55
55
  "opencli-plugin/package.json",
56
- "cordis.patch.yml",
57
- "images/logo.png",
58
- "NOTICE.md",
56
+ "cordis.patch.yml",
57
+ "images/logo.png",
58
+ "NOTICE.md",
59
59
  "README.md"
60
60
  ],
61
61
  "dsh": {
@@ -78,8 +78,8 @@
78
78
  "build": "node build.mjs",
79
79
  "typecheck": "tsc -p tsconfig.json --noEmit",
80
80
  "test": "vitest run",
81
- "check": "pnpm run typecheck && pnpm run test && pnpm run build",
82
- "prepublishOnly": "pnpm run check",
81
+ "check": "pnpm run typecheck && pnpm run test && pnpm run build",
82
+ "prepublishOnly": "pnpm run check",
83
83
  "prepare": "node build.mjs"
84
84
  },
85
85
  "peerDependencies": {