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

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
@@ -39537,38 +39537,91 @@ function padDisplayEnd(text4, width) {
39537
39537
  return `${text4}${" ".repeat(width - cols)}`;
39538
39538
  }
39539
39539
  function wrapDisplayLines(text4, maxCols) {
39540
+ return wrapDisplayLinesLimited(text4, maxCols, Number.POSITIVE_INFINITY).lines;
39541
+ }
39542
+ function wrapDisplayLinesLimited(text4, maxCols, limit) {
39543
+ const bounded = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : Number.POSITIVE_INFINITY;
39540
39544
  const source = text4.split("\n");
39541
- if (maxCols <= 0) return source;
39545
+ if (bounded === 0) return { lines: [], truncated: text4 !== "" };
39546
+ if (maxCols <= 0) {
39547
+ const lines = source.slice(0, bounded);
39548
+ return { lines, truncated: source.length > lines.length };
39549
+ }
39542
39550
  const out = [];
39543
- for (const line5 of source) {
39551
+ const room = () => out.length < bounded;
39552
+ const mark2 = (line5) => {
39553
+ const marker = `${wcwidthSafeSlice(line5, Math.max(0, maxCols - 1))}\u2026`;
39554
+ if (out.length < bounded) out.push(marker);
39555
+ else out[out.length - 1] = marker;
39556
+ };
39557
+ for (let lineIndex = 0; lineIndex < source.length; lineIndex += 1) {
39558
+ const line5 = source[lineIndex] ?? "";
39559
+ const laterLine = lineIndex < source.length - 1;
39544
39560
  if (line5 === "" || displayWidth(line5) <= maxCols) {
39561
+ if (!room()) {
39562
+ mark2("");
39563
+ return { lines: out, truncated: true };
39564
+ }
39565
+ if (out.length + 1 === bounded && laterLine) {
39566
+ mark2(line5);
39567
+ return { lines: out, truncated: true };
39568
+ }
39545
39569
  out.push(line5);
39546
39570
  continue;
39547
39571
  }
39548
39572
  if (/^[\x20-\x7e]*$/u.test(line5)) {
39549
39573
  for (let i2 = 0; i2 < line5.length; i2 += maxCols) {
39550
- out.push(line5.slice(i2, i2 + maxCols));
39574
+ const piece = line5.slice(i2, i2 + maxCols);
39575
+ const continues = i2 + maxCols < line5.length || laterLine;
39576
+ if (!room()) {
39577
+ mark2(piece);
39578
+ return { lines: out, truncated: true };
39579
+ }
39580
+ if (out.length + 1 === bounded && continues) {
39581
+ mark2(piece);
39582
+ return { lines: out, truncated: true };
39583
+ }
39584
+ out.push(piece);
39551
39585
  }
39552
39586
  continue;
39553
39587
  }
39554
39588
  let curLine = "";
39555
39589
  let curCols = 0;
39556
- for (const { segment: segment2 } of GRAPHEME.segment(line5)) {
39590
+ const segments = [...GRAPHEME.segment(line5)];
39591
+ for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
39592
+ const segment2 = segments[segmentIndex]?.segment ?? "";
39557
39593
  const width = displayWidth(segment2);
39558
- if (curCols + width > maxCols && curLine !== "") {
39559
- out.push(curLine);
39560
- curLine = segment2;
39561
- curCols = width;
39562
- } else {
39594
+ if (curCols + width <= maxCols || curLine === "") {
39563
39595
  curLine += segment2;
39564
39596
  curCols += width;
39597
+ continue;
39598
+ }
39599
+ if (!room()) {
39600
+ mark2(curLine);
39601
+ return { lines: out, truncated: true };
39602
+ }
39603
+ const continues = segmentIndex < segments.length || laterLine;
39604
+ if (out.length + 1 === bounded && continues) {
39605
+ mark2(curLine);
39606
+ return { lines: out, truncated: true };
39565
39607
  }
39608
+ out.push(curLine);
39609
+ curLine = segment2;
39610
+ curCols = width;
39566
39611
  }
39567
39612
  if (curLine !== "") {
39613
+ if (!room()) {
39614
+ mark2(curLine);
39615
+ return { lines: out, truncated: true };
39616
+ }
39617
+ if (out.length + 1 === bounded && laterLine) {
39618
+ mark2(curLine);
39619
+ return { lines: out, truncated: true };
39620
+ }
39568
39621
  out.push(curLine);
39569
39622
  }
39570
39623
  }
39571
- return out;
39624
+ return { lines: out, truncated: false };
39572
39625
  }
39573
39626
 
39574
39627
  // packages/tui/tui-render/src/terminal-capabilities.ts
@@ -56347,6 +56400,14 @@ function transcriptBlockId(row) {
56347
56400
  function transcriptBlockVersion(row, revisions) {
56348
56401
  return revisions.revision(row.kind === "compaction" ? row.divider : row.message);
56349
56402
  }
56403
+ function settledProjectionSignature(row, revisions, options) {
56404
+ return [
56405
+ transcriptBlockVersion(row, revisions),
56406
+ options.hasSubsequent ? "gap" : "tail",
56407
+ options.isLatestAssistant ? "latest" : "",
56408
+ options.expandedCompactionId ?? ""
56409
+ ].join("\0");
56410
+ }
56350
56411
  function activeTurnVersion(turn, revisions) {
56351
56412
  return revisions.revision(turn, [
56352
56413
  turn.assistantText,
@@ -56880,6 +56941,9 @@ function StreamView({
56880
56941
  const viewportRef = (0, import_react37.useRef)(null);
56881
56942
  const layoutCache = (0, import_react37.useRef)(new TranscriptLayoutCache());
56882
56943
  const projectorStates = (0, import_react37.useRef)(/* @__PURE__ */ new Map());
56944
+ const settledProjectionCache = (0, import_react37.useRef)(/* @__PURE__ */ new Map());
56945
+ const settledProjectionScope = (0, import_react37.useRef)("");
56946
+ const settledProjectionPresenters = (0, import_react37.useRef)(void 0);
56883
56947
  const rebuildBlocks = (0, import_react37.useRef)(/* @__PURE__ */ new Map());
56884
56948
  const lineStore = (0, import_react37.useRef)(void 0);
56885
56949
  if (lineStore.current === void 0) {
@@ -57059,6 +57123,24 @@ function StreamView({
57059
57123
  );
57060
57124
  const hasActiveTurn = activeTurn !== void 0;
57061
57125
  const settledEntryRows = (0, import_react37.useMemo)(() => {
57126
+ const projectionScope = [
57127
+ settledBlockRowsScope.scopeKey,
57128
+ settledAssistantBlockRowsScope.scopeKey,
57129
+ reasoningExpanded ? "r1" : "r0",
57130
+ toolCardsExpanded ? "t1" : "t0",
57131
+ mode,
57132
+ locale
57133
+ ].join("\0");
57134
+ if (settledProjectionScope.current !== projectionScope) {
57135
+ settledProjectionCache.current = /* @__PURE__ */ new Map();
57136
+ settledProjectionScope.current = projectionScope;
57137
+ }
57138
+ if (settledProjectionPresenters.current !== presenters) {
57139
+ settledProjectionCache.current = /* @__PURE__ */ new Map();
57140
+ settledProjectionPresenters.current = presenters;
57141
+ }
57142
+ const previousCache = settledProjectionCache.current;
57143
+ const nextCache = /* @__PURE__ */ new Map();
57062
57144
  const definitions = /* @__PURE__ */ new Map();
57063
57145
  const batchPresenters = presenters === void 0 ? void 0 : { get(name2) {
57064
57146
  if (!definitions.has(name2)) definitions.set(name2, presenters.get(name2));
@@ -57075,6 +57157,24 @@ function StreamView({
57075
57157
  const latestAssistant = hasActiveTurn ? void 0 : latestAssistantId(history, void 0);
57076
57158
  for (const [index2, row] of transcript.entries()) {
57077
57159
  const id = transcriptBlockId(row);
57160
+ const hasSubsequent = index2 < transcript.length - 1 || hasActiveTurn;
57161
+ const cached2 = previousCache.get(id);
57162
+ const signature = settledProjectionSignature(row, revisions, {
57163
+ hasSubsequent,
57164
+ isLatestAssistant: row.kind === "message" && row.message.id === latestAssistant,
57165
+ expandedCompactionId
57166
+ });
57167
+ if (cached2?.signature === signature) {
57168
+ map2.set(id, cached2.lines);
57169
+ if (cached2.textRanges !== void 0) textRanges.set(id, cached2.textRanges);
57170
+ if (cached2.reasoningSticky !== void 0) reasoningSticky.set(id, cached2.reasoningSticky);
57171
+ if (cached2.toolSticky !== void 0) toolSticky.set(id, [...cached2.toolSticky]);
57172
+ if (cached2.userSticky !== void 0) userSticky.set(id, cached2.userSticky);
57173
+ for (const block of cached2.storeBlocks) storeBlocks.set(block.entry.id, block);
57174
+ nextCache.set(id, cached2);
57175
+ continue;
57176
+ }
57177
+ const blocksBefore = storeBlocks.size;
57078
57178
  if (row.kind === "compaction") {
57079
57179
  const meta3 = {
57080
57180
  compactionSummary: row.divider.summary,
@@ -57089,10 +57189,18 @@ function StreamView({
57089
57189
  meta: meta3
57090
57190
  }, settledBlockRowsScope, void 0, false);
57091
57191
  map2.set(id, projection.lines);
57192
+ nextCache.set(id, {
57193
+ signature,
57194
+ lines: projection.lines,
57195
+ textRanges: void 0,
57196
+ storeBlocks: [...storeBlocks.values()].slice(blocksBefore),
57197
+ reasoningSticky: void 0,
57198
+ toolSticky: void 0,
57199
+ userSticky: void 0
57200
+ });
57092
57201
  continue;
57093
57202
  }
57094
57203
  if (row.message.kind === "user") {
57095
- const hasSubsequent = index2 < transcript.length - 1 || hasActiveTurn;
57096
57204
  const projection = project(id, {
57097
57205
  id,
57098
57206
  kind: "user",
@@ -57100,7 +57208,17 @@ function StreamView({
57100
57208
  ...hasSubsequent ? { meta: { userMessageGap: true } } : {}
57101
57209
  }, settledBlockRowsScope, void 0, false);
57102
57210
  map2.set(id, projection.lines);
57103
- userSticky.set(id, { start: 0, end: projection.lines.length, text: row.message.text });
57211
+ const sticky = { start: 0, end: projection.lines.length, text: row.message.text };
57212
+ userSticky.set(id, sticky);
57213
+ nextCache.set(id, {
57214
+ signature,
57215
+ lines: projection.lines,
57216
+ textRanges: void 0,
57217
+ storeBlocks: [...storeBlocks.values()].slice(blocksBefore),
57218
+ reasoningSticky: void 0,
57219
+ toolSticky: void 0,
57220
+ userSticky: sticky
57221
+ });
57104
57222
  continue;
57105
57223
  }
57106
57224
  const parts = displayedParts(
@@ -57204,9 +57322,20 @@ function StreamView({
57204
57322
  meta: tailMeta
57205
57323
  }, settledBlockRowsScope, void 0, false).lines);
57206
57324
  }
57207
- map2.set(id, rows2.build());
57325
+ const built = rows2.build();
57326
+ map2.set(id, built);
57208
57327
  if (ranges.length > 0) textRanges.set(id, ranges);
57328
+ nextCache.set(id, {
57329
+ signature,
57330
+ lines: built,
57331
+ textRanges: ranges.length > 0 ? ranges : void 0,
57332
+ storeBlocks: [...storeBlocks.values()].slice(blocksBefore),
57333
+ reasoningSticky: reasoningSticky.get(id),
57334
+ toolSticky: toolSticky.get(id),
57335
+ userSticky: void 0
57336
+ });
57209
57337
  }
57338
+ settledProjectionCache.current = nextCache;
57210
57339
  return { rows: map2, textRanges, storeBlocks, reasoningSticky, toolSticky, userSticky };
57211
57340
  }, [
57212
57341
  contentWidth,
@@ -59068,14 +59197,16 @@ function SettingsBrowser(props) {
59068
59197
  const detailBudget = compact || active === void 0 ? 0 : Math.min(6, Math.max(0, space - Math.min(3, Math.max(1, rows.length))));
59069
59198
  if (active !== void 0 && detailBudget > 0) {
59070
59199
  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));
59200
+ const pushBounded = (text4, limit) => {
59201
+ if (limit <= 0 || detail.length >= detailBudget) return;
59202
+ const room = Math.min(limit, detailBudget - detail.length);
59203
+ const wrapped = wrapDisplayLinesLimited(escapeContent(text4), width, room);
59204
+ detail.push(...wrapped.lines);
59205
+ };
59206
+ pushBounded(`\u5F53\u524D\u9879: ${source}`, filtered ? 2 : 1);
59072
59207
  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
- }
59208
+ pushBounded(`${active.navigable === true ? "\u5305\u542B" : "\u5F53\u524D\u503C"}: ${value}`, detailBudget);
59209
+ if (active.description !== void 0) pushBounded(active.description, detailBudget);
59079
59210
  }
59080
59211
  const size = Math.min(rows.length, Math.max(0, space - detail.length));
59081
59212
  const start = Math.max(0, Math.min(rows.length - size, selected - Math.floor(size / 2)));
@@ -99586,7 +99717,19 @@ function parseSettingsFieldValue(namespace, field, current, draft, knownProvider
99586
99717
  if (field === "defaultInput" || field.endsWith(".defaultInput")) {
99587
99718
  const trimmed = draft.trim().toLowerCase();
99588
99719
  if (trimmed === "") return ["text", "image"];
99589
- const tokens = trimmed.split(/[,\s]+/).filter((t2) => t2 !== "");
99720
+ const json2 = trimmed.startsWith("[") ? trimmed : void 0;
99721
+ let parsed;
99722
+ if (json2 !== void 0) {
99723
+ try {
99724
+ parsed = JSON.parse(json2);
99725
+ } catch {
99726
+ throw new TypeError("JSON \u65E0\u6548");
99727
+ }
99728
+ if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
99729
+ throw new TypeError('defaultInput \u9700\u8981\u5B57\u7B26\u4E32\u6570\u7EC4\uFF0C\u4F8B\u5982 ["text", "image"]');
99730
+ }
99731
+ }
99732
+ const tokens = parsed === void 0 ? trimmed.split(/[,\s]+/).filter((t2) => t2 !== "") : parsed;
99590
99733
  const result = [];
99591
99734
  for (const token of tokens) {
99592
99735
  if (token === "text" || token === "\u6587\u672C") {
@@ -99633,6 +99776,10 @@ function childSchema(schema, key) {
99633
99776
  const node2 = record2(schema);
99634
99777
  return record2(node2.dict)[key] ?? record2(node2.properties)[key] ?? node2.inner ?? node2.items;
99635
99778
  }
99779
+ function navigableValue(value) {
99780
+ if (Array.isArray(value)) return value.some((item) => typeof item !== "string");
99781
+ return value !== null && typeof value === "object";
99782
+ }
99636
99783
  function summary(value) {
99637
99784
  return Array.isArray(value) ? `${String(value.length)} \u9879` : value !== null && typeof value === "object" ? `${String(Object.keys(value).length)} \u4E2A\u5B57\u6BB5` : stringifySettingValue(value);
99638
99785
  }
@@ -99672,6 +99819,7 @@ function settingsTreeFromDescribe(descriptors, credentials) {
99672
99819
  const identity = Array.isArray(parentValue) && typeof modelId === "string" && parentValue.filter((entry) => record2(entry).id === modelId).length === 1 ? `id:${modelId}` : key;
99673
99820
  const id = `${parentId}:${JSON.stringify(identity)}`;
99674
99821
  const nested = value !== null && typeof value === "object";
99822
+ const navigable = navigableValue(value);
99675
99823
  const meta3 = record2(record2(schema).meta);
99676
99824
  const label = Array.isArray(parentValue) ? String(record2(value).id ?? record2(value).name ?? key) : key;
99677
99825
  nodes.push({
@@ -99681,14 +99829,14 @@ function settingsTreeFromDescribe(descriptors, credentials) {
99681
99829
  field,
99682
99830
  sourcePath: path2,
99683
99831
  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),
99832
+ 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
99833
  editValue: stringifySettingValue(value),
99686
- ...nested && secrets.some((secret) => path2.every((key2, index2) => secret[index2] === key2)) ? { editable: false } : {},
99687
- ...nested ? { navigable: true } : {},
99834
+ ...navigable && secrets.some((secret) => path2.every((key2, index2) => secret[index2] === key2)) ? { editable: false } : {},
99835
+ ...navigable ? { navigable: true } : {},
99688
99836
  ...typeof meta3.description === "string" ? { description: meta3.description } : {},
99689
99837
  ...meta3.required === true ? { required: true } : {}
99690
99838
  });
99691
- if (nested) {
99839
+ if (navigable) {
99692
99840
  for (const [child, next] of Object.entries(value)) walk(next, [...path2, child], id, childSchema(schema, child));
99693
99841
  if (namespace === "llm-pi-ai" && field === "providers") {
99694
99842
  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 +100747,9 @@ var RuntimeController = class _RuntimeController {
100599
100747
  settingsOpen = false;
100600
100748
  settingsRows = [];
100601
100749
  settingsSelectedIndex = 0;
100750
+ /** Selection changes inside one input burst share one settings notification. */
100751
+ settingsMovePending = false;
100752
+ settingsMoveEmit;
100602
100753
  settingsNavigation = new SettingsNavigation([]);
100603
100754
  settingsSearching = false;
100604
100755
  settingsEditing = false;
@@ -103523,7 +103674,8 @@ ${line5}`.slice(-64e3);
103523
103674
  Math.max(0, this.settingsSelectedIndex + action.delta)
103524
103675
  );
103525
103676
  this.settingsNavigation.selectedIndex = this.settingsSelectedIndex;
103526
- this.emit();
103677
+ this.settingsPaneSnapshot = void 0;
103678
+ this.scheduleSettingsSelectionEmit();
103527
103679
  return;
103528
103680
  case "settings-edit":
103529
103681
  case "settings-edit-json":
@@ -106857,6 +107009,8 @@ ${row.hint ?? ""}` });
106857
107009
  notifyListeners() {
106858
107010
  this.emitLastRun = Date.now();
106859
107011
  this.emitPending = false;
107012
+ this.settingsMovePending = false;
107013
+ this.settingsMoveEmit = void 0;
106860
107014
  for (const listener of this.listeners) {
106861
107015
  try {
106862
107016
  listener();
@@ -106865,8 +107019,27 @@ ${row.hint ?? ""}` });
106865
107019
  }
106866
107020
  }
106867
107021
  }
107022
+ /**
107023
+ * Publish only the settings selection. A wheel or key burst keeps the latest
107024
+ * index readable immediately and notifies subscribers once per turn.
107025
+ */
107026
+ scheduleSettingsSelectionEmit() {
107027
+ if (this.closed) return;
107028
+ this.settingsMovePending = true;
107029
+ if (this.settingsMoveEmit !== void 0) return;
107030
+ const token = queueMicrotask(() => {
107031
+ if (this.settingsMoveEmit !== token || this.closed || !this.settingsMovePending) return;
107032
+ this.settingsMoveEmit = void 0;
107033
+ this.settingsMovePending = false;
107034
+ if (this.emitTimer !== void 0) this.emitPending = true;
107035
+ else this.notifyListeners();
107036
+ });
107037
+ this.settingsMoveEmit = token;
107038
+ }
106868
107039
  emit() {
106869
107040
  if (this.closed) return;
107041
+ this.settingsMovePending = false;
107042
+ this.settingsMoveEmit = void 0;
106870
107043
  this.modelSnapshot = void 0;
106871
107044
  this.sessionPaneSnapshot = void 0;
106872
107045
  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.53",
4
4
  "description": "DeepSeek Harness terminal interface: a dsh plugin bundle plus its launcher",
5
5
  "type": "module",
6
6
  "license": "MIT",