@deepseek-harness-tui/dsh-tui 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/dsh-tui.js +26 -4
- package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -13
- package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -15
- package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -32
- package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -166
- package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -42
- package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -143
- package/dsh-ecosystem-spec/registry/README.md +14 -14
- package/dsh-ecosystem-spec/registry/host-descriptor.tui.example.json +56 -56
- package/dsh-ecosystem-spec/registry/permissions-0.1.json +13 -13
- package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -149
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +7 -8
- package/lib/types/components/BalanceReportRow.d.ts +24 -0
- package/lib/types/components/BalanceReportRow.d.ts.map +1 -0
- package/lib/types/components/BalanceReportRow.js +87 -0
- package/lib/types/components/HistorySearchDialog.js +1 -1
- package/lib/types/components/MessageList.d.ts +3 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +11 -7
- package/lib/types/components/PluginSceneBoundary.d.ts +1 -1
- package/lib/types/components/PluginSceneBoundary.js +1 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +16 -0
- package/lib/types/components/Whale.d.ts +15 -4
- package/lib/types/components/Whale.d.ts.map +1 -1
- package/lib/types/components/Whale.js +9 -8
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +7 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +34 -4
- package/lib/types/deepseekBalance.d.ts +53 -0
- package/lib/types/deepseekBalance.d.ts.map +1 -0
- package/lib/types/deepseekBalance.js +86 -0
- package/lib/types/deepseekPricing.d.ts +82 -0
- package/lib/types/deepseekPricing.d.ts.map +1 -0
- package/lib/types/deepseekPricing.js +133 -0
- package/lib/types/dsh-adapter/channel.d.ts +46 -1
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +84 -10
- package/lib/types/dsh-adapter/index.d.ts +5 -0
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +1 -0
- package/lib/types/dsh-adapter/packaged-skills.d.ts +9 -0
- package/lib/types/dsh-adapter/packaged-skills.d.ts.map +1 -1
- package/lib/types/dsh-adapter/packaged-skills.js +19 -4
- package/lib/types/dsh-adapter/plugin.d.ts +6 -0
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +122 -18
- package/lib/types/dsh-adapter/settings-sections.d.ts +1 -0
- package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -1
- package/lib/types/dsh-adapter/settings-sections.js +35 -2
- package/lib/types/history.d.ts +4 -3
- package/lib/types/history.d.ts.map +1 -1
- package/lib/types/history.js +66 -18
- package/lib/types/i18n.d.ts +122 -55
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +39 -18
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +67 -17
- package/lib/types/ink/root.d.ts +16 -0
- package/lib/types/ink/root.d.ts.map +1 -1
- package/lib/types/ink/root.js +2 -0
- package/lib/types/ink/terminal-querier.d.ts +8 -0
- package/lib/types/ink/terminal-querier.d.ts.map +1 -1
- package/lib/types/ink/terminal-querier.js +13 -0
- package/lib/types/ink/terminal.d.ts +42 -0
- package/lib/types/ink/terminal.d.ts.map +1 -1
- package/lib/types/ink/terminal.js +52 -2
- package/lib/types/plugin-spec/registry.d.ts +1 -1
- package/lib/types/plugin-spec/registry.js +1 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +72 -37
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
- package/lib/types/screens/SessionBrowser.js +28 -13
- package/lib/types/screens/SessionTree.d.ts.map +1 -1
- package/lib/types/screens/SessionTree.js +33 -9
- package/lib/types/screens/Settings.d.ts +7 -5
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +206 -99
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +32 -1
- package/lib/types/sessionHistory.d.ts +11 -0
- package/lib/types/sessionHistory.d.ts.map +1 -1
- package/lib/types/sessionHistory.js +29 -0
- package/lib/types/sessions/format.d.ts.map +1 -1
- package/lib/types/sessions/format.js +14 -4
- package/lib/types/tips.js +2 -2
- package/lib/types/tuiDisplayPrefs.d.ts +3 -0
- package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
- package/lib/types/tuiDisplayPrefs.js +1 -0
- package/lib/types/update.d.ts +46 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +232 -3
- package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +21 -21
- package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +44 -44
- package/node_modules/@dsh-std/command/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/command/LICENSE +21 -21
- package/node_modules/@dsh-std/command/README.md +26 -26
- package/node_modules/@dsh-std/command/README.zh.md +28 -28
- package/node_modules/@dsh-std/command/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/command/package.json +22 -22
- package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/node_modules/@dsh-std/connection/README.md +42 -42
- package/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/node_modules/@dsh-std/connection/package.json +20 -20
- package/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/core/LICENSE +21 -21
- package/node_modules/@dsh-std/core/README.md +13 -13
- package/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/core/package.json +21 -21
- package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/node_modules/@dsh-std/manifest/README.md +5 -5
- package/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/manifest/package.json +20 -20
- package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/messages/LICENSE +21 -21
- package/node_modules/@dsh-std/messages/README.md +5 -5
- package/node_modules/@dsh-std/messages/README.zh.md +7 -7
- package/node_modules/@dsh-std/messages/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/messages/package.json +20 -20
- package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -54
- package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/node_modules/@dsh-std/presentation/README.md +27 -27
- package/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/presentation/package.json +25 -25
- package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/storage/LICENSE +21 -21
- package/node_modules/@dsh-std/storage/README.md +5 -5
- package/node_modules/@dsh-std/storage/README.zh.md +7 -7
- package/node_modules/@dsh-std/storage/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/storage/package.json +20 -20
- package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -56
- package/package.json +95 -5
- package/skills/pr-comments/SKILL.md +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -27
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -25
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -21
|
@@ -8,6 +8,7 @@ import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js';
|
|
|
8
8
|
import { isMod, isPlainReturn, modLabel } from '../utils/modifiers.js';
|
|
9
9
|
import { spreadRow, tailWidth, truncateWidth, wrapWidth } from '../sessions/format.js';
|
|
10
10
|
import { stringWidth } from '../ink/stringWidth.js';
|
|
11
|
+
import { TICK, MULTIPLICATION_X } from '../cc/figures.js';
|
|
11
12
|
import { t } from '../i18n.js';
|
|
12
13
|
import { droppedTurnInfo, filterTree, flattenTree, nearestVisibleIndex, TREE_FILTERS, } from '../dsh-adapter/sessionTree.js';
|
|
13
14
|
/** Local HH:MM (or MM/DD HH:MM when older than this year). */
|
|
@@ -95,10 +96,19 @@ function clampPrefix(prefix, budget) {
|
|
|
95
96
|
return '…';
|
|
96
97
|
return `…${tailWidth(prefix, budget - 1)}`;
|
|
97
98
|
}
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Rows the layout cannot do without: the header, the bordered search card
|
|
101
|
+
* (three rows), the notice slot, and the hints. Everything else — the rule,
|
|
102
|
+
* the tree itself — yields before these do.
|
|
103
|
+
*/
|
|
104
|
+
const MANDATORY_LINES = 6;
|
|
105
|
+
/**
|
|
106
|
+
* The one horizontal rule, above the hints. The search card's own border
|
|
107
|
+
* separates it from the header and the tree (the two rules that used to
|
|
108
|
+
* flank it are gone); the rule that remains lifts the hints off the content —
|
|
109
|
+
* decoration, and the first thing a short terminal drops.
|
|
110
|
+
*/
|
|
111
|
+
const RULE_PRIORITY = [2];
|
|
102
112
|
/** Width from which the preview pane sits beside the list instead of below. */
|
|
103
113
|
const SPLIT_MIN_COLUMNS = 100;
|
|
104
114
|
/** Lines the bottom preview block may take on narrow terminals. */
|
|
@@ -347,6 +357,16 @@ export function SessionTree({ channel, currentSessionId, onClose, onRestoreText,
|
|
|
347
357
|
focusIndexRef.current = at;
|
|
348
358
|
setSelectedId(list[at]?.node.id ?? null);
|
|
349
359
|
};
|
|
360
|
+
/**
|
|
361
|
+
* Mouse wheel over the tree walks the cursor (cursor-centered window, so
|
|
362
|
+
* rolling IS scrolling; the hover-driven preview rides along). The menu /
|
|
363
|
+
* confirm seats keep the keyboard as sole owner.
|
|
364
|
+
*/
|
|
365
|
+
const handleWheel = (event) => {
|
|
366
|
+
if (seat !== 'tree')
|
|
367
|
+
return;
|
|
368
|
+
step(event.deltaY >= 0 ? 1 : -1, 1, false);
|
|
369
|
+
};
|
|
350
370
|
useInput((input, key) => {
|
|
351
371
|
// While an action is in flight the seat swallows every key — closing now
|
|
352
372
|
// would drop the user back into a half-swapped session.
|
|
@@ -437,7 +457,9 @@ export function SessionTree({ channel, currentSessionId, onClose, onRestoreText,
|
|
|
437
457
|
const previewBlock = seat === 'tree' && previewEntry !== null && columns < SPLIT_MIN_COLUMNS && hoverId !== null
|
|
438
458
|
? PREVIEW_BLOCK_LINES
|
|
439
459
|
: 0;
|
|
440
|
-
|
|
460
|
+
// The notice slot is mandatory (permanent): a rewind/fork report must never
|
|
461
|
+
// shift the tree by arriving.
|
|
462
|
+
const extraLines = menuLines + confirmLines + previewBlock;
|
|
441
463
|
const ruleBudget = Math.max(0, Math.min(RULE_PRIORITY.length, rows - MANDATORY_LINES - extraLines));
|
|
442
464
|
const rules = new Set(RULE_PRIORITY.slice(0, ruleBudget));
|
|
443
465
|
const listHeight = Math.max(0, rows - MANDATORY_LINES - extraLines - rules.size);
|
|
@@ -465,17 +487,19 @@ export function SessionTree({ channel, currentSessionId, onClose, onRestoreText,
|
|
|
465
487
|
: fitHint([t('tree-hint', { mod: modLabel }), t('tree-hint-short')], inputBudget);
|
|
466
488
|
// ── Loading / busy seats ────────────────────────────────────────────────
|
|
467
489
|
if (loading || busy) {
|
|
468
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: "remember", bold: true, children: truncateWidth(` ${t('tree-title')}`, inputBudget) }) }),
|
|
490
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: "remember", bold: true, children: truncateWidth(` ${t('tree-title')}`, inputBudget) }) }), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { dimColor: true, italic: true, children: ` ${truncateWidth(busy ? t('tree-rewinding') : t('tree-loading'), inputBudget)}` }) })] }));
|
|
469
491
|
}
|
|
470
492
|
// ── Render ──────────────────────────────────────────────────────────────
|
|
471
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: "remember", bold: true, children: header.left }), _jsx(Text, { dimColor: true, children: `${' '.repeat(header.gap)}${header.right}` })] }),
|
|
493
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: "remember", bold: true, children: header.left }), _jsx(Text, { dimColor: true, children: `${' '.repeat(header.gap)}${header.right}` })] }), _jsx(Box, { flexShrink: 0, children: _jsx(SearchBox, { query: tailWidth(query, inputBudget), isFocused: seat === 'tree', isTerminalFocused: isTerminalFocused, placeholder: truncateWidth(t('tree-search'), inputBudget) }) }), _jsxs("ink-box", { style: { flexDirection: 'row', flexGrow: 1, flexShrink: 1, overflow: 'hidden' }, onWheel: handleWheel, children: [_jsxs(Box, { flexDirection: "column", width: listWidth, height: listHeight, flexShrink: 0, children: [visible.length === 0 && (_jsx(Text, { dimColor: true, italic: true, children: ` ${truncateWidth(t('tree-empty'), Math.max(0, listWidth - 2))}` })), windowed.map((flatNode, index) => (_jsx(TreeRow, { flatNode: flatNode, meta: data?.sessions.get(flatNode.node.sessionId), liveSessionId: currentSessionId, width: listWidth, focused: windowStart + index === focusIndex, onActivePath: data?.activePath.has(flatNode.node.id) === true, onHover: setHoverId, onClick: () => {
|
|
472
494
|
// Click = focus + open the action menu (the mouse path to
|
|
473
495
|
// every action this screen offers); the menu's own rows
|
|
474
496
|
// execute.
|
|
475
497
|
setSelectedId(flatNode.node.id);
|
|
476
498
|
setMenuNodeId(flatNode.node.id);
|
|
477
499
|
setMenuIndex(0);
|
|
478
|
-
} }, flatNode.node.id)))] }), splitPreview && previewEntry !== null && (_jsx(PreviewPane, { entry: previewEntry, meta: data?.sessions.get(previewEntry.sessionId), width: previewWidth, height: listHeight, liveSessionId: currentSessionId, now: Date.now() }))] }), previewBlock > 0 && previewEntry !== null && (_jsx(PreviewPane, { entry: previewEntry, meta: data?.sessions.get(previewEntry.sessionId), width: columns, height: previewBlock, liveSessionId: currentSessionId, now: Date.now(), compact: true })),
|
|
500
|
+
} }, flatNode.node.id)))] }), splitPreview && previewEntry !== null && (_jsx(PreviewPane, { entry: previewEntry, meta: data?.sessions.get(previewEntry.sessionId), width: previewWidth, height: listHeight, liveSessionId: currentSessionId, now: Date.now() }))] }), previewBlock > 0 && previewEntry !== null && (_jsx(PreviewPane, { entry: previewEntry, meta: data?.sessions.get(previewEntry.sessionId), width: columns, height: previewBlock, liveSessionId: currentSessionId, now: Date.now(), compact: true })), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: notice?.tone === 'error' ? 'error' : 'success', children: notice === undefined
|
|
501
|
+
? ' '
|
|
502
|
+
: ` ${notice.tone === 'error' ? MULTIPLICATION_X : TICK} ${truncateWidth(notice.text, inputBudget - 2)}` }) }), seat === 'menu' && menuEntry !== null && (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: "remember", children: ` ${truncateWidth(`❰ ${menuEntry.text}`, inputBudget)}` }) }), options.map((option, index) => (_jsx(MenuOptionRow, { option: option, highlighted: index === menuIndex, width: inputBudget, onClick: () => {
|
|
479
503
|
setMenuIndex(index);
|
|
480
504
|
runMenuOption(option);
|
|
481
505
|
}, onHover: () => setMenuIndex(index) }, option.id)))] })), seat === 'confirm' && confirmState !== null && (_jsxs(Box, { flexShrink: 0, flexDirection: "column",
|
|
@@ -484,7 +508,7 @@ export function SessionTree({ channel, currentSessionId, onClose, onRestoreText,
|
|
|
484
508
|
? t('tree-confirm-adopt', { text: confirmState.entry.text })
|
|
485
509
|
: confirmState.droppedEntries !== undefined && confirmState.droppedEntries > 0
|
|
486
510
|
? t('tree-confirm-drop', { text: confirmState.entry.text, n: confirmState.droppedEntries })
|
|
487
|
-
: t('tree-confirm-rewind', { text: confirmState.entry.text }), inputBudget)}` }), confirmState.dropsBranch === true && (_jsx(Text, { color: "error", children: ` ${truncateWidth(t('tree-confirm-drops-branch'), inputBudget)}` }))] })), rules.has(
|
|
511
|
+
: t('tree-confirm-rewind', { text: confirmState.entry.text }), inputBudget)}` }), confirmState.dropsBranch === true && (_jsx(Text, { color: "error", children: ` ${truncateWidth(t('tree-confirm-drops-branch'), inputBudget)}` }))] })), rules.has(2) && (_jsx(Box, { flexShrink: 0, children: _jsx(Divider, { width: columns }) })), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: hint }) }) })] }));
|
|
488
512
|
}
|
|
489
513
|
/** Index wrap helper for the menu cursor. */
|
|
490
514
|
function wrapIndex(current, by, count) {
|
|
@@ -6,12 +6,14 @@ import type { Channel } from '../dsh-adapter/channel.js';
|
|
|
6
6
|
* The TUI owns only presentation here: plugin-declared sections from the
|
|
7
7
|
* `tuiSettingsSections` seam render as editable forms; every write goes back
|
|
8
8
|
* through the dsh settings service (revision-fenced `mutate` path ops) or the
|
|
9
|
-
* credentials seam (secret fields, blank-until-typed).
|
|
10
|
-
* declared a section for stay read-only with a YAML hint — the same fallback
|
|
11
|
-
* the web front door gives namespaces without a card.
|
|
9
|
+
* credentials seam (secret fields, blank-until-typed).
|
|
12
10
|
*
|
|
13
|
-
* Edits
|
|
14
|
-
*
|
|
11
|
+
* Edits auto-save: toggling a boolean/select writes on the spot, confirming a
|
|
12
|
+
* text draft writes on Enter, and Esc just leaves — there is no save key and
|
|
13
|
+
* nothing to discard. The SettingsForm still stages drafts under the hood (its
|
|
14
|
+
* snapshot save keeps rapid successive edits serialized; see
|
|
15
|
+
* settingsEditor.ts), so the brief in-flight window and a failed save keep
|
|
16
|
+
* their `*` / badge feedback.
|
|
15
17
|
*/
|
|
16
18
|
export declare function Settings({ channel, onClose, }: {
|
|
17
19
|
channel: Channel;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Settings.d.ts","sourceRoot":"","sources":["../../../src/screens/Settings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"Settings.d.ts","sourceRoot":"","sources":["../../../src/screens/Settings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAczB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAuPxD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EACP,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CAkgBlB"}
|
|
@@ -1,26 +1,84 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text, useInput, useTerminalSize } from '../ui.js';
|
|
4
4
|
import { Divider } from '../components/design-system/Divider.js';
|
|
5
5
|
import { HintLine } from '../components/design-system/HintLine.js';
|
|
6
6
|
import { isMod, isPlainReturn } from '../utils/modifiers.js';
|
|
7
7
|
import { truncateWidth } from '../sessions/format.js';
|
|
8
|
+
import { stringWidth } from '../ink/stringWidth.js';
|
|
9
|
+
import { POINTER, TICK, MULTIPLICATION_X } from '../cc/figures.js';
|
|
8
10
|
import { getLang, t } from '../i18n.js';
|
|
9
11
|
import { SettingsForm } from '../dsh-adapter/settingsEditor.js';
|
|
10
12
|
/** Pick the provider-owned translation for the active language. */
|
|
11
13
|
function pick(text, descriptions) {
|
|
12
14
|
return descriptions?.[getLang()] ?? text;
|
|
13
15
|
}
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Pad a glyph to a fixed display width.
|
|
18
|
+
*
|
|
19
|
+
* CJK terminal fonts commonly render symbols like `❯`/`✓` (U+276F/U+2713)
|
|
20
|
+
* a full cell wide where Latin fonts render them narrow — a marker column
|
|
21
|
+
* built on raw characters then shifts every row's content by one cell when
|
|
22
|
+
* the focused glyph swaps in. Padding with `stringWidth` (display columns,
|
|
23
|
+
* not string length) pins the column to `cols` cells on either font, so
|
|
24
|
+
* rows never move when focus changes.
|
|
25
|
+
*/
|
|
26
|
+
function padTo(glyph, cols) {
|
|
27
|
+
const width = stringWidth(glyph);
|
|
28
|
+
return width >= cols ? glyph : glyph + ' '.repeat(cols - width);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One editable field row. Always exactly one line tall — the value column
|
|
32
|
+
* stays flush right (badges attach to its left), and the field's hint lives
|
|
33
|
+
* in the bottom help bar instead of a second row, so moving the focus never
|
|
34
|
+
* reflows the list. 行高亮背景由外层 CardRow 负责,本组件只管文字与 chip。
|
|
35
|
+
*/
|
|
36
|
+
function FieldRow({ label, kind, value, selectLabel, focused, editing, invalid, staged, onClick, onMouseEnter, }) {
|
|
37
|
+
const booleanChip = kind === 'boolean' && (value === 'true' || value === 'false');
|
|
38
|
+
return (_jsxs(Box, { flexDirection: "row", height: 1, flexShrink: 0, overflow: "hidden", onClick: onClick, onMouseEnter: onMouseEnter, children: [_jsx(Text, { color: focused ? 'suggestion' : undefined, children: focused ? padTo(POINTER, 2) : ' ' }), _jsx(Text, { bold: focused, children: label }), _jsx(Box, { flexGrow: 1 }), invalid && _jsxs(Text, { color: "error", children: [t('settings-field-invalid'), " "] }), staged && !editing && _jsx(Text, { color: "suggestion", children: "* " }), booleanChip ? (_jsx(Text, { color: value === 'true' ? 'success' : 'inactive', children: value === 'true' ? `[${padTo(TICK, 2)}]` : '[ ]' })) : selectLabel !== undefined ? (_jsxs(Text, { children: [_jsx(Text, { color: focused ? 'suggestion' : 'subtle', children: '‹ ' }), _jsx(Text, { color: focused ? 'suggestion' : undefined, children: selectLabel }), _jsx(Text, { color: focused ? 'suggestion' : 'subtle', children: ' ›' })] })) : (_jsx(Text, { color: editing || focused ? 'suggestion' : undefined, dimColor: !focused && !editing && value === '', children: value }))] }));
|
|
39
|
+
}
|
|
40
|
+
/** A group navigation row — opens the group's subpage on Enter/click. */
|
|
41
|
+
function GroupRow({ title, focused, onClick, onMouseEnter, }) {
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "row", height: 1, flexShrink: 0, overflow: "hidden", onClick: onClick, onMouseEnter: onMouseEnter, children: [_jsx(Text, { color: focused ? 'suggestion' : undefined, children: focused ? padTo(POINTER, 2) : ' ' }), _jsx(Text, { bold: focused, children: title }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { color: focused ? 'suggestion' : 'subtle', children: '›' })] }));
|
|
43
|
+
}
|
|
44
|
+
/** 区块卡片的边框色(沿用输入框的 panel 边框 token)。 */
|
|
45
|
+
const CARD_BORDER = 'promptBorder';
|
|
46
|
+
/**
|
|
47
|
+
* 区块卡片的顶边:`╭─ 标题 (副标题) ────── [徽章] ╮`。
|
|
48
|
+
*
|
|
49
|
+
* 标题用区块主题色加粗、徽章保留各自的语义色(未保存/重启/失败),其余
|
|
50
|
+
* 边框线用卡片边框色。宽度按 stringWidth 精确计算(CJK 标题占 2 格),
|
|
51
|
+
* 极窄终端依次退化:先丢副标题、再丢徽章、最后截断标题。
|
|
52
|
+
*/
|
|
53
|
+
function CardTop({ title, subtitle, badges, columns, color = CARD_BORDER, }) {
|
|
54
|
+
const subtitleText = subtitle === undefined ? '' : ` (${subtitle})`;
|
|
55
|
+
let showSubtitle = subtitleText !== '';
|
|
56
|
+
let showBadges = badges.length > 0;
|
|
57
|
+
let titleText = title;
|
|
58
|
+
const badgesWidth = badges.reduce((sum, badge) => sum + stringWidth(`[${badge.text}]`), 0) + Math.max(0, badges.length - 1);
|
|
59
|
+
// 布局:'╭─ ' + 标题 + 副标题 + ' ' + 虚线 + [' ' + 徽章 + ' '] + '╮'
|
|
60
|
+
const used = (titleWidth, withSubtitle, withBadges) => 5 + titleWidth + (withSubtitle ? stringWidth(subtitleText) : 0) + (withBadges ? 2 + badgesWidth : 0);
|
|
61
|
+
if (used(stringWidth(titleText), showSubtitle, showBadges) + 1 > columns && showSubtitle)
|
|
62
|
+
showSubtitle = false;
|
|
63
|
+
if (used(stringWidth(titleText), showSubtitle, showBadges) + 1 > columns && showBadges)
|
|
64
|
+
showBadges = false;
|
|
65
|
+
if (used(stringWidth(titleText), showSubtitle, showBadges) + 1 > columns) {
|
|
66
|
+
titleText = truncateWidth(titleText, Math.max(4, columns - 6 - (showSubtitle ? stringWidth(subtitleText) : 0) - (showBadges ? 2 + badgesWidth : 0)));
|
|
22
67
|
}
|
|
23
|
-
|
|
68
|
+
const dashes = Math.max(0, columns - used(stringWidth(titleText), showSubtitle, showBadges));
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "row", height: 1, flexShrink: 0, overflow: "hidden", children: [_jsx(Text, { color: color, children: '╭─ ' }), _jsx(Text, { bold: true, color: "permission", children: titleText }), showSubtitle && _jsx(Text, { dimColor: true, children: subtitleText }), _jsx(Text, { color: color, children: ` ${'─'.repeat(dashes)}` }), showBadges && (_jsxs(_Fragment, { children: [_jsx(Text, { color: color, children: ' ' }), badges.map((badge, index) => (_jsxs(React.Fragment, { children: [index > 0 && _jsx(Text, { color: color, children: ' ' }), _jsx(Text, { color: badge.color, children: `[${badge.text}]` })] }, index))), _jsx(Text, { color: color, children: ' ' })] })), _jsx(Text, { color: color, children: '╮' })] }));
|
|
70
|
+
}
|
|
71
|
+
/** 区块卡片的底边:`╰──────╯`。 */
|
|
72
|
+
function CardBottom({ columns, color = CARD_BORDER }) {
|
|
73
|
+
return (_jsx(Text, { color: color, wrap: "truncate-end", children: `╰${'─'.repeat(Math.max(0, columns - 2))}╯` }));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 卡片内一行:`│` 左右边框 + 内容区。聚焦行的高亮背景涂在内容区上
|
|
77
|
+
* (padding 也在背景内),边框线本身不吃高亮——高亮条正好嵌在两道
|
|
78
|
+
* 竖线之间。行内容恒 1 行,与窗口化滚动逐行兼容(SuggestionCard 同族)。
|
|
79
|
+
*/
|
|
80
|
+
function CardRow({ children, highlight, color = CARD_BORDER, }) {
|
|
81
|
+
return (_jsxs(Box, { flexDirection: "row", height: 1, flexShrink: 0, overflow: "hidden", children: [_jsx(Text, { color: color, children: "\u2502" }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minWidth: 0, paddingX: 1, backgroundColor: highlight === true ? 'selectionBg' : undefined, children: children }), _jsx(Text, { color: color, children: "\u2502" })] }));
|
|
24
82
|
}
|
|
25
83
|
/**
|
|
26
84
|
* The settings screen — `/settings` as a screen of its own (issue #165).
|
|
@@ -28,12 +86,14 @@ function valuePreview(value, budget) {
|
|
|
28
86
|
* The TUI owns only presentation here: plugin-declared sections from the
|
|
29
87
|
* `tuiSettingsSections` seam render as editable forms; every write goes back
|
|
30
88
|
* through the dsh settings service (revision-fenced `mutate` path ops) or the
|
|
31
|
-
* credentials seam (secret fields, blank-until-typed).
|
|
32
|
-
* declared a section for stay read-only with a YAML hint — the same fallback
|
|
33
|
-
* the web front door gives namespaces without a card.
|
|
89
|
+
* credentials seam (secret fields, blank-until-typed).
|
|
34
90
|
*
|
|
35
|
-
* Edits
|
|
36
|
-
*
|
|
91
|
+
* Edits auto-save: toggling a boolean/select writes on the spot, confirming a
|
|
92
|
+
* text draft writes on Enter, and Esc just leaves — there is no save key and
|
|
93
|
+
* nothing to discard. The SettingsForm still stages drafts under the hood (its
|
|
94
|
+
* snapshot save keeps rapid successive edits serialized; see
|
|
95
|
+
* settingsEditor.ts), so the brief in-flight window and a failed save keep
|
|
96
|
+
* their `*` / badge feedback.
|
|
37
97
|
*/
|
|
38
98
|
export function Settings({ channel, onClose, }) {
|
|
39
99
|
// Explicit terminal size (not flexGrow) — the same rule SessionBrowser's
|
|
@@ -123,11 +183,22 @@ export function Settings({ channel, onClose, }) {
|
|
|
123
183
|
]);
|
|
124
184
|
const effFocus = Math.min(focusIndex, Math.max(0, focusable.length - 1));
|
|
125
185
|
const focused = focusable.length === 0 ? undefined : focusable[effFocus];
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Save a section's staged edits right now (auto-save: every confirmed edit
|
|
188
|
+
* writes immediately — no manual save key, Esc just leaves). A save already
|
|
189
|
+
* in flight chains: the pending marker re-runs the save once it settles, and
|
|
190
|
+
* the editor's snapshot semantics keep drafts staged mid-flight alive, so
|
|
191
|
+
* rapid toggles serialize instead of racing the revision fence.
|
|
192
|
+
*/
|
|
193
|
+
const pendingSaveRef = React.useRef(new Set());
|
|
194
|
+
const saveSoon = (ns) => {
|
|
195
|
+
const form = formsRef.current.get(ns);
|
|
196
|
+
if (form === undefined || !form.available || form.shell().invalid || !form.shell().dirty)
|
|
130
197
|
return;
|
|
198
|
+
if (form.saving) {
|
|
199
|
+
pendingSaveRef.current.add(ns);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
131
202
|
void form.save().then(ok => {
|
|
132
203
|
if (!mountedRef.current)
|
|
133
204
|
return;
|
|
@@ -139,12 +210,17 @@ export function Settings({ channel, onClose, }) {
|
|
|
139
210
|
else {
|
|
140
211
|
setNotice({ text: t('settings-save-failed', { ns }), tone: 'error' });
|
|
141
212
|
}
|
|
213
|
+
if (pendingSaveRef.current.delete(ns)) {
|
|
214
|
+
const next = formsRef.current.get(ns);
|
|
215
|
+
if (next !== undefined && next.shell().dirty)
|
|
216
|
+
saveSoon(ns);
|
|
217
|
+
}
|
|
142
218
|
});
|
|
143
219
|
};
|
|
144
|
-
/**
|
|
220
|
+
/** Cycle a boolean/select field and save the change immediately. */
|
|
145
221
|
const cycleField = (ns, field) => {
|
|
146
222
|
const form = forms.get(ns);
|
|
147
|
-
if (form === undefined || !form.available
|
|
223
|
+
if (form === undefined || !form.available)
|
|
148
224
|
return;
|
|
149
225
|
const current = form.field(field).text;
|
|
150
226
|
if (field.kind === 'boolean') {
|
|
@@ -157,8 +233,8 @@ export function Settings({ channel, onClose, }) {
|
|
|
157
233
|
const index = options.findIndex(option => option.value === current);
|
|
158
234
|
form.edit(field, options[(index + 1) % options.length]?.value ?? options[0]?.value ?? '');
|
|
159
235
|
}
|
|
160
|
-
setNotice(undefined);
|
|
161
236
|
bump();
|
|
237
|
+
saveSoon(ns);
|
|
162
238
|
};
|
|
163
239
|
/**
|
|
164
240
|
* Activate one focusable entry — the keyboard Enter path, shared with the
|
|
@@ -172,10 +248,8 @@ export function Settings({ channel, onClose, }) {
|
|
|
172
248
|
setWindowStart(0);
|
|
173
249
|
return;
|
|
174
250
|
}
|
|
175
|
-
// A save in flight owns the section's drafts; starting an edit mid-write
|
|
176
|
-
// is exactly the lost-draft race the staged model exists to prevent.
|
|
177
251
|
const form = forms.get(entry.ns);
|
|
178
|
-
if (form === undefined || !form.available
|
|
252
|
+
if (form === undefined || !form.available)
|
|
179
253
|
return;
|
|
180
254
|
if (entry.field.kind === 'boolean' || entry.field.kind === 'select') {
|
|
181
255
|
cycleField(entry.ns, entry.field);
|
|
@@ -196,11 +270,21 @@ export function Settings({ channel, onClose, }) {
|
|
|
196
270
|
useInput((input, key) => {
|
|
197
271
|
if (mode === 'edit' && editing !== null) {
|
|
198
272
|
if (isPlainReturn(key)) {
|
|
273
|
+
// Confirm the draft: an invalid value keeps the editor open with the
|
|
274
|
+
// error badge and a toast; a valid one saves immediately (auto-save).
|
|
199
275
|
const form = forms.get(editing.ns);
|
|
200
|
-
form
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
276
|
+
if (form !== undefined) {
|
|
277
|
+
form.edit(editing.field, editing.draft);
|
|
278
|
+
if (form.field(editing.field).invalid) {
|
|
279
|
+
setNotice({ text: t('settings-field-invalid'), tone: 'error' });
|
|
280
|
+
bump();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
bump();
|
|
284
|
+
setMode('list');
|
|
285
|
+
setEditing(null);
|
|
286
|
+
saveSoon(editing.ns);
|
|
287
|
+
}
|
|
204
288
|
}
|
|
205
289
|
else if (key.escape) {
|
|
206
290
|
setMode('list');
|
|
@@ -227,40 +311,17 @@ export function Settings({ channel, onClose, }) {
|
|
|
227
311
|
else if (isPlainReturn(key) && focused !== undefined) {
|
|
228
312
|
activateEntry(focused);
|
|
229
313
|
}
|
|
230
|
-
else if (input === 's' && focused !== undefined) {
|
|
231
|
-
commitSave(focused.ns);
|
|
232
|
-
}
|
|
233
|
-
else if (input === 'd' && focused !== undefined) {
|
|
234
|
-
if (focusedForm?.saving)
|
|
235
|
-
return;
|
|
236
|
-
focusedForm?.discard();
|
|
237
|
-
setNotice(undefined);
|
|
238
|
-
bump();
|
|
239
|
-
}
|
|
240
314
|
else if (key.escape) {
|
|
241
315
|
if (activeGroupSpec !== undefined) {
|
|
242
|
-
// Group navigation
|
|
316
|
+
// Group navigation only unwinds the page; edits save as they land.
|
|
243
317
|
setActiveGroup(null);
|
|
244
318
|
setFocusIndex(0);
|
|
245
319
|
setWindowStart(0);
|
|
246
320
|
return;
|
|
247
321
|
}
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
// discarding around it.
|
|
252
|
-
const dirty = [...forms.values()].filter(form => form.shell().dirty);
|
|
253
|
-
if (dirty.length > 0) {
|
|
254
|
-
if (dirty.some(form => form.saving))
|
|
255
|
-
return;
|
|
256
|
-
for (const form of dirty)
|
|
257
|
-
form.discard();
|
|
258
|
-
setNotice({ text: t('settings-discarded'), tone: 'success' });
|
|
259
|
-
bump();
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
onClose();
|
|
263
|
-
}
|
|
322
|
+
// Auto-save means there is nothing left to discard: Esc leaves. A save
|
|
323
|
+
// still in flight was already dispatched — it settles on its own.
|
|
324
|
+
onClose();
|
|
264
325
|
}
|
|
265
326
|
});
|
|
266
327
|
const renderField = (section, field, focus) => {
|
|
@@ -270,7 +331,6 @@ export function Settings({ channel, onClose, }) {
|
|
|
270
331
|
const isFocused = focused?.kind === 'field' && focused.ns === ns && focused.field === field;
|
|
271
332
|
const isEditing = isFocused && mode === 'edit' && editing !== null;
|
|
272
333
|
const label = pick(field.label, field.descriptions);
|
|
273
|
-
const hint = field.hint !== undefined ? pick(field.hint, field.hintDescriptions) : undefined;
|
|
274
334
|
// 鼠标:编辑态整屏不响应(草稿由键盘独占,防误触抢焦点);列表态
|
|
275
335
|
// 悬停即移动焦点(lazygit 语义——焦点就是选中),点击 = 焦点 + 该行
|
|
276
336
|
// 的 Enter 动作(boolean/select 循环值,文本/secret 进编辑态)。
|
|
@@ -307,40 +367,75 @@ export function Settings({ channel, onClose, }) {
|
|
|
307
367
|
else {
|
|
308
368
|
value = state.text;
|
|
309
369
|
}
|
|
310
|
-
|
|
370
|
+
// select 的值渲染成按钮 chip:用选项的本地化文案('zh' → '中文')而不是
|
|
371
|
+
// 存储的原始值;没命中选项时退回原始值。仅在非编辑态、值非空时提供。
|
|
372
|
+
const selectLabel = field.kind === 'select' && !isEditing && state.text !== ''
|
|
373
|
+
? (() => {
|
|
374
|
+
const option = field.options?.find(entry => entry.value === state.text);
|
|
375
|
+
return option === undefined ? state.text : pick(option.label, option.descriptions);
|
|
376
|
+
})()
|
|
377
|
+
: undefined;
|
|
378
|
+
return (_jsx(FieldRow, { label: label, kind: field.kind, value: value, selectLabel: selectLabel, focused: isFocused, editing: isEditing, invalid: state.invalid, staged: form?.isStaged(field) === true, onClick: rowEvents?.onClick, onMouseEnter: rowEvents?.onMouseEnter }));
|
|
311
379
|
};
|
|
312
380
|
// ── Layout: a flat entry list with accounted line heights, windowed so the
|
|
313
|
-
// focused row is always on screen no matter how long the current page gets.
|
|
381
|
+
// focused row is always on screen no matter how long the current page gets.
|
|
382
|
+
// Each section renders as a bordered card (╭─ title ─╮ / │ rows │ / ╰──╯);
|
|
383
|
+
// borders are their own entries so windowing can cut a long card anywhere. ─
|
|
314
384
|
const entries = [];
|
|
315
385
|
let focusCursor = 0;
|
|
386
|
+
/** Badges for a section's form, rendered on the card's top border. */
|
|
387
|
+
const sectionBadges = (section) => {
|
|
388
|
+
const form = forms.get(section.ns);
|
|
389
|
+
const view = form?.namespace;
|
|
390
|
+
const shell = form?.shell();
|
|
391
|
+
const badges = [];
|
|
392
|
+
if (view === undefined)
|
|
393
|
+
badges.push({ text: t('settings-section-unavailable'), color: 'warning' });
|
|
394
|
+
if (view?.applies === 'restart')
|
|
395
|
+
badges.push({ text: t('settings-badge-restart'), color: 'warning' });
|
|
396
|
+
if (shell?.dirty === true)
|
|
397
|
+
badges.push({ text: t('settings-badge-dirty'), color: 'suggestion' });
|
|
398
|
+
if (shell?.saving === true)
|
|
399
|
+
badges.push({ text: t('settings-badge-saving'), color: 'inactive' });
|
|
400
|
+
if (shell?.failed === true)
|
|
401
|
+
badges.push({ text: t('settings-badge-failed'), color: 'error' });
|
|
402
|
+
return badges;
|
|
403
|
+
};
|
|
316
404
|
const addField = (section, field) => {
|
|
317
|
-
const isFocused = focused?.kind === 'field' && focused.ns === section.ns && focused.field === field;
|
|
318
405
|
const index = focusCursor;
|
|
319
406
|
focusCursor += 1;
|
|
407
|
+
const isFocused = focused?.kind === 'field' && focused.ns === section.ns && focused.field === field;
|
|
408
|
+
// Field rows are always exactly one line: the hint lives in the bottom
|
|
409
|
+
// help bar, so focusing a field never reflows the list below it.
|
|
320
410
|
entries.push({
|
|
321
411
|
key: `field:${section.ns}:${field.path.join('.')}`,
|
|
322
|
-
lines:
|
|
412
|
+
lines: 1,
|
|
323
413
|
focus: index,
|
|
324
|
-
node: renderField(section, field, index),
|
|
414
|
+
node: _jsx(CardRow, { highlight: isFocused, children: renderField(section, field, index) }),
|
|
325
415
|
});
|
|
326
416
|
};
|
|
327
417
|
if (activeSection !== undefined && activeGroupSpec !== undefined) {
|
|
328
418
|
const groupFields = activeSection.fields.filter(field => field.group === activeGroupSpec.id);
|
|
419
|
+
entries.push({
|
|
420
|
+
key: 'card:group:top',
|
|
421
|
+
lines: 1,
|
|
422
|
+
node: (_jsx(CardTop, { title: pick(activeGroupSpec.title, activeGroupSpec.descriptions), subtitle: pick(activeSection.title, activeSection.descriptions), badges: sectionBadges(activeSection), columns: columns })),
|
|
423
|
+
});
|
|
329
424
|
for (const field of groupFields)
|
|
330
425
|
addField(activeSection, field);
|
|
331
426
|
if (groupFields.length === 0) {
|
|
332
|
-
entries.push({ key: 'group:empty', lines: 1, node: _jsx(Text, { dimColor: true, children: t('settings-group-empty') }) });
|
|
427
|
+
entries.push({ key: 'group:empty', lines: 1, node: _jsx(CardRow, { children: _jsx(Text, { dimColor: true, children: t('settings-group-empty') }) }) });
|
|
333
428
|
}
|
|
429
|
+
entries.push({ key: 'card:group:bottom', lines: 1, node: _jsx(CardBottom, { columns: columns }) });
|
|
334
430
|
}
|
|
335
431
|
else {
|
|
336
432
|
sections.forEach((section, sectionIndex) => {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const shell = form?.shell();
|
|
433
|
+
if (sectionIndex > 0)
|
|
434
|
+
entries.push({ key: `gap:${section.ns}`, lines: 1, node: _jsx(Text, { children: " " }) });
|
|
340
435
|
entries.push({
|
|
341
|
-
key: `
|
|
342
|
-
lines:
|
|
343
|
-
node: (
|
|
436
|
+
key: `card:${section.ns}:top`,
|
|
437
|
+
lines: 1,
|
|
438
|
+
node: (_jsx(CardTop, { title: pick(section.title, section.descriptions), subtitle: section.ns, badges: sectionBadges(section), columns: columns })),
|
|
344
439
|
});
|
|
345
440
|
for (const field of section.fields) {
|
|
346
441
|
if (field.group === undefined)
|
|
@@ -365,32 +460,14 @@ export function Settings({ channel, onClose, }) {
|
|
|
365
460
|
key: `group:${section.ns}:${group.id}`,
|
|
366
461
|
lines: 1,
|
|
367
462
|
focus: index,
|
|
368
|
-
node: (
|
|
463
|
+
node: (_jsx(CardRow, { highlight: isFocused, children: _jsx(GroupRow, { title: pick(group.title, group.descriptions), focused: isFocused, onClick: groupRowEvents?.onClick, onMouseEnter: groupRowEvents?.onMouseEnter }) })),
|
|
369
464
|
});
|
|
370
465
|
}
|
|
466
|
+
entries.push({ key: `card:${section.ns}:bottom`, lines: 1, node: _jsx(CardBottom, { columns: columns }) });
|
|
371
467
|
});
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (
|
|
375
|
-
entries.push({
|
|
376
|
-
key: 'readonly:heading',
|
|
377
|
-
lines: 2,
|
|
378
|
-
node: (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, dimColor: true, children: t('settings-readonly-heading') })] })),
|
|
379
|
-
});
|
|
380
|
-
for (const entry of readonlyNamespaces) {
|
|
381
|
-
entries.push({
|
|
382
|
-
key: `readonly:${entry.ns}`,
|
|
383
|
-
lines: 1,
|
|
384
|
-
node: (_jsxs(Box, { children: [_jsxs(Text, { children: [' ', entry.ns] }), entry.applies === 'restart' && _jsxs(Text, { color: "warning", children: [" [", t('settings-badge-restart'), "]"] }), _jsxs(Text, { dimColor: true, children: [' ', valuePreview(entry.value, 60)] })] })),
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
entries.push({
|
|
388
|
-
key: 'readonly:hint',
|
|
389
|
-
lines: 1,
|
|
390
|
-
node: _jsxs(Text, { dimColor: true, children: [' ', t('settings-readonly-hint', { path: '~/.dsh/settings.yaml' })] }),
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
if (sections.length === 0 && readonlyNamespaces.length === 0) {
|
|
468
|
+
// Namespaces without a plugin-declared section are deliberately NOT
|
|
469
|
+
// listed (they used to get a read-only YAML hint; user feedback: noise).
|
|
470
|
+
if (sections.length === 0) {
|
|
394
471
|
entries.push({
|
|
395
472
|
key: 'empty',
|
|
396
473
|
lines: 1,
|
|
@@ -409,8 +486,10 @@ export function Settings({ channel, onClose, }) {
|
|
|
409
486
|
}
|
|
410
487
|
totalLines += entry.lines;
|
|
411
488
|
}
|
|
412
|
-
// Chrome: title row,
|
|
413
|
-
|
|
489
|
+
// Chrome: title row, footer rule, notice slot, help row. The notice slot is
|
|
490
|
+
// permanent (blank while quiet) so a save/discard toast never shifts the
|
|
491
|
+
// list above it.
|
|
492
|
+
const viewport = Math.max(1, rows - 4);
|
|
414
493
|
React.useEffect(() => {
|
|
415
494
|
setWindowStart(start => {
|
|
416
495
|
if (focusedOffset < start)
|
|
@@ -426,9 +505,37 @@ export function Settings({ channel, onClose, }) {
|
|
|
426
505
|
entryOffset += entry.lines;
|
|
427
506
|
return start >= windowStart && start + entry.lines <= windowStart + viewport;
|
|
428
507
|
});
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
508
|
+
const inGroup = activeSection !== undefined && activeGroupSpec !== undefined;
|
|
509
|
+
const navigationHint = inGroup ? t('settings-hint-group') : t('settings-hint-list');
|
|
510
|
+
// The bottom help bar: the focused field's hint on the left (truncated
|
|
511
|
+
// first), the navigation keys pinned to the right — a truncated hint still
|
|
512
|
+
// reads, a truncated shortcut hint hides the keys nobody can guess.
|
|
513
|
+
const focusedHint = focused?.kind === 'field' && focused.field.hint !== undefined
|
|
514
|
+
? pick(focused.field.hint, focused.field.hintDescriptions)
|
|
515
|
+
: undefined;
|
|
516
|
+
// A field whose user layer carries a value (settings.yaml) is "customized"
|
|
517
|
+
// — worth knowing, too noisy to badge every row with. It rides the help
|
|
518
|
+
// bar instead: focus the field and the suffix appears next to its hint.
|
|
519
|
+
const focusedCustomized = focused?.kind === 'field'
|
|
520
|
+
? forms.get(focused.ns)?.field(focused.field).overridden === true
|
|
521
|
+
: false;
|
|
522
|
+
const keysLine = mode === 'edit' ? t('settings-hint-edit') : navigationHint;
|
|
523
|
+
const keysWidth = stringWidth(keysLine.replace(/\*\*/gu, ''));
|
|
524
|
+
const hintBudget = Math.max(0, columns - keysWidth - 2);
|
|
525
|
+
// The customized suffix is the whole point of this line — reserve its
|
|
526
|
+
// width first and truncate the hint instead, so it never becomes '已自…'.
|
|
527
|
+
const customText = focusedCustomized ? t('settings-field-customized') : undefined;
|
|
528
|
+
const customWidth = customText !== undefined ? stringWidth(customText) + 3 : 0; // ' · '
|
|
529
|
+
let hintText = '';
|
|
530
|
+
if (focusedHint !== undefined && hintBudget >= 12) {
|
|
531
|
+
hintText = truncateWidth(focusedHint, Math.max(0, hintBudget - customWidth));
|
|
532
|
+
if (customText !== undefined)
|
|
533
|
+
hintText += ` · ${customText}`;
|
|
534
|
+
}
|
|
535
|
+
else if (customText !== undefined && hintBudget >= 12) {
|
|
536
|
+
hintText = truncateWidth(customText, hintBudget);
|
|
537
|
+
}
|
|
538
|
+
// Header stats: where the focus sits in the current page's focusable list.
|
|
539
|
+
// (Auto-save means there is no unsaved-draft count to surface here.)
|
|
540
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: t('settings-title') }), inGroup && (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: [' › ', pick(activeSection.title, activeSection.descriptions)] }), _jsx(Text, { dimColor: true, children: ' › ' }), _jsx(Text, { bold: true, children: pick(activeGroupSpec.title, activeGroupSpec.descriptions) })] })), _jsx(Box, { flexGrow: 1 }), host === undefined && _jsx(Text, { color: "warning", children: `${t('settings-unavailable')} ` }), focusable.length > 0 && _jsx(Text, { dimColor: true, children: `${effFocus + 1}/${focusable.length}` })] }), _jsx("ink-box", { style: { flexDirection: 'column', flexGrow: 1, flexShrink: 1, overflow: 'hidden' }, onWheel: handleWheel, children: visible.map(entry => (_jsx(React.Fragment, { children: entry.node }, entry.key))) }), _jsx(Box, { flexGrow: 1 }), _jsx(Divider, {}), _jsx(Text, { color: notice?.tone === 'error' ? 'error' : 'success', children: notice === undefined ? ' ' : `${notice.tone === 'error' ? MULTIPLICATION_X : TICK} ${notice.text}` }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, italic: true, children: hintText }), _jsx(Box, { flexGrow: 1 }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: keysLine }) })] })] }));
|
|
434
541
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatusLine.d.ts","sourceRoot":"","sources":["../../../src/screens/StatusLine.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"StatusLine.d.ts","sourceRoot":"","sources":["../../../src/screens/StatusLine.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAcxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AA8EvD,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,eAAuB,EACvB,QAAgB,EAChB,IAAI,GACL,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACvD,qBAuXA;AAED,KAAK,aAAa,GAAG;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAsID,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKvF"}
|