@crazyhappyone/dsh-tui 0.1.0-alpha.52 → 0.1.0-alpha.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-alpha.53 — 2026-10-22
4
+
5
+ Prerelease for DSH `0.2.1-rc.2`; npm channel: `next`.
6
+
7
+ ### Fixed
8
+
9
+ - String arrays such as `defaultInput` and `tags` stay on one editable settings row. Enter opens the value editor instead of one child row per element, and JSON or comma-separated input round-trips through the official settings patch. Arrays containing non-strings, including model lists and numeric arrays, remain browsable.
10
+ - Settings details stop measuring a long value once the visible row budget is full. The last visible row keeps the next source chunk and ends with `…`.
11
+ - Rapid settings selection updates coalesce to one notification per turn, and settled transcript rows reuse their projection while the block version and render scope stay unchanged.
12
+ - CI checks out the DSH tag named by `COMPATIBLE_DSH_VERSION`, activates that checkout's declared pnpm version, and runs the TUI tests from the mounted plugin sources.
13
+
14
+ ### Verification and upgrade
15
+
16
+ - Focused verification passed 250 tests: settings navigation, settings interaction, content wrapping, settings usability and hierarchy layout, stream view, markdown rendering, and block rows. The full TUI suite, launcher tests, typecheck, and OS PTY checks were not rerun for this patch.
17
+ - Update with `npm install --global @crazyhappyone/dsh-tui@next`, then relaunch. Source-linked installations can use `/reload` once the new bundle is available. The official profile and configuration services remain authoritative; no migration is required.
18
+
19
+ ### 中文变更说明
20
+
21
+ - `defaultInput`、`tags` 等纯字符串数组留在同一行编辑,Enter 打开值编辑器,不再为每个元素生成子行;JSON 和逗号输入都通过官方 settings patch 往返。包含非字符串的数组,包括模型列表和数字数组,仍可逐项浏览。
22
+ - 设置详情在可见行数用完后停止测量超长值,最后一行保留下一段内容并以 `…` 结束。
23
+ - 连续移动设置选中项时,每个事件循环只通知一次;已完成的聊天投影在块版本和渲染范围不变时复用。
24
+ - CI 按 `COMPATIBLE_DSH_VERSION` 检出对应 DSH tag,启用该检出声明的 pnpm 版本,再运行挂载后的 TUI 测试。
25
+ - 验证:上述聚焦测试 250 项通过。本补丁未重跑完整 TUI 套件、launcher 测试、类型检查和 OS PTY。
26
+ - 升级:`npm install --global @crazyhappyone/dsh-tui@next` 后重新启动;源码链接安装可在新 bundle 就绪后 `/reload`。继续使用官方配置服务,无需迁移。
27
+
28
+ [Full comparison](https://github.com/zhangyang-crazy-one/dsh-tui/compare/v0.1.0-alpha.52...v0.1.0-alpha.53)
29
+
3
30
  ## 0.1.0-alpha.52 — 2026-10-21
4
31
 
5
32
  Prerelease for DSH `0.2.1-rc.2`; npm channel: `next`.
package/README.md CHANGED
@@ -15,9 +15,9 @@ English | [中文](README.zh.md)
15
15
 
16
16
  `@crazyhappyone/dsh-tui` is a terminal interface for DeepSeek Harness (dsh), shipped as a dsh **plugin bundle** plus a `dsh-tui` launcher. The package declares `dsh.bundle`, so dsh installs it into a profile like any other plugin; the launcher only automates that install and boots the profile. This repository owns the plugin sources (`tui/`, `tui-render/`). DSH continues to own agents, sessions, tools, persistence, providers, permissions, and profile assembly.
17
17
 
18
- ## Release 0.1.0-alpha.52
18
+ ## Release 0.1.0-alpha.53
19
19
 
20
- This prerelease targets DSH `0.2.1-rc.2`. Settings now shows category tabs, a contrasting selected row, long-value details and clear save/cancel controls. Switching categories remembers the last nested location; search preserves selection, and pending settings writes reject duplicate submissions. See the [changelog](CHANGELOG.md#010-alpha52--2026-10-21) for the full changes and verification boundaries.
20
+ This prerelease targets DSH `0.2.1-rc.2`. String arrays such as `defaultInput` now edit in place, settings details truncate long values at the visible row budget, and settled transcript rows reuse their projection while unchanged. See the [changelog](CHANGELOG.md#010-alpha53--2026-10-22) for the verification boundary.
21
21
 
22
22
  ```sh
23
23
  npm install --global @crazyhappyone/dsh-tui@next
@@ -35,7 +35,7 @@ In `/settings`, Tab / Shift+Tab switches categories and restores each category's
35
35
 
36
36
  ## Table of Contents
37
37
 
38
- - [Release and upgrade](#release-010-alpha52)
38
+ - [Release and upgrade](#release-010-alpha53)
39
39
  - [Changelog](CHANGELOG.md)
40
40
  - [Terminal capabilities](#terminal-capabilities)
41
41
  - [Screenshots](#screenshots)
package/README.zh.md CHANGED
@@ -15,9 +15,9 @@ kind: "package-group"
15
15
 
16
16
  `@crazyhappyone/dsh-tui` 是 DeepSeek Harness(dsh)的终端界面,以 dsh **插件 bundle** 加 `dsh-tui` launcher 的形式发布。包声明了 `dsh.bundle`,因此 dsh 会像安装其他插件一样把它装进 profile;launcher 只负责自动完成这一安装并启动 profile。本仓库拥有插件源码(`tui/`、`tui-render/`)。Agent、会话、工具、持久化、provider、权限与 profile 组装仍由 DSH 拥有。
17
17
 
18
- ## 0.1.0-alpha.52 版本
18
+ ## 0.1.0-alpha.53 版本
19
19
 
20
- 本预发布版面向 DSH `0.2.1-rc.2`,设置增加可见分类栏、选中行背景、长值详情和明确的保存/取消提示。切换分类记忆深层位置,搜索确认保留选择,保存期间防止重复提交。完整变更与验证边界见 [changelog](CHANGELOG.md#010-alpha52--2026-10-21)。
20
+ 本预发布版面向 DSH `0.2.1-rc.2`。`defaultInput` 等纯字符串数组改为原位编辑,设置详情按可见行数截断长值,未变化的已完成聊天行复用投影。验证边界见 [changelog](CHANGELOG.md#010-alpha53--2026-10-22)。
21
21
 
22
22
  ```sh
23
23
  npm install --global @crazyhappyone/dsh-tui@next
@@ -35,7 +35,7 @@ dsh-tui
35
35
 
36
36
  ## 目录
37
37
 
38
- - [版本与升级](#010-alpha52-版本)
38
+ - [版本与升级](#010-alpha53-版本)
39
39
  - [Changelog](CHANGELOG.md)
40
40
  - [终端能力](#终端能力)
41
41
  - [截图](#screenshots)
package/dist/index.js CHANGED
@@ -39570,6 +39570,44 @@ function wrapDisplayLines(text4, maxCols) {
39570
39570
  }
39571
39571
  return out;
39572
39572
  }
39573
+ function wrapDisplayLinesLimited(text4, maxCols, limit) {
39574
+ const bounded = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : Number.POSITIVE_INFINITY;
39575
+ if (bounded === 0) return { lines: [], truncated: text4 !== "" };
39576
+ if (bounded === Number.POSITIVE_INFINITY) return { lines: wrapDisplayLines(text4, maxCols), truncated: false };
39577
+ if (maxCols <= 0) {
39578
+ const lines2 = text4.split("\n", bounded + 1);
39579
+ const truncated2 = lines2.length > bounded;
39580
+ if (truncated2) lines2.pop();
39581
+ return { lines: lines2, truncated: truncated2 };
39582
+ }
39583
+ const lines = [];
39584
+ let row = "";
39585
+ let columns = 0;
39586
+ const truncated = () => ({
39587
+ lines: [...lines, `${wcwidthSafeSlice(row, Math.max(0, maxCols - 1))}\u2026`],
39588
+ truncated: true
39589
+ });
39590
+ for (const { segment: segment2 } of GRAPHEME.segment(text4)) {
39591
+ if (segment2 === "\n") {
39592
+ if (lines.length + 1 === bounded) return truncated();
39593
+ lines.push(row);
39594
+ row = "";
39595
+ columns = 0;
39596
+ continue;
39597
+ }
39598
+ const width = displayWidth(segment2);
39599
+ if (columns + width > maxCols && row !== "") {
39600
+ if (lines.length + 1 === bounded) return truncated();
39601
+ lines.push(row);
39602
+ row = "";
39603
+ columns = 0;
39604
+ }
39605
+ row += segment2;
39606
+ columns += width;
39607
+ }
39608
+ lines.push(row);
39609
+ return { lines, truncated: false };
39610
+ }
39573
39611
 
39574
39612
  // packages/tui/tui-render/src/terminal-capabilities.ts
39575
39613
  var ESC_TIMEOUT_MS = 40;
@@ -56347,6 +56385,14 @@ function transcriptBlockId(row) {
56347
56385
  function transcriptBlockVersion(row, revisions) {
56348
56386
  return revisions.revision(row.kind === "compaction" ? row.divider : row.message);
56349
56387
  }
56388
+ function settledProjectionSignature(row, revisions, options) {
56389
+ return [
56390
+ transcriptBlockVersion(row, revisions),
56391
+ options.hasSubsequent ? "gap" : "tail",
56392
+ options.isLatestAssistant ? "latest" : "",
56393
+ row.kind === "compaction" && options.expandedCompactionId === row.divider.compactionId ? "expanded" : ""
56394
+ ].join("\0");
56395
+ }
56350
56396
  function activeTurnVersion(turn, revisions) {
56351
56397
  return revisions.revision(turn, [
56352
56398
  turn.assistantText,
@@ -56880,6 +56926,9 @@ function StreamView({
56880
56926
  const viewportRef = (0, import_react37.useRef)(null);
56881
56927
  const layoutCache = (0, import_react37.useRef)(new TranscriptLayoutCache());
56882
56928
  const projectorStates = (0, import_react37.useRef)(/* @__PURE__ */ new Map());
56929
+ const settledProjectionCache = (0, import_react37.useRef)(/* @__PURE__ */ new Map());
56930
+ const settledProjectionScope = (0, import_react37.useRef)("");
56931
+ const settledProjectionPresenters = (0, import_react37.useRef)(void 0);
56883
56932
  const rebuildBlocks = (0, import_react37.useRef)(/* @__PURE__ */ new Map());
56884
56933
  const lineStore = (0, import_react37.useRef)(void 0);
56885
56934
  if (lineStore.current === void 0) {
@@ -57059,6 +57108,24 @@ function StreamView({
57059
57108
  );
57060
57109
  const hasActiveTurn = activeTurn !== void 0;
57061
57110
  const settledEntryRows = (0, import_react37.useMemo)(() => {
57111
+ const projectionScope = [
57112
+ settledBlockRowsScope.scopeKey,
57113
+ settledAssistantBlockRowsScope.scopeKey,
57114
+ reasoningExpanded ? "r1" : "r0",
57115
+ toolCardsExpanded ? "t1" : "t0",
57116
+ mode,
57117
+ locale
57118
+ ].join("\0");
57119
+ if (settledProjectionScope.current !== projectionScope) {
57120
+ settledProjectionCache.current = /* @__PURE__ */ new Map();
57121
+ settledProjectionScope.current = projectionScope;
57122
+ }
57123
+ if (settledProjectionPresenters.current !== presenters) {
57124
+ settledProjectionCache.current = /* @__PURE__ */ new Map();
57125
+ settledProjectionPresenters.current = presenters;
57126
+ }
57127
+ const previousCache = settledProjectionCache.current;
57128
+ const nextCache = /* @__PURE__ */ new Map();
57062
57129
  const definitions = /* @__PURE__ */ new Map();
57063
57130
  const batchPresenters = presenters === void 0 ? void 0 : { get(name2) {
57064
57131
  if (!definitions.has(name2)) definitions.set(name2, presenters.get(name2));
@@ -57071,10 +57138,33 @@ function StreamView({
57071
57138
  const toolSticky = /* @__PURE__ */ new Map();
57072
57139
  const userSticky = /* @__PURE__ */ new Map();
57073
57140
  const projectorCache = projectorStates.current;
57074
- const project = makeBlockProjector({ toolRows, plainRows, locale }, storeBlocks);
57075
57141
  const latestAssistant = hasActiveTurn ? void 0 : latestAssistantId(history, void 0);
57076
57142
  for (const [index2, row] of transcript.entries()) {
57077
57143
  const id = transcriptBlockId(row);
57144
+ const hasSubsequent = index2 < transcript.length - 1 || hasActiveTurn;
57145
+ const cached2 = previousCache.get(id);
57146
+ const signature = settledProjectionSignature(row, revisions, {
57147
+ hasSubsequent,
57148
+ isLatestAssistant: row.kind === "message" && row.message.id === latestAssistant,
57149
+ expandedCompactionId
57150
+ });
57151
+ if (cached2?.signature === signature) {
57152
+ map2.set(id, cached2.lines);
57153
+ if (cached2.textRanges !== void 0) textRanges.set(id, cached2.textRanges);
57154
+ if (cached2.reasoningSticky !== void 0) reasoningSticky.set(id, cached2.reasoningSticky);
57155
+ if (cached2.toolSticky !== void 0) toolSticky.set(id, [...cached2.toolSticky]);
57156
+ if (cached2.userSticky !== void 0) userSticky.set(id, cached2.userSticky);
57157
+ for (const block of cached2.storeBlocks) storeBlocks.set(block.entry.id, block);
57158
+ nextCache.set(id, cached2);
57159
+ continue;
57160
+ }
57161
+ const rowBlocks = /* @__PURE__ */ new Map();
57162
+ const project = makeBlockProjector({ toolRows, plainRows, locale }, rowBlocks);
57163
+ const collectBlocks = () => {
57164
+ const blocks = [...rowBlocks.values()];
57165
+ for (const block of blocks) storeBlocks.set(block.entry.id, block);
57166
+ return blocks;
57167
+ };
57078
57168
  if (row.kind === "compaction") {
57079
57169
  const meta3 = {
57080
57170
  compactionSummary: row.divider.summary,
@@ -57089,10 +57179,18 @@ function StreamView({
57089
57179
  meta: meta3
57090
57180
  }, settledBlockRowsScope, void 0, false);
57091
57181
  map2.set(id, projection.lines);
57182
+ nextCache.set(id, {
57183
+ signature,
57184
+ lines: projection.lines,
57185
+ textRanges: void 0,
57186
+ storeBlocks: collectBlocks(),
57187
+ reasoningSticky: void 0,
57188
+ toolSticky: void 0,
57189
+ userSticky: void 0
57190
+ });
57092
57191
  continue;
57093
57192
  }
57094
57193
  if (row.message.kind === "user") {
57095
- const hasSubsequent = index2 < transcript.length - 1 || hasActiveTurn;
57096
57194
  const projection = project(id, {
57097
57195
  id,
57098
57196
  kind: "user",
@@ -57100,7 +57198,17 @@ function StreamView({
57100
57198
  ...hasSubsequent ? { meta: { userMessageGap: true } } : {}
57101
57199
  }, settledBlockRowsScope, void 0, false);
57102
57200
  map2.set(id, projection.lines);
57103
- userSticky.set(id, { start: 0, end: projection.lines.length, text: row.message.text });
57201
+ const sticky = { start: 0, end: projection.lines.length, text: row.message.text };
57202
+ userSticky.set(id, sticky);
57203
+ nextCache.set(id, {
57204
+ signature,
57205
+ lines: projection.lines,
57206
+ textRanges: void 0,
57207
+ storeBlocks: collectBlocks(),
57208
+ reasoningSticky: void 0,
57209
+ toolSticky: void 0,
57210
+ userSticky: sticky
57211
+ });
57104
57212
  continue;
57105
57213
  }
57106
57214
  const parts = displayedParts(
@@ -57204,9 +57312,20 @@ function StreamView({
57204
57312
  meta: tailMeta
57205
57313
  }, settledBlockRowsScope, void 0, false).lines);
57206
57314
  }
57207
- map2.set(id, rows2.build());
57315
+ const built = rows2.build();
57316
+ map2.set(id, built);
57208
57317
  if (ranges.length > 0) textRanges.set(id, ranges);
57318
+ nextCache.set(id, {
57319
+ signature,
57320
+ lines: built,
57321
+ textRanges: ranges.length > 0 ? ranges : void 0,
57322
+ storeBlocks: collectBlocks(),
57323
+ reasoningSticky: reasoningSticky.get(id),
57324
+ toolSticky: toolSticky.get(id),
57325
+ userSticky: void 0
57326
+ });
57209
57327
  }
57328
+ settledProjectionCache.current = nextCache;
57210
57329
  return { rows: map2, textRanges, storeBlocks, reasoningSticky, toolSticky, userSticky };
57211
57330
  }, [
57212
57331
  contentWidth,
@@ -58994,7 +59113,8 @@ var SETTINGS_GROUP_LABELS = {
58994
59113
  var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
58995
59114
  function fit(text4, width) {
58996
59115
  const safe = escapeContent(text4).replace(/\n/gu, " \u21B5 ");
58997
- return displayWidth(safe) <= width ? safe : `${wcwidthSafeSlice(safe, Math.max(0, width - 1))}\u2026`;
59116
+ const prefix = wcwidthSafeSlice(safe, width);
59117
+ return prefix.length === safe.length ? safe : `${wcwidthSafeSlice(prefix, Math.max(0, width - 1))}\u2026`;
58998
59118
  }
58999
59119
  function canToggleSetting(row) {
59000
59120
  return row !== void 0 && row.navigable !== true && row.editable !== false && (row.value === "true" || row.value === "false" || row.namespace === "tui" && ["brandAnimation", "locale", "theme", "colorTier"].includes(row.field));
@@ -59068,14 +59188,16 @@ function SettingsBrowser(props) {
59068
59188
  const detailBudget = compact || active === void 0 ? 0 : Math.min(6, Math.max(0, space - Math.min(3, Math.max(1, rows.length))));
59069
59189
  if (active !== void 0 && detailBudget > 0) {
59070
59190
  const source = filtered ? `${active.namespace} \xB7 ${active.field}` : fieldLabel(active, locale);
59071
- detail.push(...wrapDisplayLines(escapeContent(`\u5F53\u524D\u9879: ${source}`), width).slice(0, filtered ? 2 : 1));
59191
+ const pushBounded = (text4, limit) => {
59192
+ if (limit <= 0 || detail.length >= detailBudget) return;
59193
+ const room = Math.min(limit, detailBudget - detail.length);
59194
+ const wrapped = wrapDisplayLinesLimited(escapeContent(text4), width, room);
59195
+ detail.push(...wrapped.lines);
59196
+ };
59197
+ pushBounded(`\u5F53\u524D\u9879: ${source}`, filtered ? 2 : 1);
59072
59198
  const value = active.value === "" ? "\u2014" : active.value;
59073
- detail.push(...wrapDisplayLines(escapeContent(`${active.navigable === true ? "\u5305\u542B" : "\u5F53\u524D\u503C"}: ${value}`), width));
59074
- if (active.description !== void 0) detail.push(...wrapDisplayLines(escapeContent(active.description), width));
59075
- if (detail.length > detailBudget) {
59076
- detail.length = detailBudget;
59077
- detail[detail.length - 1] = `${wcwidthSafeSlice(detail[detail.length - 1], Math.max(0, width - 1))}\u2026`;
59078
- }
59199
+ pushBounded(`${active.navigable === true ? "\u5305\u542B" : "\u5F53\u524D\u503C"}: ${value}`, detailBudget);
59200
+ if (active.description !== void 0) pushBounded(active.description, detailBudget);
59079
59201
  }
59080
59202
  const size = Math.min(rows.length, Math.max(0, space - detail.length));
59081
59203
  const start = Math.max(0, Math.min(rows.length - size, selected - Math.floor(size / 2)));
@@ -99586,7 +99708,19 @@ function parseSettingsFieldValue(namespace, field, current, draft, knownProvider
99586
99708
  if (field === "defaultInput" || field.endsWith(".defaultInput")) {
99587
99709
  const trimmed = draft.trim().toLowerCase();
99588
99710
  if (trimmed === "") return ["text", "image"];
99589
- const tokens = trimmed.split(/[,\s]+/).filter((t2) => t2 !== "");
99711
+ const json2 = trimmed.startsWith("[") ? trimmed : void 0;
99712
+ let parsed;
99713
+ if (json2 !== void 0) {
99714
+ try {
99715
+ parsed = JSON.parse(json2);
99716
+ } catch {
99717
+ throw new TypeError("JSON \u65E0\u6548");
99718
+ }
99719
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
99720
+ throw new TypeError('defaultInput \u9700\u8981\u5B57\u7B26\u4E32\u6570\u7EC4\uFF0C\u4F8B\u5982 ["text", "image"]');
99721
+ }
99722
+ }
99723
+ const tokens = parsed === void 0 ? trimmed.split(/[,\s]+/).filter((t2) => t2 !== "") : parsed;
99590
99724
  const result = [];
99591
99725
  for (const token of tokens) {
99592
99726
  if (token === "text" || token === "\u6587\u672C") {
@@ -99633,6 +99767,10 @@ function childSchema(schema, key) {
99633
99767
  const node2 = record2(schema);
99634
99768
  return record2(node2.dict)[key] ?? record2(node2.properties)[key] ?? node2.inner ?? node2.items;
99635
99769
  }
99770
+ function navigableValue(value) {
99771
+ if (Array.isArray(value)) return value.some((item) => typeof item !== "string");
99772
+ return value !== null && typeof value === "object";
99773
+ }
99636
99774
  function summary(value) {
99637
99775
  return Array.isArray(value) ? `${String(value.length)} \u9879` : value !== null && typeof value === "object" ? `${String(Object.keys(value).length)} \u4E2A\u5B57\u6BB5` : stringifySettingValue(value);
99638
99776
  }
@@ -99671,7 +99809,7 @@ function settingsTreeFromDescribe(descriptors, credentials) {
99671
99809
  const modelId = record2(value).id;
99672
99810
  const identity = Array.isArray(parentValue) && typeof modelId === "string" && parentValue.filter((entry) => record2(entry).id === modelId).length === 1 ? `id:${modelId}` : key;
99673
99811
  const id = `${parentId}:${JSON.stringify(identity)}`;
99674
- const nested = value !== null && typeof value === "object";
99812
+ const navigable = navigableValue(value);
99675
99813
  const meta3 = record2(record2(schema).meta);
99676
99814
  const label = Array.isArray(parentValue) ? String(record2(value).id ?? record2(value).name ?? key) : key;
99677
99815
  nodes.push({
@@ -99681,14 +99819,14 @@ function settingsTreeFromDescribe(descriptors, credentials) {
99681
99819
  field,
99682
99820
  sourcePath: path2,
99683
99821
  label,
99684
- value: path2.length === 2 && path2[0] === "providers" ? `${String(Array.isArray(record2(value).models) ? record2(value).models.length : 0)} \u6A21\u578B \xB7 ${String(record2(value).api ?? "\u9ED8\u8BA4\u534F\u8BAE")}` : nested ? summary(value) : stringifySettingsFieldValue(namespace, field, value),
99822
+ value: path2.length === 2 && path2[0] === "providers" ? `${String(Array.isArray(record2(value).models) ? record2(value).models.length : 0)} \u6A21\u578B \xB7 ${String(record2(value).api ?? "\u9ED8\u8BA4\u534F\u8BAE")}` : navigable ? summary(value) : stringifySettingsFieldValue(namespace, field, value),
99685
99823
  editValue: stringifySettingValue(value),
99686
- ...nested && secrets.some((secret) => path2.every((key2, index2) => secret[index2] === key2)) ? { editable: false } : {},
99687
- ...nested ? { navigable: true } : {},
99824
+ ...navigable && secrets.some((secret) => path2.every((key2, index2) => secret[index2] === key2)) ? { editable: false } : {},
99825
+ ...navigable ? { navigable: true } : {},
99688
99826
  ...typeof meta3.description === "string" ? { description: meta3.description } : {},
99689
99827
  ...meta3.required === true ? { required: true } : {}
99690
99828
  });
99691
- if (nested) {
99829
+ if (navigable) {
99692
99830
  for (const [child, next] of Object.entries(value)) walk(next, [...path2, child], id, childSchema(schema, child));
99693
99831
  if (namespace === "llm-pi-ai" && field === "providers") {
99694
99832
  nodes.push({ id: `${id}:add`, parentId: id, namespace, field, sourcePath: path2, label: "+ \u6DFB\u52A0 Provider", value: "\u76EE\u5F55\u540D / \u8868\u5355", editValue: PROVIDER_FORM_TEMPLATE });
@@ -100599,6 +100737,9 @@ var RuntimeController = class _RuntimeController {
100599
100737
  settingsOpen = false;
100600
100738
  settingsRows = [];
100601
100739
  settingsSelectedIndex = 0;
100740
+ /** Selection changes inside one input burst share one settings notification. */
100741
+ settingsMovePending = false;
100742
+ settingsMoveEmit;
100602
100743
  settingsNavigation = new SettingsNavigation([]);
100603
100744
  settingsSearching = false;
100604
100745
  settingsEditing = false;
@@ -103523,7 +103664,8 @@ ${line5}`.slice(-64e3);
103523
103664
  Math.max(0, this.settingsSelectedIndex + action.delta)
103524
103665
  );
103525
103666
  this.settingsNavigation.selectedIndex = this.settingsSelectedIndex;
103526
- this.emit();
103667
+ this.settingsPaneSnapshot = void 0;
103668
+ this.scheduleSettingsSelectionEmit();
103527
103669
  return;
103528
103670
  case "settings-edit":
103529
103671
  case "settings-edit-json":
@@ -106857,6 +106999,8 @@ ${row.hint ?? ""}` });
106857
106999
  notifyListeners() {
106858
107000
  this.emitLastRun = Date.now();
106859
107001
  this.emitPending = false;
107002
+ this.settingsMovePending = false;
107003
+ this.settingsMoveEmit = void 0;
106860
107004
  for (const listener of this.listeners) {
106861
107005
  try {
106862
107006
  listener();
@@ -106865,8 +107009,28 @@ ${row.hint ?? ""}` });
106865
107009
  }
106866
107010
  }
106867
107011
  }
107012
+ /**
107013
+ * Publish only the settings selection. A wheel or key burst keeps the latest
107014
+ * index readable immediately and notifies subscribers once per turn.
107015
+ */
107016
+ scheduleSettingsSelectionEmit() {
107017
+ if (this.closed) return;
107018
+ this.settingsMovePending = true;
107019
+ if (this.settingsMoveEmit !== void 0) return;
107020
+ const token = {};
107021
+ this.settingsMoveEmit = token;
107022
+ queueMicrotask(() => {
107023
+ if (this.settingsMoveEmit !== token || this.closed || !this.settingsMovePending) return;
107024
+ this.settingsMoveEmit = void 0;
107025
+ this.settingsMovePending = false;
107026
+ if (this.emitTimer !== void 0) this.emitPending = true;
107027
+ else this.notifyListeners();
107028
+ });
107029
+ }
106868
107030
  emit() {
106869
107031
  if (this.closed) return;
107032
+ this.settingsMovePending = false;
107033
+ this.settingsMoveEmit = void 0;
106870
107034
  this.modelSnapshot = void 0;
106871
107035
  this.sessionPaneSnapshot = void 0;
106872
107036
  this.searchPaneSnapshot = void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crazyhappyone/dsh-tui",
3
- "version": "0.1.0-alpha.52",
3
+ "version": "0.1.0-alpha.54",
4
4
  "description": "DeepSeek Harness terminal interface: a dsh plugin bundle plus its launcher",
5
5
  "type": "module",
6
6
  "license": "MIT",