dsh-input-assistant 0.0.0-stage → 0.1.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/lib/client.js ADDED
@@ -0,0 +1,1704 @@
1
+ window.__ModuleLoader__.load({id:"dsh-input-assistant",factory:(require)=>{
2
+ var module={exports:{}};var exports=module.exports;
3
+ "use strict";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name2 in all)
10
+ __defProp(target, name2, { get: all[name2], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/browser/index.ts
23
+ var index_exports = {};
24
+ __export(index_exports, {
25
+ BrowserRuntime: () => BrowserRuntime,
26
+ InputAssistantController: () => InputAssistantController,
27
+ apply: () => apply,
28
+ inject: () => inject,
29
+ name: () => name
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/browser/components.tsx
34
+ var import_react = require("react");
35
+ var import_react_dom = require("react-dom");
36
+
37
+ // src/core/diff.ts
38
+ var tokenPattern = /(\s+|[^\p{L}\p{N}_]|[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}])/u;
39
+ var split = (text) => text.split(tokenPattern).filter(Boolean);
40
+ var coarsePattern = /(\n+|[。!?;,、.!?;]+\s*)/u;
41
+ var splitCoarse = (text) => text.split(coarsePattern).filter(Boolean);
42
+ var MAX_CELLS = 1e6;
43
+ function diffTokens(a, b) {
44
+ const result = [];
45
+ const push = (type, value) => {
46
+ if (!value) return;
47
+ const last = result[result.length - 1];
48
+ if (last?.type === type) last.value += value;
49
+ else result.push({ type, value });
50
+ };
51
+ const rows = Array.from({ length: a.length + 1 }, () => new Uint32Array(b.length + 1));
52
+ for (let i2 = a.length - 1; i2 >= 0; i2--) {
53
+ for (let j2 = b.length - 1; j2 >= 0; j2--) {
54
+ rows[i2][j2] = a[i2] === b[j2] ? rows[i2 + 1][j2 + 1] + 1 : Math.max(rows[i2 + 1][j2], rows[i2][j2 + 1]);
55
+ }
56
+ }
57
+ let i = 0, j = 0;
58
+ while (i < a.length || j < b.length) {
59
+ if (i < a.length && j < b.length && a[i] === b[j]) {
60
+ push("equal", a[i++]);
61
+ j++;
62
+ } else if (i < a.length && (j === b.length || rows[i + 1][j] >= rows[i][j + 1])) push("delete", a[i++]);
63
+ else push("insert", b[j++]);
64
+ }
65
+ return result;
66
+ }
67
+ function diffText(before, after) {
68
+ if (before === after) return [{ type: "equal", value: before }];
69
+ const a = split(before), b = split(after);
70
+ let head = 0, tail = 0;
71
+ while (head < a.length && head < b.length && a[head] === b[head]) head++;
72
+ while (tail < a.length - head && tail < b.length - head && a[a.length - tail - 1] === b[b.length - tail - 1]) tail++;
73
+ const middleA = a.slice(head, a.length - tail), middleB = b.slice(head, b.length - tail);
74
+ const result = [];
75
+ const push = (type, value) => {
76
+ if (!value) return;
77
+ const last = result[result.length - 1];
78
+ if (last?.type === type) last.value += value;
79
+ else result.push({ type, value });
80
+ };
81
+ push("equal", a.slice(0, head).join(""));
82
+ if (middleA.length === 0) push("insert", middleB.join(""));
83
+ else if (middleB.length === 0) push("delete", middleA.join(""));
84
+ else if (middleA.length * middleB.length <= MAX_CELLS) for (const part of diffTokens(middleA, middleB)) push(part.type, part.value);
85
+ else {
86
+ const coarseA = splitCoarse(middleA.join("")), coarseB = splitCoarse(middleB.join(""));
87
+ if (coarseA.length * coarseB.length <= MAX_CELLS) for (const part of diffTokens(coarseA, coarseB)) push(part.type, part.value);
88
+ else {
89
+ push("delete", middleA.join(""));
90
+ push("insert", middleB.join(""));
91
+ }
92
+ }
93
+ push("equal", tail ? a.slice(a.length - tail).join("") : "");
94
+ return result;
95
+ }
96
+
97
+ // src/browser/dom.ts
98
+ var CHIP_SELECTOR = "[data-composer-chip],[data-composer-text-ref],[data-composer-command]";
99
+ function findEditor(anchor) {
100
+ return anchor?.closest("[data-composer-card]")?.querySelector("[data-composer-input]") ?? null;
101
+ }
102
+ function focusedIn(editor) {
103
+ return editor !== null && (document.activeElement === editor || editor.contains(document.activeElement));
104
+ }
105
+ function referenceOverlaps(span, references) {
106
+ let shrink = 0;
107
+ for (const reference of references) {
108
+ const start = reference.offset - shrink;
109
+ if (span.start < start + 1 && span.end > start) return true;
110
+ shrink += reference.length - 1;
111
+ }
112
+ return false;
113
+ }
114
+ function clipboardOffset(detectOffset, references) {
115
+ let shrink = 0;
116
+ for (const reference of references) {
117
+ const start = reference.offset - shrink;
118
+ if (detectOffset <= start) break;
119
+ shrink += reference.length - 1;
120
+ }
121
+ return detectOffset + shrink;
122
+ }
123
+ function selectedPlainText(editor, draft, span, references) {
124
+ if (span.start === span.end) return null;
125
+ if (referenceOverlaps(span, references)) return null;
126
+ const selection = editor?.ownerDocument.getSelection();
127
+ if (!editor || !selection || selection.rangeCount !== 1) return null;
128
+ const range = selection.getRangeAt(0);
129
+ if (!editor.contains(range.startContainer) || !editor.contains(range.endContainer)) return null;
130
+ for (const chip of editor.querySelectorAll(CHIP_SELECTOR)) {
131
+ if (range.intersectsNode(chip)) return null;
132
+ }
133
+ const clipboardStart = clipboardOffset(span.start, references);
134
+ const clipboardEnd = clipboardOffset(span.end, references);
135
+ const text = draft.slice(clipboardStart, clipboardEnd);
136
+ if (!text) return null;
137
+ if (references.length && range.toString() !== text) return null;
138
+ return { text, clipboardStart, clipboardEnd };
139
+ }
140
+ function measureGhost(editor, empty) {
141
+ if (!editor || !focusedIn(editor)) return null;
142
+ const editorBox = editor.getBoundingClientRect();
143
+ if (editorBox.width <= 0 || editorBox.height <= 0) return null;
144
+ const style = getComputedStyle(editor);
145
+ const selection = editor.ownerDocument.getSelection();
146
+ let rect = null;
147
+ if (selection?.rangeCount === 1) {
148
+ const range = selection.getRangeAt(0);
149
+ if (!range.collapsed || !editor.contains(range.startContainer)) return null;
150
+ if (typeof range.getBoundingClientRect === "function") {
151
+ const measured = range.getBoundingClientRect();
152
+ if (measured.height > 0 && measured.left >= editorBox.left - 1 && measured.left <= editorBox.right + 1) rect = measured;
153
+ }
154
+ if (!rect && !empty && range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) {
155
+ const preceding = range.cloneRange();
156
+ preceding.setStart(range.startContainer, range.startOffset - 1);
157
+ const last = preceding.getBoundingClientRect();
158
+ if (last.height > 0) rect = new DOMRect(last.right, last.top, 0, last.height);
159
+ }
160
+ }
161
+ if (!rect && empty) {
162
+ rect = new DOMRect(editorBox.left + parseFloat(style.paddingLeft || "0"), editorBox.top + parseFloat(style.paddingTop || "0"), 0, parseFloat(style.lineHeight) || parseFloat(style.fontSize));
163
+ }
164
+ if (!rect) return null;
165
+ const viewport = window.visualViewport;
166
+ const visible = {
167
+ left: Math.max(editorBox.left, viewport?.offsetLeft ?? 0),
168
+ top: Math.max(editorBox.top, viewport?.offsetTop ?? 0),
169
+ right: Math.min(editorBox.right, (viewport?.offsetLeft ?? 0) + (viewport?.width ?? window.innerWidth)),
170
+ bottom: Math.min(editorBox.bottom, (viewport?.offsetTop ?? 0) + (viewport?.height ?? window.innerHeight))
171
+ };
172
+ for (let ancestor = editor.parentElement; ancestor && ancestor !== document.body; ancestor = ancestor.parentElement) {
173
+ const clipping = getComputedStyle(ancestor);
174
+ const paint = /paint|strict|content/.test(clipping.contain);
175
+ const clipX = paint || /hidden|clip|auto|scroll/.test(clipping.overflowX || clipping.overflow);
176
+ const clipY = paint || /hidden|clip|auto|scroll/.test(clipping.overflowY || clipping.overflow);
177
+ if (clipX || clipY) {
178
+ const box = ancestor.getBoundingClientRect();
179
+ if (clipX) {
180
+ visible.left = Math.max(visible.left, box.left);
181
+ visible.right = Math.min(visible.right, box.right);
182
+ }
183
+ if (clipY) {
184
+ visible.top = Math.max(visible.top, box.top);
185
+ visible.bottom = Math.min(visible.bottom, box.bottom);
186
+ }
187
+ }
188
+ }
189
+ if (rect.left < visible.left - 1 || rect.top < visible.top - 1 || rect.bottom > visible.bottom + 1) return null;
190
+ const width = Math.min(editorBox.right - parseFloat(style.paddingRight || "0"), visible.right) - rect.left;
191
+ if (width < 24) return null;
192
+ return {
193
+ left: rect.left,
194
+ top: rect.top,
195
+ width,
196
+ maxHeight: visible.bottom - rect.top,
197
+ font: style.font,
198
+ lineHeight: style.lineHeight,
199
+ letterSpacing: style.letterSpacing
200
+ };
201
+ }
202
+ function nativeMenuOpen(editor) {
203
+ const scope = editor?.closest("[data-composer-seat]") ?? editor?.closest("[data-composer-card]");
204
+ return Boolean(scope?.querySelector('[data-trigger-menu],[role="listbox"]'));
205
+ }
206
+ function canUseTab(event, editor, composing) {
207
+ return event.key === "Tab" && !event.defaultPrevented && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey && !event.isComposing && event.keyCode !== 229 && !composing && focusedIn(editor) && !editor?.hasAttribute("data-composer-composing") && !nativeMenuOpen(editor);
208
+ }
209
+
210
+ // src/browser/floating.ts
211
+ function placeFloating(anchor, contentHeight, viewport, preferredWidth = 440) {
212
+ const margin = 12, gap = 8;
213
+ const width = Math.max(1, Math.min(preferredWidth, viewport.width - margin * 2));
214
+ const left = Math.max(viewport.left + margin, Math.min(anchor.right - width, viewport.left + viewport.width - margin - width));
215
+ const above = Math.max(0, anchor.top - viewport.top - margin - gap);
216
+ const below = Math.max(0, viewport.top + viewport.height - margin - anchor.bottom - gap);
217
+ const maximum = Math.max(1, Math.min(560, viewport.height - margin * 2));
218
+ const useAbove = above >= Math.min(contentHeight, maximum) || above >= below;
219
+ const room = useAbove ? above : below;
220
+ if (room < Math.min(120, maximum)) return { left, top: viewport.top + margin, width, maxHeight: maximum };
221
+ const maxHeight = Math.min(maximum, room);
222
+ const height = Math.min(Math.max(1, contentHeight), maxHeight);
223
+ const top = useAbove ? anchor.top - gap - height : anchor.bottom + gap;
224
+ return { left, top: Math.max(viewport.top + margin, top), width, maxHeight };
225
+ }
226
+
227
+ // src/browser/components.tsx
228
+ var import_jsx_runtime = require("react/jsx-runtime");
229
+ var EMPTY_MODEL = { current: null };
230
+ var noopSubscribe = () => () => {
231
+ };
232
+ function useFloatingPlacement(anchor, surface, visible, card = false) {
233
+ const [placement, setPlacement] = (0, import_react.useState)(null);
234
+ (0, import_react.useLayoutEffect)(() => {
235
+ if (!visible) {
236
+ setPlacement(null);
237
+ return;
238
+ }
239
+ const update = () => {
240
+ const trigger = card ? findEditor(anchor.current)?.closest("[data-composer-card]") : anchor.current;
241
+ const element = surface.current;
242
+ if (!trigger || !element) return;
243
+ const viewport = window.visualViewport;
244
+ const bounds = {
245
+ left: viewport?.offsetLeft ?? 0,
246
+ top: viewport?.offsetTop ?? 0,
247
+ width: viewport?.width ?? window.innerWidth,
248
+ height: viewport?.height ?? window.innerHeight
249
+ };
250
+ const rect = trigger.getBoundingClientRect();
251
+ const next = placeFloating(
252
+ rect,
253
+ element.scrollHeight || element.getBoundingClientRect().height || 160,
254
+ bounds,
255
+ card ? Math.min(640, rect.width || 440) : 440
256
+ );
257
+ setPlacement((previous) => previous && Object.keys(next).every((key) => previous[key] === next[key]) ? previous : next);
258
+ };
259
+ update();
260
+ window.addEventListener("resize", update);
261
+ window.addEventListener("scroll", update, true);
262
+ window.visualViewport?.addEventListener("resize", update);
263
+ window.visualViewport?.addEventListener("scroll", update);
264
+ const observer = typeof ResizeObserver === "function" ? new ResizeObserver(update) : null;
265
+ if (surface.current) observer?.observe(surface.current);
266
+ if (anchor.current) observer?.observe(anchor.current);
267
+ return () => {
268
+ window.removeEventListener("resize", update);
269
+ window.removeEventListener("scroll", update, true);
270
+ window.visualViewport?.removeEventListener("resize", update);
271
+ window.visualViewport?.removeEventListener("scroll", update);
272
+ observer?.disconnect();
273
+ };
274
+ }, [anchor, surface, visible, card]);
275
+ return placement;
276
+ }
277
+ function useController(runtime, sessionId) {
278
+ const controller = (0, import_react.useMemo)(() => runtime.controllerFor(sessionId), [runtime, sessionId]);
279
+ const state = (0, import_react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
280
+ (0, import_react.useEffect)(() => runtime.retain(sessionId), [runtime, sessionId]);
281
+ return { controller, state };
282
+ }
283
+ function AssistantRight({ runtime, sessionId, useInput, useSession, inputActions }) {
284
+ const { controller, state } = useController(runtime, sessionId);
285
+ const input = useInput((value) => value);
286
+ const running = useSession((value) => value.running);
287
+ const removed = useSession((value) => value.removed);
288
+ const generation = (0, import_react.useSyncExternalStore)(
289
+ runtime.connection.generation.subscribe.bind(runtime.connection.generation),
290
+ () => runtime.transport.generation(),
291
+ () => null
292
+ );
293
+ const directory = (0, import_react.useMemo)(() => runtime.models?.directoryFor(sessionId), [runtime, sessionId]);
294
+ const model = (0, import_react.useSyncExternalStore)(
295
+ directory?.store.subscribe.bind(directory.store) ?? noopSubscribe,
296
+ directory?.store.getSnapshot.bind(directory.store) ?? (() => EMPTY_MODEL),
297
+ () => EMPTY_MODEL
298
+ );
299
+ const anchor = (0, import_react.useRef)(null);
300
+ const menuRef = (0, import_react.useRef)(null);
301
+ const noticeRef = (0, import_react.useRef)(null);
302
+ const [open, setOpen] = (0, import_react.useState)(false);
303
+ const menuPlacement = useFloatingPlacement(anchor, menuRef, open);
304
+ const noticePlacement = useFloatingPlacement(anchor, noticeRef, !open && Boolean(state.notice));
305
+ const [provider, setProvider] = (0, import_react.useState)(state.settings.provider);
306
+ const [modelId, setModelId] = (0, import_react.useState)(state.settings.model);
307
+ (0, import_react.useEffect)(() => {
308
+ void directory?.load().catch(() => {
309
+ });
310
+ }, [directory]);
311
+ (0, import_react.useEffect)(() => {
312
+ setProvider(state.settings.provider);
313
+ setModelId(state.settings.model);
314
+ }, [state.settings.provider, state.settings.model]);
315
+ (0, import_react.useLayoutEffect)(() => {
316
+ if (removed) {
317
+ controller.dispose();
318
+ return;
319
+ }
320
+ controller.bind({ sessionId, input, actions: inputActions, running, editor: findEditor(anchor.current), route: model.current ?? void 0 });
321
+ }, [controller, sessionId, input, inputActions, running, model.current, removed, generation]);
322
+ (0, import_react.useEffect)(() => {
323
+ const editor = findEditor(anchor.current);
324
+ if (!editor) return;
325
+ const selectionChanged = () => controller.notifySelectionChange();
326
+ const compositionStart = () => controller.setComposing(true);
327
+ const compositionEnd = () => controller.setComposing(false);
328
+ const keyDown = (event) => {
329
+ if (!controller.getSnapshot().suggestion || !canUseTab(event, editor, false)) return;
330
+ if (![...document.querySelectorAll("[data-dsh-ia-visible-suggestion]")].some((node) => node.getAttribute("data-dsh-ia-session") === sessionId)) return;
331
+ if (controller.acceptSuggestion()) event.preventDefault();
332
+ };
333
+ document.addEventListener("selectionchange", selectionChanged);
334
+ editor.addEventListener("focus", selectionChanged);
335
+ editor.addEventListener("blur", selectionChanged);
336
+ editor.addEventListener("compositionstart", compositionStart);
337
+ editor.addEventListener("compositionend", compositionEnd);
338
+ window.addEventListener("keydown", keyDown);
339
+ return () => {
340
+ document.removeEventListener("selectionchange", selectionChanged);
341
+ editor.removeEventListener("focus", selectionChanged);
342
+ editor.removeEventListener("blur", selectionChanged);
343
+ editor.removeEventListener("compositionstart", compositionStart);
344
+ editor.removeEventListener("compositionend", compositionEnd);
345
+ window.removeEventListener("keydown", keyDown);
346
+ };
347
+ }, [controller, sessionId]);
348
+ (0, import_react.useEffect)(() => {
349
+ if (!open) return;
350
+ const close = (event) => {
351
+ if (event.target instanceof Node && !anchor.current?.contains(event.target) && !menuRef.current?.contains(event.target)) setOpen(false);
352
+ };
353
+ const escape = (event) => {
354
+ if (event.key === "Escape" && !event.defaultPrevented) {
355
+ setOpen(false);
356
+ event.preventDefault();
357
+ }
358
+ };
359
+ document.addEventListener("pointerdown", close);
360
+ window.addEventListener("keydown", escape);
361
+ return () => {
362
+ document.removeEventListener("pointerdown", close);
363
+ window.removeEventListener("keydown", escape);
364
+ };
365
+ }, [open]);
366
+ const preserveSelection = (event) => {
367
+ event.preventDefault();
368
+ };
369
+ const canEdit = input.phase === "plain" && !state.busy && !state.settingsSaving;
370
+ const canOptimize = canEdit && state.settings.optimizeEnabled;
371
+ const pure = !input.occurrences.length && !input.attachmentIds.length;
372
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ia dsh-ia-root", ref: anchor, "data-dsh-input-assistant": "", children: [
373
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
374
+ "button",
375
+ {
376
+ className: "dsh-ia-trigger",
377
+ type: "button",
378
+ disabled: !canOptimize || !input.draft.trim() || !state.connected,
379
+ onPointerDown: preserveSelection,
380
+ onClick: () => {
381
+ void controller.optimize();
382
+ },
383
+ title: "\u6709\u9009\u533A\u65F6\u4F18\u5316\u9009\u533A\uFF0C\u5426\u5219\u4F18\u5316\u6574\u7A3F",
384
+ children: state.busy === "optimize" ? "\u4F18\u5316\u4E2D\u2026" : "\u4F18\u5316"
385
+ }
386
+ ),
387
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "aria-label": "\u8F93\u5165\u52A9\u624B\u83DC\u5355", "aria-expanded": open, onPointerDown: preserveSelection, onClick: () => setOpen((value) => !value), children: "\u25BE" }),
388
+ !open && state.notice && (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
389
+ "div",
390
+ {
391
+ ref: noticeRef,
392
+ role: "status",
393
+ className: "dsh-ia dsh-ia-notice",
394
+ style: noticePlacement ?? { visibility: "hidden" },
395
+ onClick: () => controller.clearNotice(),
396
+ children: state.notice
397
+ }
398
+ ), document.body),
399
+ open && (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: menuRef, className: "dsh-ia dsh-ia-menu", "aria-label": "\u8F93\u5165\u52A9\u624B", style: menuPlacement ?? { visibility: "hidden" }, children: [
400
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "\u8F93\u5165\u52A9\u624B" }),
401
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-ia-muted", children: "\u5F53\u524D\u4F1A\u8BDD \xB7 \u91C7\u7EB3\u540E\u7531\u4F60\u53D1\u9001" }),
402
+ state.settingsSaving && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "status", children: "\u8BBE\u7F6E\u4FDD\u5B58\u4E2D\u2026" }),
403
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ia-row", children: [
404
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
405
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", disabled: state.settingsSaving, checked: state.settings.optimizeEnabled, onChange: (event) => {
406
+ void controller.updateSettings({ optimizeEnabled: event.target.checked });
407
+ } }),
408
+ "\u63D0\u793A\u8BCD\u4F18\u5316"
409
+ ] }),
410
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
411
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", disabled: state.settingsSaving, checked: state.settings.stashEnabled, onChange: (event) => {
412
+ void controller.updateSettings({ stashEnabled: event.target.checked });
413
+ } }),
414
+ "\u8349\u7A3F\u6682\u5B58"
415
+ ] })
416
+ ] }),
417
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
418
+ "\u4F18\u5316\u5F3A\u5EA6 ",
419
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
420
+ "select",
421
+ {
422
+ "aria-label": "\u4F18\u5316\u5F3A\u5EA6",
423
+ value: state.settings.optimizationStrength,
424
+ disabled: state.settingsSaving || !state.settings.optimizeEnabled,
425
+ onChange: (event) => {
426
+ void controller.updateSettings({ optimizationStrength: event.target.value });
427
+ },
428
+ children: [
429
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "light", children: "\u8F7B\u5EA6\u6DA6\u8272" }),
430
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "standard", children: "\u6807\u51C6\u4F18\u5316" }),
431
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "deep", children: "\u6DF1\u5EA6\u68B3\u7406" })
432
+ ]
433
+ }
434
+ )
435
+ ] }),
436
+ state.notice && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-ia-notice", role: "status", children: state.notice }),
437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ia-row", children: [
438
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", disabled: !canOptimize, onPointerDown: preserveSelection, onClick: () => controller.lockSelection(), children: [
439
+ "\u4FDD\u62A4\u9009\u4E2D\u539F\u6587",
440
+ state.locked.length ? ` (${state.locked.length})` : ""
441
+ ] }),
442
+ state.locked.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => controller.clearLocks(), children: "\u6E05\u9664\u4FDD\u62A4" })
443
+ ] }),
444
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-ia-muted", children: "\u4EE3\u7801\u3001\u8DEF\u5F84\u548C\u7F51\u5740\u81EA\u52A8\u4FDD\u62A4\uFF1B\u624B\u52A8\u4FDD\u62A4\u5728\u7F16\u8F91\u8349\u7A3F\u540E\u6E05\u9664\u3002" }),
445
+ state.change && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dsh-ia-section", children: [
446
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", { children: "\u67E5\u770B\u6539\u52A8" }),
447
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-ia-pre dsh-ia-diff", children: diffText(state.change.beforeTarget, state.change.afterTarget).map((part, index) => part.type === "insert" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ins", { children: part.value }, index) : part.type === "delete" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("del", { children: part.value }, index) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: part.value }, index)) }),
448
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ia-row", children: [
449
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: !controller.canRestore(), onPointerDown: preserveSelection, onClick: () => controller.restoreOriginal(), children: "\u8FD8\u539F" }),
450
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => {
451
+ void copyText(state.change.beforeTarget, controller);
452
+ }, children: "\u590D\u5236\u539F\u6587" })
453
+ ] }),
454
+ !controller.canRestore() && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-ia-muted", children: "\u8349\u7A3F\u5DF2\u6709\u65B0\u7F16\u8F91\uFF0C\u539F\u6587\u4ECD\u53EF\u590D\u5236\u3002" })
455
+ ] }),
456
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-ia-section", children: [
457
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h4", { children: [
458
+ "\u8349\u7A3F\u6682\u5B58 (",
459
+ state.drafts.length,
460
+ "/3)"
461
+ ] }),
462
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: !state.settings.stashEnabled || !canEdit || !pure || !input.draft.trim() || state.drafts.length >= 3, onPointerDown: preserveSelection, onClick: () => {
463
+ void controller.stash();
464
+ }, children: "\u6682\u5B58\u5E76\u6E05\u7A7A" }),
465
+ !state.settings.stashEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ia-muted", children: "\u6682\u5B58\u5DF2\u5173\u95ED\uFF1B\u5386\u53F2\u8349\u7A3F\u4ECD\u53EF\u67E5\u770B\u548C\u5220\u9664\uFF0C\u91CD\u65B0\u542F\u7528\u540E\u53EF\u6062\u590D\u3002" }),
466
+ !pure && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ia-muted", children: "\u6682\u5B58\u548C\u6062\u590D\u4EC5\u652F\u6301\u65E0\u5F15\u7528\u3001\u65E0\u9644\u4EF6\u7684\u7EAF\u6587\u672C\u8349\u7A3F\u3002" }),
467
+ state.drafts.map((draft) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
468
+ DraftCard,
469
+ {
470
+ draft,
471
+ controller,
472
+ canEdit: state.settings.stashEnabled && canEdit && pure,
473
+ canReplace: state.settings.stashEnabled && canEdit && pure && Boolean(input.draft.trim()) && state.drafts.length >= 3
474
+ },
475
+ draft.id
476
+ )),
477
+ state.drafts.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ia-muted", children: "\u6682\u5B58\u540E\u53EF\u4EE5\u5148\u8FFD\u95EE\u522B\u7684\uFF0C\u518D\u627E\u56DE\u539F\u6765\u7684\u8349\u7A3F\u3002" }),
478
+ state.drafts.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => {
479
+ void controller.clearDrafts();
480
+ }, children: "\u6E05\u7A7A\u6682\u5B58" })
481
+ ] }),
482
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-ia-section", children: [
483
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { children: "\u8F93\u5165\u5EFA\u8BAE" }),
484
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ia-row", children: [
485
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
486
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", disabled: state.settingsSaving, checked: state.settings.nextEnabled, onChange: (event) => {
487
+ void controller.updateSettings({ nextEnabled: event.target.checked });
488
+ } }),
489
+ "\u56DE\u590D\u540E\u9884\u6D4B"
490
+ ] }),
491
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
492
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", disabled: state.settingsSaving, checked: state.settings.completionEnabled, onChange: (event) => {
493
+ void controller.updateSettings({ completionEnabled: event.target.checked });
494
+ } }),
495
+ "\u8F93\u5165\u7EED\u5199"
496
+ ] })
497
+ ] }),
498
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
499
+ "\u663E\u793A\u65B9\u5F0F ",
500
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { "aria-label": "\u663E\u793A\u65B9\u5F0F", disabled: state.settingsSaving, value: state.settings.displayMode, onChange: (event) => {
501
+ void controller.updateSettings({ displayMode: event.target.value });
502
+ }, children: [
503
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "ghost", children: "\u7070\u5B57" }),
504
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "panel", children: "\u5019\u9009\u9762\u677F" })
505
+ ] })
506
+ ] }),
507
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-ia-row", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => controller.dismissSuggestion(), children: "\u6682\u505C\u81F3\u4E0B\u6B21\u7F16\u8F91" }) })
508
+ ] }),
509
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dsh-ia-section", children: [
510
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", { children: "\u8F85\u52A9\u6A21\u578B\u8BBE\u7F6E" }),
511
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-ia-muted", children: "\u4E24\u9879\u7559\u7A7A\u65F6\u8DDF\u968F\u8F93\u5165\u6846\u6A21\u578B\uFF1B\u8F85\u52A9\u8C03\u7528\u5355\u72EC\u6D88\u8017\u6A21\u578B\u7528\u91CF\u3002" }),
512
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ia-field", children: [
513
+ "\u63D0\u4F9B\u5546",
514
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { disabled: state.settingsSaving, value: provider, onChange: (event) => setProvider(event.target.value), placeholder: "\u8DDF\u968F\u8F93\u5165\u6846", autoComplete: "off" })
515
+ ] }),
516
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ia-field", children: [
517
+ "\u6A21\u578B",
518
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { disabled: state.settingsSaving, value: modelId, onChange: (event) => setModelId(event.target.value), placeholder: "\u8DDF\u968F\u8F93\u5165\u6846", autoComplete: "off" })
519
+ ] }),
520
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: state.settingsSaving, onClick: () => {
521
+ void controller.updateSettings({ provider, model: modelId });
522
+ }, children: "\u4FDD\u5B58\u6A21\u578B\u8BBE\u7F6E" })
523
+ ] }),
524
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dsh-ia-section", children: [
525
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", { children: "\u672C\u6B21\u4F7F\u7528\u7EDF\u8BA1" }),
526
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "dsh-ia-muted", children: [
527
+ "\u5F53\u524D\u4F1A\u8BDD\uFF1A\u5C55\u793A ",
528
+ state.metrics.exposures,
529
+ " \u6B21\uFF0C\u91C7\u7EB3 ",
530
+ state.metrics.accepted,
531
+ " \u6B21\uFF0C \u91C7\u7EB3\u540E\u7EE7\u7EED\u7F16\u8F91 ",
532
+ state.metrics.editedAcceptances,
533
+ " \u6B21\uFF0C\u7F16\u8F91\u5B57\u6570\u7EA6 ",
534
+ state.metrics.editedCharacters,
535
+ "\uFF0C\u8F85\u52A9\u8C03\u7528 ",
536
+ state.metrics.calls,
537
+ " \u6B21\u3002"
538
+ ] }),
539
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-ia-muted", children: "\u4EC5\u4FDD\u7559\u5728\u672C\u6B21\u63D2\u4EF6\u8FD0\u884C\u5185\uFF0C\u4E0D\u4E0A\u4F20\uFF1B\u5FFD\u7565\u5EFA\u8BAE\u4E0D\u4F1A\u88AB\u89C6\u4E3A\u8D1F\u53CD\u9988\u3002" })
540
+ ] }),
541
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ia-muted", children: "\u66F4\u591A\u8BBE\u7F6E\uFF1ADSH \u8BBE\u7F6E \u2192 \u8F93\u5165\u52A9\u624B" })
542
+ ] }), document.body)
543
+ ] });
544
+ }
545
+ function DraftCard({ draft, controller, canEdit, canReplace }) {
546
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dsh-ia-draft", children: [
547
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("summary", { children: draft.text.split("\n")[0].slice(0, 50) || "\u672A\u547D\u540D\u8349\u7A3F" }),
548
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("time", { children: new Date(draft.createdAt).toLocaleString() }),
549
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-ia-pre", children: draft.text }),
550
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ia-row", children: [
551
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: !canEdit, onPointerDown: (event) => event.preventDefault(), onClick: () => controller.restoreDraft(draft, "append"), children: "\u8FFD\u52A0" }),
552
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: !canEdit, onPointerDown: (event) => event.preventDefault(), onClick: () => controller.restoreDraft(draft, "replace"), children: "\u66FF\u6362" }),
553
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => {
554
+ void controller.deleteDraft(draft.id);
555
+ }, children: "\u5220\u9664" }),
556
+ canReplace && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onPointerDown: (event) => event.preventDefault(), onClick: () => {
557
+ void controller.stash(draft.id);
558
+ }, children: "\u7528\u5F53\u524D\u8349\u7A3F\u66FF\u6362\u6B64\u6761" })
559
+ ] })
560
+ ] });
561
+ }
562
+ async function copyText(text, controller) {
563
+ try {
564
+ await navigator.clipboard.writeText(text);
565
+ controller.notice("\u539F\u6587\u5DF2\u590D\u5236");
566
+ } catch {
567
+ controller.notice("\u590D\u5236\u4E0D\u53EF\u7528\uFF0C\u8BF7\u5728\u6539\u52A8\u9762\u677F\u624B\u52A8\u9009\u62E9\u539F\u6587");
568
+ }
569
+ }
570
+ function AssistantOverlay({ runtime, sessionId }) {
571
+ const { controller, state } = useController(runtime, sessionId);
572
+ const anchor = (0, import_react.useRef)(null);
573
+ const panelRef = (0, import_react.useRef)(null);
574
+ const [geometry, setGeometry] = (0, import_react.useState)(null);
575
+ const suggestion = state.suggestion;
576
+ const panelPlacement = useFloatingPlacement(anchor, panelRef, Boolean(suggestion) && state.settings.displayMode === "panel", true);
577
+ (0, import_react.useEffect)(() => {
578
+ if (suggestion && (state.settings.displayMode === "panel" && panelPlacement || geometry)) controller.markExposed(suggestion.snapshot.requestId);
579
+ }, [controller, suggestion, geometry, panelPlacement, state.settings.displayMode]);
580
+ (0, import_react.useLayoutEffect)(() => {
581
+ if (!suggestion || state.settings.displayMode !== "ghost") {
582
+ setGeometry(null);
583
+ return;
584
+ }
585
+ const update = () => setGeometry(measureGhost(findEditor(anchor.current), suggestion.snapshot.draft === ""));
586
+ update();
587
+ window.addEventListener("resize", update);
588
+ window.addEventListener("scroll", update, true);
589
+ document.addEventListener("selectionchange", update);
590
+ const editor = findEditor(anchor.current);
591
+ editor?.addEventListener("focus", update);
592
+ editor?.addEventListener("blur", update);
593
+ const resize = typeof ResizeObserver === "function" ? new ResizeObserver(update) : null;
594
+ if (editor) resize?.observe(editor);
595
+ return () => {
596
+ window.removeEventListener("resize", update);
597
+ window.removeEventListener("scroll", update, true);
598
+ document.removeEventListener("selectionchange", update);
599
+ editor?.removeEventListener("focus", update);
600
+ editor?.removeEventListener("blur", update);
601
+ resize?.disconnect();
602
+ };
603
+ }, [suggestion, state.settings.displayMode]);
604
+ const ghostStyle = geometry ? {
605
+ left: geometry.left,
606
+ top: geometry.top,
607
+ width: geometry.width,
608
+ maxHeight: `min(3lh, ${geometry.maxHeight}px)`,
609
+ font: geometry.font,
610
+ lineHeight: geometry.lineHeight,
611
+ letterSpacing: geometry.letterSpacing
612
+ } : void 0;
613
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { ref: anchor, className: "dsh-ia", style: { display: "contents" }, children: [
614
+ suggestion && state.settings.displayMode === "panel" && (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
615
+ "div",
616
+ {
617
+ ref: panelRef,
618
+ className: "dsh-ia dsh-ia-candidate",
619
+ role: "status",
620
+ style: panelPlacement ?? { visibility: "hidden" },
621
+ "data-dsh-ia-visible-suggestion": panelPlacement ? "" : void 0,
622
+ "data-dsh-ia-session": sessionId,
623
+ children: [
624
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ia-candidate-text", children: suggestion.text }),
625
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onPointerDown: (event) => event.preventDefault(), onClick: () => controller.acceptSuggestion(), children: "\u91C7\u7EB3" }),
626
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "aria-label": "\u5FFD\u7565\u5EFA\u8BAE", onClick: () => controller.dismissSuggestion(), children: "\xD7" })
627
+ ]
628
+ }
629
+ ), document.body),
630
+ suggestion && geometry && state.settings.displayMode === "ghost" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
631
+ suggestion.snapshot.draft === "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { hidden: true, "data-dsh-ia-empty-ghost": "" }),
632
+ (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
633
+ "span",
634
+ {
635
+ className: "dsh-ia dsh-ia-ghost",
636
+ style: ghostStyle,
637
+ "aria-hidden": "true",
638
+ "data-dsh-ia-visible-suggestion": "",
639
+ "data-dsh-ia-session": sessionId,
640
+ children: suggestion.text
641
+ }
642
+ ), document.body)
643
+ ] })
644
+ ] });
645
+ }
646
+
647
+ // src/core/types.ts
648
+ var DEFAULT_CONFIG = Object.freeze({
649
+ optimizeEnabled: true,
650
+ stashEnabled: true,
651
+ optimizationStrength: "standard",
652
+ nextEnabled: true,
653
+ completionEnabled: true,
654
+ displayMode: "ghost",
655
+ debounceMs: 600,
656
+ minIntervalMs: 2e3,
657
+ maxHistoryChars: 16e3,
658
+ maxDraftChars: 8e3,
659
+ maxNextChars: 120,
660
+ maxCompletionChars: 80,
661
+ optimizeTimeoutMs: 6e4,
662
+ suggestionTimeoutMs: 15e3,
663
+ provider: "",
664
+ model: ""
665
+ });
666
+ var AssistantError = class extends Error {
667
+ constructor(code, message) {
668
+ super(message);
669
+ this.code = code;
670
+ this.name = "AssistantError";
671
+ }
672
+ };
673
+
674
+ // src/core/snapshot.ts
675
+ function snapshotMatches(expected, current, checkCaret = true) {
676
+ return expected.sessionId === current.sessionId && expected.sourceTurnId === current.sourceTurnId && expected.connectionGeneration === current.connectionGeneration && expected.routeKey === current.routeKey && expected.draftRev === current.draftRev && expected.draft === current.draft && (!checkCaret || expected.span.start === current.span.start && expected.span.end === current.span.end);
677
+ }
678
+
679
+ // src/core/context.ts
680
+ var charCount = (text) => Array.from(text).length;
681
+
682
+ // src/browser/controller.ts
683
+ var emptyMetrics = () => ({ exposures: 0, accepted: 0, editedAcceptances: 0, editedCharacters: 0, calls: 0, totalLatencyMs: 0 });
684
+ var initial = () => ({
685
+ status: null,
686
+ settings: { ...DEFAULT_CONFIG },
687
+ busy: null,
688
+ notice: "",
689
+ suggestion: null,
690
+ change: null,
691
+ drafts: [],
692
+ locked: [],
693
+ connected: false,
694
+ settingsSaving: false,
695
+ metrics: emptyMetrics()
696
+ });
697
+ var InputAssistantController = class {
698
+ constructor(sessionId, transport, metrics, saveSettings) {
699
+ this.sessionId = sessionId;
700
+ this.transport = transport;
701
+ this.saveSettings = saveSettings;
702
+ if (metrics) this.state.metrics = { ...metrics };
703
+ }
704
+ state = initial();
705
+ listeners = /* @__PURE__ */ new Set();
706
+ binding = null;
707
+ active = null;
708
+ timer;
709
+ /** Cache key the armed completion timer was scheduled for; null when no timer is armed. */
710
+ timerKey = null;
711
+ poll;
712
+ polling = false;
713
+ lifecycle = new AbortController();
714
+ disposed = false;
715
+ composing = false;
716
+ settingsPending = false;
717
+ settingsEpoch = 0;
718
+ nextTurns = /* @__PURE__ */ new Set();
719
+ nextCache = null;
720
+ completionCache = /* @__PURE__ */ new Map();
721
+ failedCompletionKey = null;
722
+ lastCompletionStart = -Infinity;
723
+ suppressUntilEdit = null;
724
+ sequence = 0;
725
+ clientGeneration = null;
726
+ lockedRanges = [];
727
+ exposed = /* @__PURE__ */ new Set();
728
+ acceptance = null;
729
+ getSnapshot = () => this.state;
730
+ subscribe = (listener) => {
731
+ this.listeners.add(listener);
732
+ return () => {
733
+ this.listeners.delete(listener);
734
+ };
735
+ };
736
+ patch(update) {
737
+ if (this.disposed) return;
738
+ this.state = { ...this.state, ...update };
739
+ for (const listener of this.listeners) listener();
740
+ }
741
+ start() {
742
+ if (this.poll || this.disposed) return;
743
+ void this.refresh();
744
+ void this.refreshDrafts();
745
+ this.poll = setInterval(() => {
746
+ void this.refresh();
747
+ }, 900);
748
+ }
749
+ dispose() {
750
+ if (this.disposed) return;
751
+ this.cancelActive();
752
+ this.clearTimer();
753
+ clearInterval(this.poll);
754
+ this.lifecycle.abort();
755
+ this.disposed = true;
756
+ this.listeners.clear();
757
+ this.binding = null;
758
+ }
759
+ bind(binding) {
760
+ if (this.disposed || binding.sessionId !== this.sessionId) return;
761
+ const previous = this.binding;
762
+ this.binding = binding;
763
+ const generation = this.transport.generation();
764
+ if (generation !== this.clientGeneration) {
765
+ this.clientGeneration = generation;
766
+ this.invalidate(true);
767
+ this.patch({ connected: false, status: null, drafts: [] });
768
+ if (generation !== null && this.poll) {
769
+ void this.refresh();
770
+ void this.refreshDrafts();
771
+ }
772
+ }
773
+ const changed = previous && (previous.input.draftRev !== binding.input.draftRev || previous.input.draft !== binding.input.draft || this.routeKey(previous.route) !== this.routeKey(binding.route) || previous.running !== binding.running || previous.input.phase !== binding.input.phase || previous.input.attachmentIds.length !== binding.input.attachmentIds.length || previous.input.occurrences.length !== binding.input.occurrences.length);
774
+ if (changed) {
775
+ this.cancelActive();
776
+ if (previous.input.draft !== binding.input.draft) this.observeAcceptedEdit(binding.input.draft);
777
+ if (previous.input.draft !== binding.input.draft) this.lockedRanges = [];
778
+ if (previous.input.draft !== binding.input.draft) this.failedCompletionKey = null;
779
+ this.patch({ suggestion: null, ...previous.input.draft !== binding.input.draft ? { locked: [] } : {} });
780
+ if (this.suppressUntilEdit !== null && binding.input.draft !== this.suppressUntilEdit) this.suppressUntilEdit = null;
781
+ }
782
+ this.driveSuggestions();
783
+ }
784
+ notifySelectionChange() {
785
+ if (nativeMenuOpen(this.binding?.editor ?? null)) {
786
+ if (this.active?.snapshot.operation !== "optimize") this.cancelActive();
787
+ this.clearTimer();
788
+ this.patch({ suggestion: null });
789
+ return;
790
+ }
791
+ if (this.active && this.active.snapshot.operation !== "optimize" && !this.sameCaret(this.active.caret)) this.cancelActive();
792
+ if (this.state.suggestion && !this.isCurrent(this.state.suggestion.snapshot, true)) this.patch({ suggestion: null });
793
+ this.driveSuggestions();
794
+ }
795
+ setComposing(value) {
796
+ this.composing = value;
797
+ if (value) {
798
+ this.cancelActive();
799
+ this.clearTimer();
800
+ this.patch({ suggestion: null });
801
+ } else this.driveSuggestions();
802
+ }
803
+ dismissSuggestion() {
804
+ this.clearTimer();
805
+ this.cancelActive();
806
+ this.patch({ suggestion: null });
807
+ this.suppressUntilEdit = this.binding?.input.draft ?? "";
808
+ }
809
+ clearNotice() {
810
+ this.patch({ notice: "" });
811
+ }
812
+ notice(text) {
813
+ this.patch({ notice: text });
814
+ }
815
+ async refresh() {
816
+ if (this.disposed || this.polling || this.transport.generation() === null) return;
817
+ const generation = this.transport.generation();
818
+ const settingsEpoch = this.settingsEpoch;
819
+ this.polling = true;
820
+ try {
821
+ const status = await this.transport.call("status", { sessionId: this.sessionId }, this.lifecycle.signal);
822
+ if (this.disposed || this.transport.generation() !== generation || settingsEpoch !== this.settingsEpoch) return;
823
+ const previous = this.state.status;
824
+ const changed = previous && (status.sourceTurnId !== previous.sourceTurnId || status.generation !== previous.generation || this.routeKey(status.route) !== this.routeKey(previous.route) || status.running !== previous.running || JSON.stringify(status.settings) !== JSON.stringify(previous.settings));
825
+ if (changed) this.invalidate(true);
826
+ this.patch({ status, settings: status.settings, connected: this.transport.generation() !== null });
827
+ this.driveSuggestions();
828
+ } catch {
829
+ if (!this.disposed) {
830
+ this.invalidate(false, false);
831
+ this.patch({ connected: false });
832
+ }
833
+ } finally {
834
+ this.polling = false;
835
+ }
836
+ }
837
+ async updateSettings(update) {
838
+ if (this.settingsPending) return;
839
+ if (this.saveSettings) {
840
+ try {
841
+ await this.saveSettings(update);
842
+ this.patch({ notice: "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58" });
843
+ } catch (error) {
844
+ this.patch({ notice: this.errorMessage(error) });
845
+ }
846
+ return;
847
+ }
848
+ this.beginSettingsUpdate();
849
+ try {
850
+ const settings = await this.transport.call("settings/set", update, this.lifecycle.signal);
851
+ this.applySettings(settings);
852
+ this.patch({ notice: "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58" });
853
+ } catch (error) {
854
+ this.patch({ notice: this.errorMessage(error) });
855
+ } finally {
856
+ this.endSettingsUpdate();
857
+ }
858
+ }
859
+ beginSettingsUpdate() {
860
+ this.settingsEpoch++;
861
+ this.settingsPending = true;
862
+ this.invalidate(false);
863
+ this.patch({ settingsSaving: true });
864
+ }
865
+ endSettingsUpdate() {
866
+ this.settingsPending = false;
867
+ this.patch({ settingsSaving: false });
868
+ this.driveSuggestions();
869
+ }
870
+ /** A settings page can update every mounted composer without waiting for its status poll. */
871
+ applySettings(settings) {
872
+ this.settingsEpoch++;
873
+ this.invalidate(true);
874
+ this.patch({ settings, ...this.state.status ? { status: { ...this.state.status, settings } } : {} });
875
+ this.driveSuggestions();
876
+ }
877
+ lockSelection() {
878
+ const captured = this.captureTarget(true);
879
+ if (!captured) return;
880
+ if (this.state.locked.includes(captured.text)) return;
881
+ this.lockedRanges.push({ text: captured.text, span: captured.span });
882
+ this.patch({ locked: [...this.state.locked, captured.text], notice: "\u5DF2\u4FDD\u62A4\u9009\u4E2D\u539F\u6587\uFF1B\u7F16\u8F91\u8349\u7A3F\u540E\u9700\u91CD\u65B0\u9501\u5B9A" });
883
+ }
884
+ clearLocks() {
885
+ this.lockedRanges = [];
886
+ this.patch({ locked: [], notice: "\u5DF2\u6E05\u9664\u624B\u52A8\u4FDD\u62A4\u7247\u6BB5" });
887
+ }
888
+ async optimize() {
889
+ if (this.settingsPending) return;
890
+ if (!this.state.settings.optimizeEnabled) {
891
+ this.patch({ notice: "\u63D0\u793A\u8BCD\u4F18\u5316\u5DF2\u5173\u95ED\uFF0C\u53EF\u5728\u8F93\u5165\u52A9\u624B\u8BBE\u7F6E\u4E2D\u542F\u7528" });
892
+ return;
893
+ }
894
+ const binding = this.binding;
895
+ if (!binding || this.state.busy) return;
896
+ const target = this.captureTarget(false);
897
+ if (!target || !target.text.trim()) return;
898
+ if (this.lockedRanges.some((lock) => target.span.start < lock.span.end && target.span.end > lock.span.start && !(target.span.start <= lock.span.start && target.span.end >= lock.span.end))) {
899
+ this.patch({ notice: "\u9009\u533A\u8DE8\u8D8A\u4E86\u53D7\u4FDD\u62A4\u7247\u6BB5\u7684\u8FB9\u754C\uFF0C\u8BF7\u5B8C\u6574\u5305\u542B\u6216\u907F\u5F00\u8BE5\u7247\u6BB5" });
900
+ return;
901
+ }
902
+ if (charCount(target.text) > this.state.settings.maxDraftChars) {
903
+ this.patch({ notice: `\u5F85\u4F18\u5316\u5185\u5BB9\u8D85\u8FC7 ${this.state.settings.maxDraftChars} \u5B57\u7B26\uFF0C\u8BF7\u9009\u62E9\u8F83\u77ED\u7247\u6BB5` });
904
+ return;
905
+ }
906
+ this.clearTimer();
907
+ this.cancelActive();
908
+ this.patch({ suggestion: null, busy: "optimize", notice: "" });
909
+ const snapshot = this.snapshot("optimize", target.span, target.text);
910
+ if (!snapshot) {
911
+ this.patch({ busy: null, notice: "\u8FDE\u63A5\u6216\u6A21\u578B\u5C1A\u672A\u5C31\u7EEA\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" });
912
+ return;
913
+ }
914
+ const active = this.begin(snapshot);
915
+ try {
916
+ const result = await this.transport.call("generate", snapshot, active.controller.signal);
917
+ if (this.active !== active || !this.isCurrent(snapshot, false) || !this.validResult(result, snapshot)) return;
918
+ if (result.text === null || !result.text.trim()) {
919
+ this.patch({ notice: "\u6CA1\u6709\u53EF\u5E94\u7528\u7684\u4F18\u5316\u7ED3\u679C\uFF0C\u539F\u6587\u5DF2\u4FDD\u7559" });
920
+ return;
921
+ }
922
+ const after = binding.input.draft.slice(0, target.clipboardStart) + result.text + binding.input.draft.slice(target.clipboardEnd);
923
+ if (!binding.actions.insertText(result.text, target.span)) {
924
+ this.patch({ notice: "\u8349\u7A3F\u5DF2\u53D8\u5316\uFF0C\u5DF2\u653E\u5F03\u672C\u6B21\u7ED3\u679C" });
925
+ return;
926
+ }
927
+ binding.actions.persistDraft?.();
928
+ this.suppressUntilEdit = after;
929
+ this.patch({ change: {
930
+ before: binding.input.draft,
931
+ after,
932
+ beforeTarget: target.text,
933
+ afterTarget: result.text,
934
+ span: { ...target.span, end: target.span.start + result.text.length },
935
+ sourceTurnId: snapshot.sourceTurnId,
936
+ connectionGeneration: snapshot.connectionGeneration,
937
+ routeKey: snapshot.routeKey,
938
+ clientGeneration: active.clientGeneration
939
+ }, notice: "\u5DF2\u4F18\u5316\uFF0C\u53EF\u67E5\u770B\u6539\u52A8\u6216\u64A4\u9500" });
940
+ } catch (error) {
941
+ if (!active.controller.signal.aborted) this.patch({ notice: this.errorMessage(error) });
942
+ } finally {
943
+ this.recordLatency(active);
944
+ if (this.active === active) {
945
+ this.active = null;
946
+ this.patch({ busy: null });
947
+ }
948
+ }
949
+ }
950
+ canRestore() {
951
+ const change = this.state.change, binding = this.binding, status = this.state.status;
952
+ return Boolean(change && binding && status && this.state.connected && binding.input.draft === change.after && binding.input.phase === "plain" && status.sourceTurnId === change.sourceTurnId && status.generation === change.connectionGeneration && this.routeKey(binding.route ?? status.route) === change.routeKey && this.transport.generation() === change.clientGeneration);
953
+ }
954
+ restoreOriginal() {
955
+ const binding = this.binding, change = this.state.change;
956
+ if (!binding || !change || !this.canRestore()) {
957
+ this.patch({ notice: "\u8349\u7A3F\u5DF2\u6709\u65B0\u7F16\u8F91\uFF0C\u53EF\u5728\u6539\u52A8\u9762\u677F\u590D\u5236\u539F\u6587" });
958
+ return false;
959
+ }
960
+ this.cancelActive();
961
+ const applied = binding.actions.insertText(change.beforeTarget, { ...change.span, draftRev: binding.input.draftRev });
962
+ if (applied) {
963
+ binding.actions.persistDraft?.();
964
+ this.suppressUntilEdit = change.before;
965
+ this.patch({ change: null, suggestion: null, notice: "\u5DF2\u8FD8\u539F\u4F18\u5316\u524D\u7684\u5185\u5BB9" });
966
+ }
967
+ return applied;
968
+ }
969
+ acceptSuggestion() {
970
+ const suggestion = this.state.suggestion, binding = this.binding;
971
+ if (!suggestion || !binding || !this.isCurrent(suggestion.snapshot, true) || !this.eligible() || (suggestion.kind === "next" ? !this.state.settings.nextEnabled : !this.state.settings.completionEnabled)) {
972
+ this.patch({ suggestion: null });
973
+ return false;
974
+ }
975
+ const applied = binding.actions.insertText(suggestion.text, suggestion.snapshot.span);
976
+ this.patch({ suggestion: null });
977
+ if (applied) {
978
+ binding.actions.persistDraft?.();
979
+ this.suppressUntilEdit = binding.input.draft + suggestion.text;
980
+ this.acceptance = { expected: this.suppressUntilEdit, edits: 0, counted: false };
981
+ this.patch({ metrics: { ...this.state.metrics, accepted: this.state.metrics.accepted + 1 } });
982
+ }
983
+ return applied;
984
+ }
985
+ /** Called only after the overlay has actually rendered a candidate. */
986
+ markExposed(requestId) {
987
+ const suggestion = this.state.suggestion;
988
+ if (!suggestion || suggestion.snapshot.requestId !== requestId) return;
989
+ const key = JSON.stringify([suggestion.snapshot.sourceTurnId, suggestion.snapshot.routeKey, suggestion.snapshot.draft, suggestion.text, suggestion.kind]);
990
+ if (this.exposed.has(key)) return;
991
+ if (this.exposed.size >= 512) this.exposed.clear();
992
+ this.exposed.add(key);
993
+ this.patch({ metrics: { ...this.state.metrics, exposures: this.state.metrics.exposures + 1 } });
994
+ }
995
+ async refreshDrafts() {
996
+ const generation = this.transport.generation();
997
+ if (generation === null) return;
998
+ try {
999
+ const drafts = await this.transport.call("drafts/list", { sessionId: this.sessionId }, this.lifecycle.signal);
1000
+ if (generation === this.transport.generation()) this.patch({ drafts });
1001
+ } catch (error) {
1002
+ if (!this.lifecycle.signal.aborted) this.patch({ notice: this.errorMessage(error) });
1003
+ }
1004
+ }
1005
+ async stash(replaceId) {
1006
+ if (this.settingsPending) return;
1007
+ if (!this.state.settings.stashEnabled) {
1008
+ this.patch({ notice: "\u8349\u7A3F\u6682\u5B58\u5DF2\u5173\u95ED\uFF0C\u5DF2\u4FDD\u5B58\u7684\u8349\u7A3F\u4ECD\u53EF\u67E5\u770B\u548C\u5220\u9664" });
1009
+ return;
1010
+ }
1011
+ const binding = this.binding;
1012
+ if (!binding || this.state.busy) return;
1013
+ if (!this.pureText() || !binding.input.draft.trim()) {
1014
+ this.patch({ notice: "\u6682\u5B58\u4EC5\u652F\u6301\u65E0\u5F15\u7528\u3001\u65E0\u9644\u4EF6\u7684\u975E\u7A7A\u7EAF\u6587\u672C\u8349\u7A3F" });
1015
+ return;
1016
+ }
1017
+ if (this.state.drafts.length >= 3 && !replaceId) {
1018
+ this.patch({ notice: "\u5DF2\u6682\u5B58\u4E09\u6761\uFF0C\u8BF7\u5220\u9664\u4E00\u6761\u6216\u9009\u62E9\u66FF\u6362" });
1019
+ return;
1020
+ }
1021
+ const original = binding.input.draft, rev = binding.input.draftRev, clientGeneration = this.transport.generation();
1022
+ this.cancelActive();
1023
+ this.clearTimer();
1024
+ this.patch({ busy: "stash", suggestion: null, notice: "" });
1025
+ try {
1026
+ await this.transport.call("drafts/save", { sessionId: this.sessionId, text: original, ...replaceId ? { replaceId } : {} }, this.lifecycle.signal);
1027
+ await this.refreshDrafts();
1028
+ const live = this.binding;
1029
+ if (!this.settingsPending && this.state.settings.stashEnabled && live?.input.draftRev === rev && live.input.draft === original && live.input.phase === "plain" && clientGeneration === this.transport.generation() && this.pureText()) {
1030
+ if (live.actions.insertText("", { start: 0, end: original.length, draftRev: rev })) {
1031
+ live.actions.persistDraft?.();
1032
+ this.suppressUntilEdit = "";
1033
+ this.patch({ notice: "\u8349\u7A3F\u5DF2\u6682\u5B58\u5E76\u6E05\u7A7A\uFF1B\u53EF\u64A4\u9500\u6E05\u7A7A" });
1034
+ return;
1035
+ }
1036
+ }
1037
+ this.patch({ notice: "\u5DF2\u4FDD\u5B58\u5FEB\u7167\uFF1B\u5F53\u524D\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u4FDD\u7559\u5F53\u524D\u8F93\u5165" });
1038
+ } catch (error) {
1039
+ this.patch({ notice: this.errorMessage(error) });
1040
+ } finally {
1041
+ this.patch({ busy: null });
1042
+ }
1043
+ }
1044
+ restoreDraft(draft, mode) {
1045
+ if (this.settingsPending) return false;
1046
+ const binding = this.binding;
1047
+ if (!this.state.settings.stashEnabled) {
1048
+ this.patch({ notice: "\u8349\u7A3F\u6682\u5B58\u5DF2\u5173\u95ED\uFF0C\u8BF7\u91CD\u65B0\u542F\u7528\u540E\u6062\u590D" });
1049
+ return false;
1050
+ }
1051
+ if (!this.state.connected || !this.state.status) {
1052
+ this.patch({ notice: "\u8FDE\u63A5\u5C1A\u672A\u5C31\u7EEA\uFF0C\u5DF2\u4FDD\u7559\u5F53\u524D\u8F93\u5165" });
1053
+ return false;
1054
+ }
1055
+ if (!binding || !this.pureText()) {
1056
+ this.patch({ notice: "\u8BF7\u5148\u79FB\u9664\u5F15\u7528\u548C\u9644\u4EF6\uFF0C\u518D\u6062\u590D\u7EAF\u6587\u672C\u8349\u7A3F" });
1057
+ return false;
1058
+ }
1059
+ this.cancelActive();
1060
+ this.clearTimer();
1061
+ const span = { start: mode === "replace" ? 0 : binding.input.draft.length, end: binding.input.draft.length, draftRev: binding.input.draftRev };
1062
+ const text = mode === "append" && binding.input.draft ? `
1063
+ ${draft.text}` : draft.text;
1064
+ const applied = binding.actions.insertText(text, span);
1065
+ if (applied) {
1066
+ binding.actions.persistDraft?.();
1067
+ this.suppressUntilEdit = mode === "replace" ? text : binding.input.draft + text;
1068
+ this.patch({ suggestion: null, notice: "\u8349\u7A3F\u5DF2\u6062\u590D\uFF0C\u5C1A\u672A\u53D1\u9001" });
1069
+ }
1070
+ return applied;
1071
+ }
1072
+ async deleteDraft(id) {
1073
+ await this.mutateDraft("drafts/delete", { sessionId: this.sessionId, id });
1074
+ }
1075
+ async clearDrafts() {
1076
+ await this.mutateDraft("drafts/clear", { sessionId: this.sessionId });
1077
+ }
1078
+ async mutateDraft(method, data) {
1079
+ try {
1080
+ await this.transport.call(method, data, this.lifecycle.signal);
1081
+ await this.refreshDrafts();
1082
+ } catch (error) {
1083
+ this.patch({ notice: this.errorMessage(error) });
1084
+ }
1085
+ }
1086
+ captureTarget(requireSelection) {
1087
+ const binding = this.binding;
1088
+ if (!binding || binding.input.phase !== "plain") {
1089
+ this.patch({ notice: "\u5F53\u524D\u8F93\u5165\u6846\u6682\u65F6\u4E0D\u53EF\u7F16\u8F91" });
1090
+ return null;
1091
+ }
1092
+ const span = binding.actions.captureInsertion();
1093
+ if (span.start !== span.end) {
1094
+ const selected = selectedPlainText(binding.editor, binding.input.draft, span, binding.input.occurrences);
1095
+ if (!selected) {
1096
+ this.patch({ notice: "\u8BF7\u9009\u62E9\u5B8C\u5168\u4F4D\u4E8E\u666E\u901A\u6587\u5B57\u4E2D\u7684\u7247\u6BB5\uFF0C\u907F\u514D\u8DE8\u8D8A\u5F15\u7528\u6807\u7B7E" });
1097
+ return null;
1098
+ }
1099
+ return { ...selected, span };
1100
+ }
1101
+ if (requireSelection) {
1102
+ this.patch({ notice: "\u8BF7\u5148\u5728\u8F93\u5165\u6846\u9009\u4E2D\u8981\u4FDD\u62A4\u7684\u6587\u5B57" });
1103
+ return null;
1104
+ }
1105
+ if (binding.input.occurrences.length) {
1106
+ this.patch({ notice: "\u542B\u5F15\u7528\u6807\u7B7E\u65F6\uFF0C\u8BF7\u9009\u62E9\u4E0D\u8DE8\u6807\u7B7E\u7684\u7EAF\u6587\u5B57\u7247\u6BB5\u4F18\u5316" });
1107
+ return null;
1108
+ }
1109
+ return { text: binding.input.draft, span: { start: 0, end: binding.input.draft.length, draftRev: binding.input.draftRev }, clipboardStart: 0, clipboardEnd: binding.input.draft.length };
1110
+ }
1111
+ pureText() {
1112
+ const input = this.binding?.input;
1113
+ return Boolean(input && input.phase === "plain" && !input.occurrences.length && !input.attachmentIds.length);
1114
+ }
1115
+ eligible() {
1116
+ return this.pureText() && !this.composing && !this.settingsPending && !this.binding?.running && !this.state.status?.running && !this.state.busy && this.state.connected && this.suppressUntilEdit === null;
1117
+ }
1118
+ driveSuggestions() {
1119
+ const binding = this.binding, status = this.state.status;
1120
+ if (!binding || !status || !this.eligible() || this.active) {
1121
+ this.clearTimer();
1122
+ return;
1123
+ }
1124
+ if (nativeMenuOpen(binding.editor)) {
1125
+ this.clearTimer();
1126
+ return;
1127
+ }
1128
+ const span = binding.actions.captureInsertion();
1129
+ if (span.start !== span.end || span.end !== binding.input.draft.length || !focusedIn(binding.editor)) {
1130
+ this.clearTimer();
1131
+ return;
1132
+ }
1133
+ const turn = status.sourceTurnId;
1134
+ const route = this.routeKey(binding.route ?? status.route);
1135
+ if (binding.input.draft === "") {
1136
+ this.clearTimer();
1137
+ if (!turn) return;
1138
+ if (!this.state.settings.nextEnabled || this.nextTurns.has(`${status.generation}:${turn}:${route}`)) return;
1139
+ if (this.nextTurns.size >= 512) this.nextTurns.clear();
1140
+ this.nextTurns.add(`${status.generation}:${turn}:${route}`);
1141
+ void this.requestSuggestion("next");
1142
+ return;
1143
+ }
1144
+ if (!this.state.settings.completionEnabled || charCount(binding.input.draft) > this.state.settings.maxDraftChars) {
1145
+ this.clearTimer();
1146
+ return;
1147
+ }
1148
+ if (this.nextCache && this.nextCache.turn === turn && this.nextCache.route === route && this.nextCache.generation === status.generation && this.nextCache.text.startsWith(binding.input.draft)) {
1149
+ this.clearTimer();
1150
+ const suffix = this.nextCache.text.slice(binding.input.draft.length);
1151
+ const snapshot = this.snapshot("complete", span);
1152
+ if (suffix && charCount(suffix) <= this.state.settings.maxCompletionChars && snapshot) this.patch({ suggestion: { text: suffix, snapshot, kind: "complete" } });
1153
+ return;
1154
+ }
1155
+ const cacheKey = this.completionKey();
1156
+ if (cacheKey === this.failedCompletionKey) {
1157
+ this.clearTimer();
1158
+ return;
1159
+ }
1160
+ if (this.completionCache.has(cacheKey)) {
1161
+ this.clearTimer();
1162
+ const text = this.completionCache.get(cacheKey), snapshot = this.snapshot("complete", span);
1163
+ if (text && snapshot) this.patch({ suggestion: { text, snapshot, kind: "complete" } });
1164
+ return;
1165
+ }
1166
+ if (this.timer !== void 0 && this.timerKey === cacheKey) return;
1167
+ this.clearTimer();
1168
+ const delay = Math.max(this.state.settings.debounceMs, this.lastCompletionStart + this.state.settings.minIntervalMs - Date.now());
1169
+ this.timerKey = cacheKey;
1170
+ this.timer = setTimeout(() => {
1171
+ this.timer = void 0;
1172
+ this.timerKey = null;
1173
+ void this.requestSuggestion("complete");
1174
+ }, delay);
1175
+ }
1176
+ async requestSuggestion(operation) {
1177
+ const binding = this.binding;
1178
+ if (!binding || !this.eligible()) return;
1179
+ const span = binding.actions.captureInsertion(), snapshot = this.snapshot(operation, span);
1180
+ if (!snapshot || span.start !== span.end || span.end !== binding.input.draft.length) return;
1181
+ if (operation === "complete") this.lastCompletionStart = Date.now();
1182
+ const active = this.begin(snapshot), cacheKey = this.completionKey();
1183
+ try {
1184
+ const result = await this.transport.call("generate", snapshot, active.controller.signal);
1185
+ if (this.active !== active || !this.isCurrent(snapshot, true)) return;
1186
+ if (!this.validResult(result, snapshot)) {
1187
+ if (operation === "complete") this.failedCompletionKey = cacheKey;
1188
+ return;
1189
+ }
1190
+ if (operation === "complete") {
1191
+ this.completionCache.set(cacheKey, result.text);
1192
+ if (this.completionCache.size > 64) this.completionCache.delete(this.completionCache.keys().next().value);
1193
+ }
1194
+ if (operation === "next" && result.text) this.nextCache = { text: result.text, turn: snapshot.sourceTurnId, route: snapshot.routeKey, generation: snapshot.connectionGeneration };
1195
+ if (result.text) this.patch({ suggestion: { text: result.text, snapshot, kind: operation } });
1196
+ } catch {
1197
+ if (operation === "complete" && !active.controller.signal.aborted && this.active === active && this.isCurrent(snapshot, false)) {
1198
+ this.failedCompletionKey = cacheKey;
1199
+ }
1200
+ } finally {
1201
+ this.recordLatency(active);
1202
+ if (this.active === active) this.active = null;
1203
+ }
1204
+ }
1205
+ snapshot(operation, span, targetText) {
1206
+ const binding = this.binding, status = this.state.status;
1207
+ if (!binding || !status || !this.state.connected) return null;
1208
+ const route = binding.route ?? status.route;
1209
+ return {
1210
+ operation,
1211
+ sessionId: this.sessionId,
1212
+ requestId: `${Date.now().toString(36)}-${++this.sequence}`,
1213
+ sourceTurnId: status.sourceTurnId,
1214
+ draft: binding.input.draft,
1215
+ draftRev: binding.input.draftRev,
1216
+ span: { ...span },
1217
+ connectionGeneration: status.generation,
1218
+ routeKey: this.routeKey(route),
1219
+ ...targetText === void 0 ? {} : { targetText },
1220
+ ...this.state.locked.length ? { lockedFragments: this.state.locked.filter((text) => (targetText ?? binding.input.draft).includes(text)) } : {},
1221
+ ...route ? { provider: route.provider, model: route.model } : {}
1222
+ };
1223
+ }
1224
+ begin(snapshot) {
1225
+ this.cancelActive();
1226
+ const active = {
1227
+ snapshot,
1228
+ controller: new AbortController(),
1229
+ clientGeneration: this.transport.generation(),
1230
+ caret: this.binding.actions.captureInsertion(),
1231
+ startedAt: Date.now(),
1232
+ latencyRecorded: false
1233
+ };
1234
+ this.active = active;
1235
+ this.patch({ metrics: { ...this.state.metrics, calls: this.state.metrics.calls + 1 } });
1236
+ return active;
1237
+ }
1238
+ observeAcceptedEdit(draft) {
1239
+ const acceptance = this.acceptance;
1240
+ if (!acceptance) return;
1241
+ if (!draft) {
1242
+ this.acceptance = null;
1243
+ return;
1244
+ }
1245
+ if (draft === acceptance.expected && !acceptance.counted) return;
1246
+ let start = 0, end = 0;
1247
+ while (start < draft.length && start < acceptance.expected.length && draft[start] === acceptance.expected[start]) start++;
1248
+ while (end < draft.length - start && end < acceptance.expected.length - start && draft[draft.length - end - 1] === acceptance.expected[acceptance.expected.length - end - 1]) end++;
1249
+ const edits = draft.length + acceptance.expected.length - 2 * start - 2 * end;
1250
+ this.patch({ metrics: {
1251
+ ...this.state.metrics,
1252
+ editedAcceptances: this.state.metrics.editedAcceptances + (acceptance.counted ? 0 : 1),
1253
+ editedCharacters: this.state.metrics.editedCharacters + edits - acceptance.edits
1254
+ } });
1255
+ acceptance.counted = true;
1256
+ acceptance.edits = edits;
1257
+ }
1258
+ isCurrent(snapshot, checkCaret) {
1259
+ const binding = this.binding, status = this.state.status;
1260
+ return Boolean(binding && status && !this.disposed && this.transport.generation() === this.clientGeneration && this.transport.generation() !== null && binding.sessionId === snapshot.sessionId && binding.input.phase === "plain" && snapshotMatches(snapshot, {
1261
+ sessionId: binding.sessionId,
1262
+ sourceTurnId: status.sourceTurnId,
1263
+ draft: binding.input.draft,
1264
+ draftRev: binding.input.draftRev,
1265
+ span: binding.actions.captureInsertion(),
1266
+ connectionGeneration: status.generation,
1267
+ routeKey: this.routeKey(binding.route ?? status.route)
1268
+ }, checkCaret));
1269
+ }
1270
+ validResult(result, request) {
1271
+ return result.requestId === request.requestId && result.sessionId === request.sessionId && result.sourceTurnId === request.sourceTurnId && result.draftRev === request.draftRev && result.connectionGeneration === request.connectionGeneration && result.routeKey === request.routeKey && result.span.start === request.span.start && result.span.end === request.span.end && (result.text === null || typeof result.text === "string");
1272
+ }
1273
+ sameCaret(span) {
1274
+ const current = this.binding?.actions.captureInsertion();
1275
+ return Boolean(current && current.start === span.start && current.end === span.end && current.draftRev === span.draftRev);
1276
+ }
1277
+ routeKey(route) {
1278
+ return route ? JSON.stringify([route.provider, route.model]) : "";
1279
+ }
1280
+ completionKey() {
1281
+ return JSON.stringify([this.state.status?.generation, this.state.status?.sourceTurnId, this.routeKey(this.binding?.route ?? this.state.status?.route), this.binding?.input.draft]);
1282
+ }
1283
+ invalidate(dropCache, cancelInFlight = true) {
1284
+ if (cancelInFlight) this.cancelActive();
1285
+ else if (this.active?.snapshot.operation !== "optimize") this.cancelActive();
1286
+ this.clearTimer();
1287
+ if (dropCache) {
1288
+ this.nextCache = null;
1289
+ this.completionCache.clear();
1290
+ this.failedCompletionKey = null;
1291
+ }
1292
+ this.patch({ suggestion: null });
1293
+ }
1294
+ clearTimer() {
1295
+ clearTimeout(this.timer);
1296
+ this.timer = void 0;
1297
+ this.timerKey = null;
1298
+ }
1299
+ cancelActive() {
1300
+ const active = this.active;
1301
+ if (!active) return;
1302
+ this.active = null;
1303
+ this.recordLatency(active);
1304
+ active.controller.abort();
1305
+ void this.transport.call("cancel", { sessionId: this.sessionId, requestId: active.snapshot.requestId }).catch(() => {
1306
+ });
1307
+ if (active.snapshot.operation === "optimize") this.patch({ busy: null });
1308
+ }
1309
+ recordLatency(active) {
1310
+ if (active.latencyRecorded) return;
1311
+ active.latencyRecorded = true;
1312
+ this.patch({ metrics: { ...this.state.metrics, totalLatencyMs: this.state.metrics.totalLatencyMs + Math.max(0, Date.now() - active.startedAt) } });
1313
+ }
1314
+ errorMessage(error) {
1315
+ return error instanceof Error ? error.message : "\u64CD\u4F5C\u5931\u8D25\uFF0C\u539F\u7A3F\u5DF2\u4FDD\u7559\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
1316
+ }
1317
+ };
1318
+
1319
+ // src/browser/runtime.ts
1320
+ var BrowserRuntime = class {
1321
+ constructor(connection, models) {
1322
+ this.connection = connection;
1323
+ this.models = models;
1324
+ this.transport = {
1325
+ generation: () => {
1326
+ const generation = connection.generation.getSnapshot();
1327
+ return generation ? String(generation.id) : null;
1328
+ },
1329
+ call: async (method, data, signal) => {
1330
+ const result = await connection.rpc.call("/api", "input-assistant/call", { args: { payload: { method, data } } }, signal);
1331
+ if (!result.ok) throw new Error(result.error.message || `\u8F93\u5165\u52A9\u624B\u8BF7\u6C42\u5931\u8D25 (${result.error.code || "unknown"})`);
1332
+ return result.value;
1333
+ }
1334
+ };
1335
+ }
1336
+ entries = /* @__PURE__ */ new Map();
1337
+ disposed = false;
1338
+ lifetime = new AbortController();
1339
+ pendingSettings = 0;
1340
+ settingsQueue = Promise.resolve();
1341
+ metrics = /* @__PURE__ */ new Map();
1342
+ transport;
1343
+ controllerFor(sessionId) {
1344
+ let entry = this.entries.get(sessionId);
1345
+ if (!entry) {
1346
+ entry = { controller: new InputAssistantController(sessionId, this.transport, this.metrics.get(sessionId), (update) => this.saveSettings(update)), refs: 0 };
1347
+ this.entries.set(sessionId, entry);
1348
+ if (this.pendingSettings) entry.controller.beginSettingsUpdate();
1349
+ }
1350
+ return entry.controller;
1351
+ }
1352
+ retain(sessionId) {
1353
+ const controller = this.controllerFor(sessionId), entry = this.entries.get(sessionId);
1354
+ entry.refs++;
1355
+ controller.start();
1356
+ return () => {
1357
+ entry.refs--;
1358
+ queueMicrotask(() => {
1359
+ if (entry.refs === 0 && this.entries.get(sessionId) === entry) {
1360
+ this.metrics.set(sessionId, { ...controller.getSnapshot().metrics });
1361
+ controller.dispose();
1362
+ this.entries.delete(sessionId);
1363
+ }
1364
+ });
1365
+ };
1366
+ }
1367
+ applySettings(settings) {
1368
+ for (const entry of this.entries.values()) entry.controller.applySettings(settings);
1369
+ }
1370
+ async saveSettings(update) {
1371
+ if (this.disposed) throw new Error("\u8F93\u5165\u52A9\u624B\u5DF2\u5378\u8F7D");
1372
+ const generation = this.transport.generation();
1373
+ if (generation === null) throw new Error("\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u8FDE\u540E\u4FDD\u5B58");
1374
+ if (this.pendingSettings++ === 0) for (const entry of this.entries.values()) entry.controller.beginSettingsUpdate();
1375
+ const task = this.settingsQueue.then(async () => {
1376
+ if (this.disposed) throw new Error("\u8F93\u5165\u52A9\u624B\u5DF2\u5378\u8F7D");
1377
+ if (generation !== this.transport.generation()) throw new Error("\u8FDE\u63A5\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u52A0\u8F7D\u8BBE\u7F6E");
1378
+ const settings = await this.transport.call("settings/set", update, this.lifetime.signal);
1379
+ if (this.disposed || generation !== this.transport.generation()) throw new Error("\u8FDE\u63A5\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u52A0\u8F7D\u8BBE\u7F6E");
1380
+ this.applySettings(settings);
1381
+ return settings;
1382
+ });
1383
+ this.settingsQueue = task.then(() => {
1384
+ }, () => {
1385
+ });
1386
+ try {
1387
+ return await task;
1388
+ } finally {
1389
+ if (--this.pendingSettings === 0) for (const entry of this.entries.values()) entry.controller.endSettingsUpdate();
1390
+ }
1391
+ }
1392
+ dispose() {
1393
+ if (this.disposed) return;
1394
+ this.disposed = true;
1395
+ this.lifetime.abort();
1396
+ for (const entry of this.entries.values()) entry.controller.dispose();
1397
+ this.entries.clear();
1398
+ this.metrics.clear();
1399
+ }
1400
+ };
1401
+
1402
+ // src/browser/styles.ts
1403
+ var STYLE_TEXT = `
1404
+ .dsh-ia {--dsh-ia-fallback-bg:#fff;--dsh-ia-fallback-fg:#0f1115;--dsh-ia-fallback-muted:#61666b;--dsh-ia-fallback-border:#d7dce2;font-family:var(--dsw-font-family,system-ui,sans-serif);color:var(--dsw-alias-label-primary,var(--dsh-ia-fallback-fg));font-size:13px;line-height:1.5;box-sizing:border-box;color-scheme:light}
1405
+ body[data-ds-dark-theme] .dsh-ia {--dsh-ia-fallback-bg:#232324;--dsh-ia-fallback-fg:#f3f4f6;--dsh-ia-fallback-muted:#adb2b8;--dsh-ia-fallback-border:#43454a;color-scheme:dark}
1406
+ .dsh-ia * {box-sizing:border-box}
1407
+ .dsh-ia button,.dsh-ia input,.dsh-ia select {font:inherit;color:inherit}
1408
+ .dsh-ia button {border:1px solid var(--dsw-alias-border-l2,var(--dsh-ia-fallback-border));border-radius:7px;padding:5px 9px;background:transparent;cursor:pointer}
1409
+ .dsh-ia button:hover:not(:disabled) {background:var(--dsw-alias-interactive-bg-hover,#8882)}
1410
+ .dsh-ia button:disabled {opacity:.45;cursor:default}
1411
+ .dsh-ia button:focus-visible,.dsh-ia input:focus-visible,.dsh-ia select:focus-visible {outline:2px solid var(--dsw-alias-state-business-primary,#4c88e4);outline-offset:2px}
1412
+ .dsh-ia-root {display:flex;align-items:center;gap:3px;position:relative}
1413
+ .dsh-ia-settings {width:100%;max-width:760px}
1414
+ .dsh-ia-settings h2 {font-size:18px;margin:0 0 8px}.dsh-ia-settings h3 {font-size:14px;margin:0 0 10px}
1415
+ .dsh-ia-settings fieldset {padding:0;margin:0;border:0;min-width:0}
1416
+ .dsh-ia-settings-toggles {display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
1417
+ .dsh-ia-settings-toggles label {display:flex;align-items:center;gap:7px}
1418
+ .dsh-ia-trigger {white-space:nowrap}
1419
+ .dsh-ia-menu {position:fixed;width:min(440px,calc(100vw - 24px));max-height:calc(100dvh - 24px);overflow:auto;overscroll-behavior:contain;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--dsh-ia-fallback-border));border-radius:12px;background:var(--dsw-alias-bg-layer-2,var(--dsh-ia-fallback-bg));box-shadow:0 8px 32px #0003;z-index:1000}
1420
+ .dsh-ia-row {display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:8px 0}
1421
+ .dsh-ia-row label {display:flex;align-items:center;gap:6px}
1422
+ .dsh-ia-field {display:flex;flex-direction:column;gap:4px;margin:8px 0}
1423
+ .dsh-ia-field input,.dsh-ia select {border:1px solid var(--dsw-alias-border-l2,var(--dsh-ia-fallback-border));border-radius:6px;background:var(--dsw-alias-bg-layer-1,var(--dsh-ia-fallback-bg));padding:6px}
1424
+ .dsh-ia-field input {width:100%}
1425
+ .dsh-ia-muted {color:var(--dsw-alias-label-secondary,var(--dsh-ia-fallback-muted));font-size:12px}
1426
+ .dsh-ia-notice {position:fixed;max-width:calc(100vw - 24px);overflow:auto;padding:8px 11px;background:var(--dsw-alias-bg-layer-2,var(--dsh-ia-fallback-bg));border:1px solid var(--dsw-alias-border-l2,var(--dsh-ia-fallback-border));border-radius:9px;box-shadow:0 4px 18px #0002;z-index:999}
1427
+ .dsh-ia-menu .dsh-ia-notice {position:static;max-width:100%;margin:8px 0;box-shadow:none}
1428
+ .dsh-ia-section {border-top:1px solid var(--dsw-alias-border-l2,var(--dsh-ia-fallback-border));margin-top:12px;padding-top:10px}
1429
+ .dsh-ia-section h4 {font-size:13px;margin:0 0 8px}
1430
+ .dsh-ia-pre {white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;font-size:12px;max-height:190px;overflow:auto;padding:9px;border:1px solid var(--dsw-alias-border-l2,var(--dsh-ia-fallback-border));border-radius:7px;background:var(--dsw-alias-bg-module-platform,#8881)}
1431
+ .dsh-ia-diff ins {background:#27a35b26;text-decoration:none}.dsh-ia-diff del {background:#d9454526}
1432
+ .dsh-ia-draft {border:1px solid var(--dsw-alias-border-l2,var(--dsh-ia-fallback-border));border-radius:8px;padding:8px;margin:7px 0}
1433
+ .dsh-ia-draft summary {cursor:pointer;overflow-wrap:anywhere}
1434
+ .dsh-ia-draft time {font-size:11px;opacity:.65}
1435
+ .dsh-ia-candidate {position:fixed;display:flex;align-items:center;gap:8px;padding:9px 12px;overflow:auto;border:1px solid var(--dsw-alias-border-l2,var(--dsh-ia-fallback-border));border-radius:10px;background:var(--dsw-alias-bg-layer-2,var(--dsh-ia-fallback-bg));box-shadow:0 4px 16px #0002;pointer-events:auto;z-index:998}
1436
+ .dsh-ia-candidate-text {flex:1;white-space:pre-wrap;overflow-wrap:anywhere}
1437
+ .dsh-ia-ghost {position:fixed;white-space:pre-wrap;overflow-wrap:anywhere;overflow:hidden;max-height:3lh;pointer-events:none;user-select:none;color:var(--dsw-alias-label-tertiary,#76808d);opacity:.9;z-index:34}
1438
+ [data-composer-card]:has([data-dsh-ia-empty-ghost]) [data-composer-placeholder] {visibility:hidden}
1439
+ `;
1440
+
1441
+ // src/browser/settings.tsx
1442
+ var import_react2 = require("react");
1443
+
1444
+ // src/core/config.ts
1445
+ var numeric = {
1446
+ debounceMs: [200, 5e3],
1447
+ minIntervalMs: [500, 3e4],
1448
+ maxHistoryChars: [1e3, 64e3],
1449
+ maxDraftChars: [100, 32e3],
1450
+ maxNextChars: [20, 300],
1451
+ maxCompletionChars: [10, 200],
1452
+ optimizeTimeoutMs: [1e3, 12e4],
1453
+ suggestionTimeoutMs: [1e3, 6e4]
1454
+ };
1455
+ var booleans = ["optimizeEnabled", "stashEnabled", "nextEnabled", "completionEnabled"];
1456
+ var strings = ["provider", "model"];
1457
+ function validate(config) {
1458
+ for (const key of booleans) {
1459
+ if (typeof config[key] !== "boolean") throw new AssistantError("CONFIG", `${key} \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
1460
+ }
1461
+ if (!["ghost", "panel"].includes(config.displayMode)) throw new AssistantError("CONFIG", "\u663E\u793A\u65B9\u5F0F\u5FC5\u987B\u662F ghost \u6216 panel");
1462
+ if (!["light", "standard", "deep"].includes(config.optimizationStrength)) throw new AssistantError("CONFIG", "\u4F18\u5316\u5F3A\u5EA6\u5FC5\u987B\u662F light\u3001standard \u6216 deep");
1463
+ for (const [key, [min, max]] of Object.entries(numeric)) {
1464
+ const value = config[key];
1465
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
1466
+ throw new AssistantError("CONFIG", `${key} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
1467
+ }
1468
+ }
1469
+ for (const key of strings) {
1470
+ if (typeof config[key] !== "string" || config[key].length > 200) throw new AssistantError("CONFIG", `${key} \u65E0\u6548`);
1471
+ config[key] = config[key].trim();
1472
+ }
1473
+ if (Boolean(config.provider) !== Boolean(config.model)) throw new AssistantError("CONFIG", "\u56FA\u5B9A\u8F85\u52A9\u6A21\u578B\u9700\u540C\u65F6\u586B\u5199\u63D0\u4F9B\u5546\u548C\u6A21\u578B\uFF1B\u7559\u7A7A\u5219\u8DDF\u968F\u8F93\u5165\u6846");
1474
+ return config;
1475
+ }
1476
+ function resolveConfig(input = {}) {
1477
+ for (const key of Object.keys(input)) if (!(key in DEFAULT_CONFIG)) throw new AssistantError("CONFIG", `\u672A\u77E5\u914D\u7F6E\uFF1A${key}`);
1478
+ return validate({ ...DEFAULT_CONFIG, ...input });
1479
+ }
1480
+
1481
+ // src/browser/settings.tsx
1482
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1483
+ var NUMBERS = [
1484
+ ["debounceMs", "\u7EED\u5199\u505C\u987F", "\u6BEB\u79D2", 200, 5e3],
1485
+ ["minIntervalMs", "\u7EED\u5199\u8BF7\u6C42\u6700\u5C0F\u95F4\u9694", "\u6BEB\u79D2", 500, 3e4],
1486
+ ["maxHistoryChars", "\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u4E0A\u9650", "\u5B57\u7B26", 1e3, 64e3],
1487
+ ["maxDraftChars", "\u5F85\u5904\u7406\u8349\u7A3F\u4E0A\u9650", "\u5B57\u7B26", 100, 32e3],
1488
+ ["maxNextChars", "\u4E0B\u4E00\u53E5\u957F\u5EA6\u4E0A\u9650", "\u5B57\u7B26", 20, 300],
1489
+ ["maxCompletionChars", "\u7EED\u5199\u957F\u5EA6\u4E0A\u9650", "\u5B57\u7B26", 10, 200],
1490
+ ["optimizeTimeoutMs", "\u4F18\u5316\u8D85\u65F6", "\u6BEB\u79D2", 1e3, 12e4],
1491
+ ["suggestionTimeoutMs", "\u81EA\u52A8\u5EFA\u8BAE\u8D85\u65F6", "\u6BEB\u79D2", 1e3, 6e4]
1492
+ ];
1493
+ function AssistantSettings({ runtime }) {
1494
+ const generation = (0, import_react2.useSyncExternalStore)(
1495
+ runtime.connection.generation.subscribe.bind(runtime.connection.generation),
1496
+ () => runtime.transport.generation(),
1497
+ () => null
1498
+ );
1499
+ const [accepted, setAccepted] = (0, import_react2.useState)(null);
1500
+ const [form, setForm] = (0, import_react2.useState)({ ...DEFAULT_CONFIG });
1501
+ const [loading, setLoading] = (0, import_react2.useState)(true);
1502
+ const [saving, setSaving] = (0, import_react2.useState)(false);
1503
+ const [message, setMessage] = (0, import_react2.useState)("");
1504
+ const [error, setError] = (0, import_react2.useState)("");
1505
+ (0, import_react2.useEffect)(() => {
1506
+ const abort = new AbortController();
1507
+ setLoading(true);
1508
+ setError("");
1509
+ setMessage("");
1510
+ if (generation === null) {
1511
+ setLoading(false);
1512
+ setError("\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u8FDE\u540E\u52A0\u8F7D\u8BBE\u7F6E");
1513
+ return () => abort.abort();
1514
+ }
1515
+ void runtime.transport.call("settings/get", {}, abort.signal).then((settings) => {
1516
+ if (abort.signal.aborted) return;
1517
+ const normalized = resolveConfig(settings);
1518
+ setAccepted(normalized);
1519
+ setForm(normalized);
1520
+ setLoading(false);
1521
+ }).catch((reason) => {
1522
+ if (!abort.signal.aborted) {
1523
+ setLoading(false);
1524
+ setError(reason instanceof Error ? reason.message : "\u8BBE\u7F6E\u52A0\u8F7D\u5931\u8D25");
1525
+ }
1526
+ });
1527
+ return () => abort.abort();
1528
+ }, [runtime, generation]);
1529
+ const change = (key, value) => {
1530
+ setForm((previous) => ({ ...previous, [key]: value }));
1531
+ setMessage("");
1532
+ setError("");
1533
+ };
1534
+ const save = async (event) => {
1535
+ event.preventDefault();
1536
+ if (!accepted || saving || generation === null) return;
1537
+ setError("");
1538
+ setMessage("");
1539
+ let draft;
1540
+ try {
1541
+ draft = resolveConfig(form);
1542
+ } catch (reason) {
1543
+ setError(reason instanceof Error ? reason.message : "\u8BBE\u7F6E\u65E0\u6548");
1544
+ return;
1545
+ }
1546
+ setSaving(true);
1547
+ try {
1548
+ const patch = Object.fromEntries(Object.entries(draft).filter(([key, value]) => value !== accepted[key]));
1549
+ const settings = await runtime.saveSettings(patch);
1550
+ if (runtime.transport.generation() !== generation) return;
1551
+ setAccepted(settings);
1552
+ setForm(settings);
1553
+ setMessage("\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u5E76\u751F\u6548");
1554
+ } catch (reason) {
1555
+ setError(`\u4FDD\u5B58\u672A\u786E\u8BA4\uFF1A${reason instanceof Error ? reason.message : "\u8BF7\u91CD\u8BD5"}\uFF1B\u8BF7\u91CD\u65B0\u6253\u5F00\u8BBE\u7F6E\u6838\u5BF9`);
1556
+ } finally {
1557
+ setSaving(false);
1558
+ }
1559
+ };
1560
+ const changed = accepted !== null && JSON.stringify(form) !== JSON.stringify(accepted);
1561
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { className: "dsh-ia dsh-ia-settings", "aria-label": "\u8F93\u5165\u52A9\u624B\u8BBE\u7F6E", onSubmit: (event) => {
1562
+ void save(event);
1563
+ }, children: [
1564
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: "\u8F93\u5165\u52A9\u624B" }),
1565
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-ia-muted", children: "\u5E2E\u52A9\u4F60\u5F62\u6210\u51C6\u786E\u3001\u53EF\u53D1\u9001\u7684\u8F93\u5165\u3002\u5404\u529F\u80FD\u72EC\u7ACB\u5F00\u5173\uFF0C\u91C7\u7EB3\u5185\u5BB9\u540E\u4ECD\u7531\u4F60\u53D1\u9001\u3002" }),
1566
+ loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", children: "\u6B63\u5728\u52A0\u8F7D\u8BBE\u7F6E\u2026" }),
1567
+ message && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "status", children: message }),
1568
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "alert", children: error }),
1569
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("fieldset", { disabled: loading || saving || generation === null || !accepted, children: [
1570
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: "dsh-ia-section", children: [
1571
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { children: "\u529F\u80FD\u5F00\u5173" }),
1572
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-ia-settings-toggles", children: [
1573
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
1574
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "checkbox", checked: form.optimizeEnabled, onChange: (event) => change("optimizeEnabled", event.target.checked) }),
1575
+ "\u63D0\u793A\u8BCD\u4F18\u5316"
1576
+ ] }),
1577
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
1578
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "checkbox", checked: form.nextEnabled, onChange: (event) => change("nextEnabled", event.target.checked) }),
1579
+ "\u56DE\u590D\u540E\u9884\u6D4B"
1580
+ ] }),
1581
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
1582
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "checkbox", checked: form.completionEnabled, onChange: (event) => change("completionEnabled", event.target.checked) }),
1583
+ "\u8F93\u5165\u7EED\u5199"
1584
+ ] }),
1585
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
1586
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { type: "checkbox", checked: form.stashEnabled, onChange: (event) => change("stashEnabled", event.target.checked) }),
1587
+ "\u8349\u7A3F\u6682\u5B58"
1588
+ ] })
1589
+ ] }),
1590
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-ia-muted", children: "\u5173\u95ED\u6682\u5B58\u540E\u4ECD\u53EF\u67E5\u770B\u548C\u5220\u9664\u5386\u53F2\u8349\u7A3F\uFF1B\u5DF2\u6709\u4F18\u5316\u6539\u52A8\u4ECD\u53EF\u67E5\u770B\u548C\u8FD8\u539F\u3002" })
1591
+ ] }),
1592
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: "dsh-ia-section", children: [
1593
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { children: "\u4F18\u5316\u4E0E\u5EFA\u8BAE" }),
1594
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "dsh-ia-field", children: [
1595
+ "\u4F18\u5316\u5F3A\u5EA6",
1596
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("select", { value: form.optimizationStrength, onChange: (event) => change("optimizationStrength", event.target.value), children: [
1597
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "light", children: "\u8F7B\u5EA6\u6DA6\u8272" }),
1598
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "standard", children: "\u6807\u51C6\u4F18\u5316" }),
1599
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "deep", children: "\u6DF1\u5EA6\u68B3\u7406" })
1600
+ ] })
1601
+ ] }),
1602
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-ia-muted", children: "\u8F7B\u5EA6\u6DA6\u8272\u53EA\u4FEE\u6B63\u8868\u8FBE\uFF1B\u6807\u51C6\u4F18\u5316\u6574\u7406\u76EE\u6807\u4E0E\u7EA6\u675F\uFF1B\u6DF1\u5EA6\u68B3\u7406\u53EF\u91CD\u7EC4\u7ED3\u6784\u3002\u5404\u6863\u4F4D\u5747\u8981\u6C42\u4FDD\u7559\u539F\u610F\uFF0C\u4E0D\u65B0\u589E\u4EFB\u52A1\u6216\u6388\u6743\uFF1B\u5E94\u7528\u540E\u53EF\u67E5\u770B\u6539\u52A8\u3002" }),
1603
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "dsh-ia-field", children: [
1604
+ "\u5EFA\u8BAE\u663E\u793A\u65B9\u5F0F",
1605
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("select", { value: form.displayMode, onChange: (event) => change("displayMode", event.target.value), children: [
1606
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "ghost", children: "\u7070\u5B57" }),
1607
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "panel", children: "\u5019\u9009\u9762\u677F" })
1608
+ ] })
1609
+ ] })
1610
+ ] }),
1611
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: "dsh-ia-section", children: [
1612
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { children: "\u8F85\u52A9\u6A21\u578B" }),
1613
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-ia-muted", children: "\u4E24\u9879\u540C\u65F6\u7559\u7A7A\u65F6\u8DDF\u968F\u8F93\u5165\u6846\u6A21\u578B\u3002\u8F85\u52A9\u8C03\u7528\u5355\u72EC\u6D88\u8017\u6A21\u578B\u7528\u91CF\u3002" }),
1614
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "dsh-ia-field", children: [
1615
+ "\u63D0\u4F9B\u5546",
1616
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { value: form.provider, onChange: (event) => change("provider", event.target.value), placeholder: "\u8DDF\u968F\u8F93\u5165\u6846", maxLength: 200, autoComplete: "off" })
1617
+ ] }),
1618
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "dsh-ia-field", children: [
1619
+ "\u6A21\u578B",
1620
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("input", { value: form.model, onChange: (event) => change("model", event.target.value), placeholder: "\u8DDF\u968F\u8F93\u5165\u6846", maxLength: 200, autoComplete: "off" })
1621
+ ] })
1622
+ ] }),
1623
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("details", { className: "dsh-ia-section", children: [
1624
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("summary", { children: "\u9AD8\u7EA7\u8BBE\u7F6E" }),
1625
+ NUMBERS.map(([key, label, unit, min, max]) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "dsh-ia-field", children: [
1626
+ label,
1627
+ "\uFF08",
1628
+ unit,
1629
+ "\uFF09",
1630
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1631
+ "input",
1632
+ {
1633
+ type: "number",
1634
+ "aria-label": `${label}\uFF08${unit}\uFF09`,
1635
+ value: Number.isNaN(form[key]) ? "" : form[key],
1636
+ min,
1637
+ max,
1638
+ step: 1,
1639
+ required: true,
1640
+ onChange: (event) => change(key, event.target.value === "" ? NaN : Number(event.target.value))
1641
+ }
1642
+ ),
1643
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dsh-ia-muted", children: [
1644
+ min.toLocaleString(),
1645
+ "\u2013",
1646
+ max.toLocaleString(),
1647
+ " ",
1648
+ unit
1649
+ ] })
1650
+ ] }, key))
1651
+ ] }),
1652
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-ia-row", children: [
1653
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "submit", disabled: !changed, children: saving ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58\u8BBE\u7F6E" }),
1654
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", disabled: !changed, onClick: () => {
1655
+ if (accepted) setForm(accepted);
1656
+ setError("");
1657
+ setMessage("\u5DF2\u653E\u5F03\u672A\u4FDD\u5B58\u7684\u4FEE\u6539");
1658
+ }, children: "\u653E\u5F03\u4FEE\u6539" }),
1659
+ changed && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-ia-muted", children: "\u6709\u672A\u4FDD\u5B58\u7684\u4FEE\u6539" })
1660
+ ] })
1661
+ ] })
1662
+ ] });
1663
+ }
1664
+
1665
+ // src/browser/index.ts
1666
+ var name = "input-assistant.client";
1667
+ var inject = ["slots", "connection", "modelDirectories"];
1668
+ function apply(ctx) {
1669
+ const connection = ctx.get("connection");
1670
+ const models = ctx.get("modelDirectories");
1671
+ const runtime = new BrowserRuntime(connection, models);
1672
+ ctx.effect(() => {
1673
+ const style = document.createElement("style");
1674
+ style.dataset.dshInputAssistant = "";
1675
+ style.textContent = STYLE_TEXT;
1676
+ document.head.append(style);
1677
+ return () => {
1678
+ runtime.dispose();
1679
+ style.remove();
1680
+ };
1681
+ }, "input-assistant: browser lifetime");
1682
+ ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
1683
+ name: "conversation.input.right",
1684
+ id: "input-assistant",
1685
+ order: 40,
1686
+ inject: () => ({ runtime })
1687
+ }, AssistantRight));
1688
+ ctx.slots.inject("conversation.input.overlay", () => ctx.slots.register({
1689
+ name: "conversation.input.overlay",
1690
+ id: "input-assistant",
1691
+ order: 40,
1692
+ inject: () => ({ runtime })
1693
+ }, AssistantOverlay));
1694
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1695
+ name: "settings.section",
1696
+ id: "input-assistant",
1697
+ order: 65,
1698
+ label: "\u8F93\u5165\u52A9\u624B",
1699
+ inject: () => ({ runtime })
1700
+ }, AssistantSettings));
1701
+ }
1702
+
1703
+ return module.exports;
1704
+ }});