dsh-prompt-enhance 0.1.11 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/README.zh-CN.md +5 -0
- package/lib/client.js +162 -20
- package/lib/index.js +279 -26
- package/lib/index.js.map +3 -3
- package/package.json +1 -3
- package/src/client/EnhanceButton.tsx +20 -11
- package/src/client/ResultPanel.tsx +17 -3
- package/src/client/enhance-client.ts +110 -1
- package/src/client/locales.ts +2 -0
- package/src/client/settings.ts +4 -0
- package/src/client/styles.ts +10 -0
- package/src/client/ui-state.ts +63 -0
- package/src/config.ts +29 -0
- package/src/context.ts +164 -0
- package/src/enhance-routes.ts +77 -13
- package/src/enhancer.ts +19 -1
- package/src/orchestrate.ts +55 -4
- package/src/prompts.ts +40 -2
- package/src/shared/protocol.ts +18 -0
- package/src/shared/stream-text.ts +115 -0
- package/src/shared/validate.ts +54 -14
package/README.md
CHANGED
|
@@ -123,6 +123,10 @@ Everything lives in the `prompt-enhance` settings namespace, edited from the web
|
|
|
123
123
|
| `shortcut` | `ctrl+alt+e` | Global shortcut spec (at least one modifier + one alphanumeric/function key — bare keys are ignored so normal typing can never be swallowed); empty disables it |
|
|
124
124
|
| `maxConcurrent` | `2` | Concurrency cap **within a single host process**; extra requests answer `429` (`concurrency-limit`). The browser UI admits exactly one in-flight request (a single preview panel), so this cap mainly protects the `/enhance` command plane and multi-client callers |
|
|
125
125
|
| `rateLimitPerMinute` | `10` | Sliding-window rate cap per minute, **within a single host process**, counting **successful calls only** — failures (timeout / upstream error / cancellation) never consume the window, so a run of failures cannot rate-limit you out; extra requests answer `429` (`rate-limit`) with a `Retry-After` in seconds |
|
|
126
|
+
| `streaming` | `true` | Show the model's output as it streams instead of waiting for the whole rewrite — **display-only**, the final text is still the normalized full result, and the client transparently falls back to the one-shot JSON route when the host or network does not support `text/event-stream` |
|
|
127
|
+
| `contextAware` | `true` | Read the current conversation's recent history and use it to ground the rewrite (resolve pronouns, fill in stated constraints, mirror the established stack and terminology). With no session, no history, or this switch off, the call degrades to the original single-prompt enhancement — context is an optimization, never a prerequisite. **Privacy:** the assembled history snippet is sent to the configured LLM alongside the draft — if the conversation contains secrets, tokens, or other sensitive content, switch `contextAware` off (or scope the conversation before enhancing). |
|
|
128
|
+
| `contextMaxMessages` | `8` | Context window breadth — how many recent `user`/`assistant` turns may ground the rewrite; `0` admits none |
|
|
129
|
+
| `contextMaxChars` | `4000` | Context window depth — character budget of the assembled history snippet, spent newest-first so the most recent turns win when the budget is tight; `0` admits none |
|
|
126
130
|
| `provider` + `model` values | — | Match the harness settings: each key under `llm-pi-ai.providers` (e.g. `zhipu`, `muyuu`) is a provider and each `models[].id` under it (e.g. `glm-5.3-flash`) is a model. Example pair: `provider: zhipu` + `model: glm-5.3-flash` |
|
|
127
131
|
|
|
128
132
|
**Model routing precedence:** explicit settings pair → the route recorded in the current session's request header → the harness-wide default model (`agent-default-model`). If none of them names a route (e.g. a fresh session with no default model), the plugin fails with an actionable message instead of guessing.
|
|
@@ -207,6 +211,7 @@ The enhance route is served by your own dsh host and reachable **only from this
|
|
|
207
211
|
- **Request-level timeouts are the host's and Node's job** — the plugin bounds the body size (including a Content-Length fast reject) and the per-call `timeoutMs`; connection-level timeouts (headers / request timeout / keep-alive) are server-level settings on the shared `http.Server` that a prefix route must not touch, so they fall to the host and Node's defaults (60 s headers / 300 s request).
|
|
208
212
|
- **Not for reverse-proxy exposure** — if you put dsh web behind a proxy that listens on the LAN, external callers appear as loopback to the route and the fence is moot. Do not expose a proxied host without adding your own authentication at the proxy.
|
|
209
213
|
- **Prompt-injection boundary** — the draft is framed between `<raw_prompt>` tags, literal closing tags inside the draft are neutralized, and the strategy prompt treats the framed text as pure data. This lowers the risk of simple tag-escape; prompt-based boundaries are best-effort, not a guarantee. Enhancements run with your own credentials and the result is only ever shown back to you.
|
|
214
|
+
- **Conversation-context data sharing** — when `contextAware` is on (default), the plugin assembles the recent `user`/`assistant` history of the current session (capped by `contextMaxMessages` / `contextMaxChars`) and sends it to the configured LLM alongside the draft. That history may carry secrets the user typed earlier in the session — API keys, tokens, internal hostnames, customer names — which the LLM provider will see verbatim. Treat the switch as the privacy boundary: turn it off (or end the session and start a fresh one before enhancing) when the conversation should not leave the machine. The hard rules on context use in the strategy prompt forbid introducing facts the raw prompt does not support, but they cannot un-share what has already been sent.
|
|
210
215
|
|
|
211
216
|
## Architecture
|
|
212
217
|
|
package/README.zh-CN.md
CHANGED
|
@@ -123,6 +123,10 @@ dsh plugin --profile web remove dsh-prompt-enhance
|
|
|
123
123
|
| `shortcut` | `ctrl+alt+e` | 快捷键规格(至少一个修饰键 + 单个字母/数字/功能键——纯裸键会被忽略,绝不会吞掉正常打字);留空禁用 |
|
|
124
124
|
| `maxConcurrent` | `2` | **单个 Host 进程内**的并发上限;超出的请求返回 `429`(`concurrency-limit`)。浏览器 UI 因单预览面板天然只允许 1 个在途请求,此上限主要保护 `/enhance` 命令入口与多客户端调用 |
|
|
125
125
|
| `rateLimitPerMinute` | `10` | **单个 Host 进程内**的每分钟滑动窗口限流,只统计**成功完成**的增强——失败(超时 / 上游错误 / 取消)不消耗窗口,连续失败不会把自己限流;超出的请求返回 `429`(`rate-limit`),响应附 `Retry-After` 秒数 |
|
|
126
|
+
| `streaming` | `true` | 模型边写边在面板展示,而非等整段写完——**仅影响显示节奏**,最终文本仍是归一化的完整结果;宿主或网络不支持 `text/event-stream` 时客户端会自动回退为一次性 JSON 请求 |
|
|
127
|
+
| `contextAware` | `true` | 读取当前对话的近期历史,用于消解代词、补全已声明的约束、镜像已确立的栈与术语。无会话、无历史、或关闭此开关时,自动回退为单条增强——上下文是优化、不是前置条件。**隐私:** 装配好的历史片段会随草稿一起发给当前配置的 LLM;若当前会话含密钥、token、内部代号等敏感内容,请关闭 `contextAware`(或先结束该会话再开新会话增强) |
|
|
128
|
+
| `contextMaxMessages` | `8` | 上下文窗口广度——最多参考多少条 user/assistant 轮;`0` 表示不参考 |
|
|
129
|
+
| `contextMaxChars` | `4000` | 上下文窗口深度——历史片段的字符预算,从最新一条起向前装配;`0` 表示不参考 |
|
|
126
130
|
| `provider` + `model` 取值 | — | 与 harness 设置一致:设置文件 `llm-pi-ai.providers` 下的键就是 provider(如 `zhipu`、`muyuu`),其 `models[].id` 就是 model(如 `glm-5.3-flash`)。示例:`provider: zhipu` + `model: glm-5.3-flash` |
|
|
127
131
|
|
|
128
132
|
**模型路由优先级:** 设置成对覆盖 → 当前会话请求头中记录的路由 → 全局默认模型(`agent-default-model`)。三者都指不出路由时(例如全新会话且无默认模型),插件给出可操作的报错而不是乱猜。
|
|
@@ -207,6 +211,7 @@ dsh plugin --profile web remove dsh-prompt-enhance
|
|
|
207
211
|
- **请求级超时由宿主与 Node 兜底** —— 插件层只限制 body 字节数(含 Content-Length 快速拒绝)与单次模型调用的 `timeoutMs`;连接级超时(headers timeout / request timeout / keep-alive)是共享 `http.Server` 的 server 级配置,插件不覆盖,由宿主与 Node 默认值(headers 60 秒 / request 300 秒)兜底。
|
|
208
212
|
- **不要经反向代理暴露** —— 若把 dsh web 放在监听局域网的代理后面,外部调用者在路由看来就是回环地址,栅栏形同虚设。除非在代理层自行加鉴权,否则不要暴露。
|
|
209
213
|
- **提示注入边界** —— 草稿被框在 `<raw_prompt>` 标签之间,草稿内的字面闭合标签会被中和,策略提示词把框内文本视为纯数据——降低简单标签逃逸的风险;基于提示词的边界是尽力而为,并非完整防护。增强只使用你自己的凭据,结果也只回显给你本人。
|
|
214
|
+
- **会话上下文数据外发** —— `contextAware` 默认开启时,本插件会装配当前会话近期的 `user`/`assistant` 历史(条数与字符数受 `contextMaxMessages` / `contextMaxChars` 限制),随草稿一起发给当前配置的 LLM。该历史可能携带你之前在会话里输入过的密钥、token、内网主机名、客户名称等,模型提供方会**原文看到**。把 `contextAware` 视为隐私边界:不该外发的会话先关闭该开关,或结束会话、开新会话再增强。策略提示词里关于上下文的硬性约束可以禁止模型「基于上下文凭空加东西」,但**无法撤回已经发出去的内容**。
|
|
210
215
|
|
|
211
216
|
## 架构
|
|
212
217
|
|
package/lib/client.js
CHANGED
|
@@ -38,24 +38,33 @@ var import_react3 = require("react");
|
|
|
38
38
|
var import_react_dom = require("react-dom");
|
|
39
39
|
|
|
40
40
|
// src/shared/validate.ts
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
function isInvisibleCodePoint(cp) {
|
|
42
|
+
return cp >= 8203 && cp <= 8205 || cp === 65279 || cp >= 8234 && cp <= 8238 || cp >= 8294 && cp <= 8297;
|
|
43
|
+
}
|
|
44
|
+
function scanText(text) {
|
|
45
|
+
let count = 0;
|
|
46
|
+
let visibleNonBlank = false;
|
|
47
|
+
for (const ch of text) {
|
|
48
|
+
count++;
|
|
49
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
50
|
+
if (!isInvisibleCodePoint(code) && !/\s/.test(ch)) visibleNonBlank = true;
|
|
51
|
+
}
|
|
52
|
+
return { count, visibleNonBlank };
|
|
44
53
|
}
|
|
45
54
|
function checkInputText(text, maxChars) {
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
55
|
+
const { count, visibleNonBlank } = scanText(text);
|
|
56
|
+
if (!visibleNonBlank) {
|
|
48
57
|
return { ok: false, code: "empty" };
|
|
49
58
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return { ok: false, code: "too-long", count: codePoints, max: maxChars };
|
|
59
|
+
if (count > maxChars) {
|
|
60
|
+
return { ok: false, code: "too-long", count, max: maxChars };
|
|
53
61
|
}
|
|
54
62
|
return { ok: true };
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
// src/shared/protocol.ts
|
|
58
66
|
var ENHANCE_ENDPOINT = "/prompt-enhance/enhance";
|
|
67
|
+
var ENHANCE_STREAM_ENDPOINT = "/prompt-enhance/enhance-stream";
|
|
59
68
|
|
|
60
69
|
// src/client/enhance-client.ts
|
|
61
70
|
var EnhanceClientError = class extends Error {
|
|
@@ -106,6 +115,83 @@ async function readEnvelope(response) {
|
|
|
106
115
|
}
|
|
107
116
|
throw new EnhanceClientError({ code: "internal", message: `\u5BBF\u4E3B\u670D\u52A1\u8FD4\u56DE\u5F02\u5E38\uFF08HTTP ${response.status}\uFF09\u3002` });
|
|
108
117
|
}
|
|
118
|
+
function takeFrames(buffer) {
|
|
119
|
+
const frames = [];
|
|
120
|
+
let rest = buffer;
|
|
121
|
+
for (let index = rest.indexOf("\n\n"); index !== -1; index = rest.indexOf("\n\n")) {
|
|
122
|
+
const raw = rest.slice(0, index);
|
|
123
|
+
rest = rest.slice(index + 2);
|
|
124
|
+
let event = "message";
|
|
125
|
+
const data = [];
|
|
126
|
+
for (const line of raw.split("\n")) {
|
|
127
|
+
if (line.startsWith(":")) continue;
|
|
128
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
129
|
+
else if (line.startsWith("data:")) data.push(line.slice(5).trim());
|
|
130
|
+
}
|
|
131
|
+
if (data.length > 0) frames.push({ event, data: data.join("\n") });
|
|
132
|
+
}
|
|
133
|
+
return { frames, rest };
|
|
134
|
+
}
|
|
135
|
+
function parseFrame(frame) {
|
|
136
|
+
if (frame.event !== "delta" && frame.event !== "done" && frame.event !== "error") return void 0;
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(frame.data);
|
|
140
|
+
} catch {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
const record = parsed;
|
|
144
|
+
if (record === null || typeof record !== "object" || record.type !== frame.event) return void 0;
|
|
145
|
+
return record;
|
|
146
|
+
}
|
|
147
|
+
async function requestEnhanceStream(body, options) {
|
|
148
|
+
let response;
|
|
149
|
+
try {
|
|
150
|
+
response = await fetch(ENHANCE_STREAM_ENDPOINT, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "content-type": "application/json", accept: "text/event-stream" },
|
|
153
|
+
body: JSON.stringify(body),
|
|
154
|
+
signal: options.signal
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (options.signal?.aborted) {
|
|
158
|
+
throw new EnhanceClientError({ code: "internal", message: "\u5DF2\u53D6\u6D88\u589E\u5F3A\uFF1B\u539F\u8F93\u5165\u672A\u6539\u52A8\u3002" });
|
|
159
|
+
}
|
|
160
|
+
void error;
|
|
161
|
+
throw new EnhanceClientError({ code: "internal", message: "\u65E0\u6CD5\u8FDE\u63A5\u5BBF\u4E3B\u670D\u52A1\uFF0C\u8BF7\u786E\u8BA4 dsh web \u6B63\u5728\u8FD0\u884C\u540E\u91CD\u8BD5\u3002" });
|
|
162
|
+
}
|
|
163
|
+
const contentType = String(response.headers.get("content-type") ?? "").toLowerCase();
|
|
164
|
+
const stream = response.body;
|
|
165
|
+
const streamable = response.ok && stream !== null && stream !== void 0 && contentType.includes("event-stream");
|
|
166
|
+
if (stream === null || stream === void 0 || !streamable) {
|
|
167
|
+
void stream?.cancel?.().catch(() => {
|
|
168
|
+
});
|
|
169
|
+
return requestEnhance(body, options.signal);
|
|
170
|
+
}
|
|
171
|
+
const reader = stream.getReader();
|
|
172
|
+
const decoder = new TextDecoder();
|
|
173
|
+
let buffer = "";
|
|
174
|
+
try {
|
|
175
|
+
while (true) {
|
|
176
|
+
const { done, value } = await reader.read();
|
|
177
|
+
if (done) break;
|
|
178
|
+
buffer += decoder.decode(value, { stream: true });
|
|
179
|
+
const { frames, rest } = takeFrames(buffer);
|
|
180
|
+
buffer = rest;
|
|
181
|
+
for (const frame of frames) {
|
|
182
|
+
const event = parseFrame(frame);
|
|
183
|
+
if (event === void 0) continue;
|
|
184
|
+
if (event.type === "delta") options.onDelta(event.text);
|
|
185
|
+
else if (event.type === "done") return parseResult(event.value);
|
|
186
|
+
else throw new EnhanceClientError(parseError(event.error));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
void reader.cancel().catch(() => {
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
throw new EnhanceClientError({ code: "internal", message: "\u5BBF\u4E3B\u670D\u52A1\u63D0\u524D\u5173\u95ED\u4E86\u589E\u5F3A\u6D41\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u539F\u8F93\u5165\u672A\u6539\u52A8\u3002" });
|
|
194
|
+
}
|
|
109
195
|
async function requestEnhance(body, signal) {
|
|
110
196
|
let response;
|
|
111
197
|
try {
|
|
@@ -134,6 +220,7 @@ var zh = {
|
|
|
134
220
|
"button.busy": "\u6B63\u5728\u589E\u5F3A\u2026",
|
|
135
221
|
"panel.title": "\u63D0\u793A\u8BCD\u589E\u5F3A",
|
|
136
222
|
"panel.loading": "\u6B63\u5728\u589E\u5F3A\uFF0C\u7A0D\u5019\u2026",
|
|
223
|
+
"panel.streaming": "\u6B63\u5728\u751F\u6210\uFF08\u8FB9\u5199\u8FB9\u663E\u793A\uFF09\u2026",
|
|
137
224
|
"panel.loading.hint": "\u539F\u6587\u4FDD\u7559\u5728\u8F93\u5165\u6846\u4E2D\uFF0C\u4E0D\u4F1A\u88AB\u52A8\u4FEE\u6539\u3002",
|
|
138
225
|
"panel.cancel": "\u53D6\u6D88",
|
|
139
226
|
"panel.original": "\u539F\u59CB\u63D0\u793A\u8BCD",
|
|
@@ -177,6 +264,7 @@ var en = {
|
|
|
177
264
|
"button.busy": "Enhancing\u2026",
|
|
178
265
|
"panel.title": "Prompt Enhance",
|
|
179
266
|
"panel.loading": "Enhancing, please wait\u2026",
|
|
267
|
+
"panel.streaming": "Generating (shown as it is written)\u2026",
|
|
180
268
|
"panel.loading.hint": "Your draft stays untouched until you apply the result.",
|
|
181
269
|
"panel.cancel": "Cancel",
|
|
182
270
|
"panel.original": "Original prompt",
|
|
@@ -332,11 +420,21 @@ function ResultPanel(props) {
|
|
|
332
420
|
] }),
|
|
333
421
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dsh-pe-close", "aria-label": t("panel.close"), onClick: onCancel, children: "\u2715" })
|
|
334
422
|
] }),
|
|
335
|
-
state.phase === "loading" && /* @__PURE__ */ (0, import_jsx_runtime.
|
|
423
|
+
state.phase === "loading" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pe-loading", children: state.streaming === void 0 || state.streaming === "" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
336
424
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pe-spin", "aria-hidden": true }),
|
|
337
425
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: t("panel.loading") }),
|
|
338
426
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pe-hint", children: t("panel.loading.hint") })
|
|
339
|
-
] })
|
|
427
|
+
] }) : (
|
|
428
|
+
// Incremental view: the text already generated, replaced by the
|
|
429
|
+
// normalized full body the moment the call settles.
|
|
430
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-pe-stream", children: [
|
|
431
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-pe-col-title", children: [
|
|
432
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-pe-spin small", "aria-hidden": true }),
|
|
433
|
+
t("panel.streaming")
|
|
434
|
+
] }),
|
|
435
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "dsh-pe-col-text", children: state.streaming })
|
|
436
|
+
] })
|
|
437
|
+
) }),
|
|
340
438
|
state.phase === "error" && state.error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pe-error", children: state.error.localized !== void 0 ? (
|
|
341
439
|
// Client-side guard failures carry their own pre-localized copy.
|
|
342
440
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: state.error.localized })
|
|
@@ -420,6 +518,25 @@ function notify() {
|
|
|
420
518
|
version++;
|
|
421
519
|
for (const listener of listeners) listener();
|
|
422
520
|
}
|
|
521
|
+
var pendingDeltaText = "";
|
|
522
|
+
var pendingDeltaSession;
|
|
523
|
+
var pendingDeltaScheduled = false;
|
|
524
|
+
function flushPendingDelta() {
|
|
525
|
+
pendingDeltaScheduled = false;
|
|
526
|
+
if (pendingDeltaText === "" || pendingDeltaSession === void 0) return;
|
|
527
|
+
const sessionId = pendingDeltaSession;
|
|
528
|
+
const text = pendingDeltaText;
|
|
529
|
+
pendingDeltaText = "";
|
|
530
|
+
pendingDeltaSession = void 0;
|
|
531
|
+
if (panelState === void 0 || panelState.sessionId !== sessionId || panelState.phase !== "loading") return;
|
|
532
|
+
panelState = { ...panelState, streaming: (panelState.streaming ?? "") + text };
|
|
533
|
+
notify();
|
|
534
|
+
}
|
|
535
|
+
function scheduleDeltaFlush() {
|
|
536
|
+
if (pendingDeltaScheduled) return;
|
|
537
|
+
pendingDeltaScheduled = true;
|
|
538
|
+
queueMicrotask(flushPendingDelta);
|
|
539
|
+
}
|
|
423
540
|
var undoStore = createUndoStack(3);
|
|
424
541
|
function pushUndo(sessionId, entry) {
|
|
425
542
|
undoStore.push(sessionId, entry);
|
|
@@ -462,6 +579,13 @@ function settleError(sessionId, error) {
|
|
|
462
579
|
panelState = { sessionId, phase: "error", original: panelState.original, error };
|
|
463
580
|
notify();
|
|
464
581
|
}
|
|
582
|
+
function appendDelta(sessionId, text) {
|
|
583
|
+
if (text === "") return;
|
|
584
|
+
if (panelState === void 0 || panelState.sessionId !== sessionId || panelState.phase !== "loading") return;
|
|
585
|
+
pendingDeltaText += text;
|
|
586
|
+
pendingDeltaSession = sessionId;
|
|
587
|
+
scheduleDeltaFlush();
|
|
588
|
+
}
|
|
465
589
|
function openError(sessionId, original, error) {
|
|
466
590
|
panelState?.abort?.();
|
|
467
591
|
panelState = { sessionId, phase: "error", original, error };
|
|
@@ -515,7 +639,8 @@ function shortcutTarget() {
|
|
|
515
639
|
var DEFAULT_CLIENT_SETTINGS = {
|
|
516
640
|
enabled: true,
|
|
517
641
|
maxInputChars: 12e3,
|
|
518
|
-
shortcut: "ctrl+alt+e"
|
|
642
|
+
shortcut: "ctrl+alt+e",
|
|
643
|
+
streaming: true
|
|
519
644
|
};
|
|
520
645
|
var listeners2 = /* @__PURE__ */ new Set();
|
|
521
646
|
var current = DEFAULT_CLIENT_SETTINGS;
|
|
@@ -538,7 +663,8 @@ function decodeClientSettings(section) {
|
|
|
538
663
|
return {
|
|
539
664
|
enabled: typeof record.enabled === "boolean" ? record.enabled : DEFAULT_CLIENT_SETTINGS.enabled,
|
|
540
665
|
maxInputChars: typeof record.maxInputChars === "number" && Number.isFinite(record.maxInputChars) && record.maxInputChars > 0 ? Math.floor(record.maxInputChars) : DEFAULT_CLIENT_SETTINGS.maxInputChars,
|
|
541
|
-
shortcut: typeof record.shortcut === "string" ? record.shortcut : DEFAULT_CLIENT_SETTINGS.shortcut
|
|
666
|
+
shortcut: typeof record.shortcut === "string" ? record.shortcut : DEFAULT_CLIENT_SETTINGS.shortcut,
|
|
667
|
+
streaming: typeof record.streaming === "boolean" ? record.streaming : DEFAULT_CLIENT_SETTINGS.streaming
|
|
542
668
|
};
|
|
543
669
|
}
|
|
544
670
|
|
|
@@ -605,14 +731,20 @@ function EnhanceButton(props) {
|
|
|
605
731
|
}
|
|
606
732
|
const controller = new AbortController();
|
|
607
733
|
openLoading({ sessionId: uiKey, original: draft, abort: () => controller.abort() });
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
(error)
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
734
|
+
const settle = (result) => settleResult(uiKey, result);
|
|
735
|
+
const fail = (error) => {
|
|
736
|
+
const detail = error instanceof EnhanceClientError ? error.detail : { code: "internal", message: error instanceof Error ? error.message : String(error) };
|
|
737
|
+
settleError(uiKey, detail);
|
|
738
|
+
};
|
|
739
|
+
if (settings.streaming) {
|
|
740
|
+
requestEnhanceStream({ sessionId: wireId, text: draft }, {
|
|
741
|
+
signal: controller.signal,
|
|
742
|
+
onDelta: (delta) => appendDelta(uiKey, delta)
|
|
743
|
+
}).then(settle, fail);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
requestEnhance({ sessionId: wireId, text: draft }, controller.signal).then(settle, fail);
|
|
747
|
+
}, [anyBusy, draft, imageCount, occurrenceCount, phase, settings, uiKey, wireId, t]);
|
|
616
748
|
const runRef = (0, import_react3.useRef)(start);
|
|
617
749
|
(0, import_react3.useEffect)(() => {
|
|
618
750
|
runRef.current = start;
|
|
@@ -804,6 +936,16 @@ var CSS = `
|
|
|
804
936
|
flex: 1; padding: 28px 16px; font-size: 13px;
|
|
805
937
|
}
|
|
806
938
|
.dsh-pe-loading .dsh-pe-hint { font-size: 11px; opacity: 0.55; }
|
|
939
|
+
/* Incremental view: the streamed text takes the whole body, left-aligned. */
|
|
940
|
+
.dsh-pe-stream {
|
|
941
|
+
display: flex; flex-direction: column;
|
|
942
|
+
flex: 1; min-height: 0; width: 100%;
|
|
943
|
+
}
|
|
944
|
+
.dsh-pe-stream .dsh-pe-col-title {
|
|
945
|
+
display: flex; align-items: center; gap: 6px;
|
|
946
|
+
padding-left: 0;
|
|
947
|
+
}
|
|
948
|
+
.dsh-pe-spin.small { width: 11px; height: 11px; border-width: 1px; }
|
|
807
949
|
.dsh-pe-spin {
|
|
808
950
|
width: 22px; height: 22px; border-radius: 50%;
|
|
809
951
|
border: 2px solid var(--pe-separator); border-top-color: #6ea8ff;
|