@deepseek-harness-tui/dsh-tui 0.7.3 → 0.8.0
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 +2 -2
- package/bin/dsh-tui.js +19 -3
- package/cordis.patch.yml +24 -11
- package/cordis.yml +9 -1
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +2 -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 +4 -4
- package/lib/types/components/PluginSceneBoundary.d.ts +31 -0
- package/lib/types/components/PluginSceneBoundary.d.ts.map +1 -0
- package/lib/types/components/PluginSceneBoundary.js +29 -0
- package/lib/types/components/SkillsPicker.d.ts +18 -0
- package/lib/types/components/SkillsPicker.d.ts.map +1 -0
- package/lib/types/components/SkillsPicker.js +50 -0
- package/lib/types/components/SplitDiffView.d.ts +28 -0
- package/lib/types/components/SplitDiffView.d.ts.map +1 -0
- package/lib/types/components/SplitDiffView.js +417 -0
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +3 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +17 -7
- package/lib/types/dsh-adapter/channel.d.ts +68 -0
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +120 -5
- package/lib/types/dsh-adapter/contract.d.ts +1 -1
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +1 -0
- package/lib/types/dsh-adapter/index.d.ts +4 -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/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +59 -0
- package/lib/types/dsh-adapter/promptDebug.d.ts +10 -0
- package/lib/types/dsh-adapter/promptDebug.d.ts.map +1 -0
- package/lib/types/dsh-adapter/promptDebug.js +148 -0
- package/lib/types/dsh-adapter/scenes.d.ts +75 -0
- package/lib/types/dsh-adapter/scenes.d.ts.map +1 -0
- package/lib/types/dsh-adapter/scenes.js +78 -0
- package/lib/types/dsh-adapter/settings-sections.d.ts +116 -0
- package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -0
- package/lib/types/dsh-adapter/settings-sections.js +63 -0
- package/lib/types/dsh-adapter/settingsEditor.d.ts +128 -0
- package/lib/types/dsh-adapter/settingsEditor.d.ts.map +1 -0
- package/lib/types/dsh-adapter/settingsEditor.js +244 -0
- package/lib/types/i18n.d.ts +137 -10
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +36 -3
- package/lib/types/jsx-runtime.d.ts +2 -0
- package/lib/types/jsx-runtime.d.ts.map +1 -0
- package/lib/types/jsx-runtime.js +13 -0
- package/lib/types/modelRoute.d.ts +10 -2
- package/lib/types/modelRoute.d.ts.map +1 -1
- package/lib/types/modelRoute.js +33 -4
- package/lib/types/scenes.d.ts +3 -0
- package/lib/types/scenes.d.ts.map +1 -0
- package/lib/types/scenes.js +4 -0
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +107 -11
- package/lib/types/screens/Settings.d.ts +20 -0
- package/lib/types/screens/Settings.d.ts.map +1 -0
- package/lib/types/screens/Settings.js +333 -0
- package/lib/types/screens/StatusLine.js +1 -1
- package/lib/types/settings-sections.d.ts +3 -0
- package/lib/types/settings-sections.d.ts.map +1 -0
- package/lib/types/settings-sections.js +4 -0
- package/lib/types/theme.d.ts +12 -0
- package/lib/types/theme.d.ts.map +1 -1
- package/lib/types/theme.js +36 -0
- package/package.json +17 -1
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { t, getLang, setLang, isLang, writeLangPref, subscribeLang } from '../i18n.js';
|
|
4
4
|
import { AlternateScreen, Box, Text, useInput, ScrollBox, useTheme, useTerminalSize } from '../ui.js';
|
|
5
|
+
import * as tuiKit from '../ui.js';
|
|
5
6
|
import { POINTER } from '../cc/figures.js';
|
|
6
7
|
import { isMod, isPlainReturnInput, modLabel } from '../utils/modifiers.js';
|
|
7
8
|
import { formatTokens } from '../cc/format.js';
|
|
@@ -26,7 +27,10 @@ import { StatusLine } from './StatusLine.js';
|
|
|
26
27
|
import { WorkingSpinner, useThinkingStatus } from '../components/WorkingSpinner.js';
|
|
27
28
|
import { ActivityLine, contextPressurePct } from '../components/ActivityLine.js';
|
|
28
29
|
import { ModelPicker } from '../components/ModelPicker.js';
|
|
30
|
+
import { PluginSceneBoundary } from '../components/PluginSceneBoundary.js';
|
|
31
|
+
import { SkillsPicker, SkillsPickerLoading } from '../components/SkillsPicker.js';
|
|
29
32
|
import { SessionBrowser } from './SessionBrowser.js';
|
|
33
|
+
import { Settings } from './Settings.js';
|
|
30
34
|
import { WorkspacePicker } from '../components/WorkspacePicker.js';
|
|
31
35
|
import { WorkspaceFlowPicker } from '../components/WorkspaceFlowPicker.js';
|
|
32
36
|
import { ActivityPicker } from '../components/ActivityPicker.js';
|
|
@@ -152,9 +156,17 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
152
156
|
const [modelPickerOpen, setModelPickerOpen] = React.useState(false);
|
|
153
157
|
const [models, setModels] = React.useState([]);
|
|
154
158
|
const [modelIndex, setModelIndex] = React.useState(0);
|
|
159
|
+
/** `/skills` 技能目录(issue #204):null = 注册表快照在途。 */
|
|
160
|
+
const [skillsPickerOpen, setSkillsPickerOpen] = React.useState(false);
|
|
161
|
+
const [skillsList, setSkillsList] = React.useState(null);
|
|
162
|
+
const [skillsIndex, setSkillsIndex] = React.useState(0);
|
|
155
163
|
/** `/resume` opens the session browser, a screen rather than a panel. It
|
|
156
164
|
* owns its own selection, filters and keyboard — Chat only opens it. */
|
|
157
165
|
const [browserOpen, setBrowserOpen] = React.useState(false);
|
|
166
|
+
/** `/settings` opens the plugin settings screen (issue #165) — like the
|
|
167
|
+
* browser, a screen rather than a panel: it owns its own focus, staged
|
|
168
|
+
* drafts and keyboard; Chat only opens it. */
|
|
169
|
+
const [settingsOpen, setSettingsOpen] = React.useState(false);
|
|
158
170
|
const [workspacePickerOpen, setWorkspacePickerOpen] = React.useState(false);
|
|
159
171
|
const [workspaceTargets, setWorkspaceTargets] = React.useState([]);
|
|
160
172
|
const [workspaceIndex, setWorkspaceIndex] = React.useState(0);
|
|
@@ -635,6 +647,23 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
635
647
|
setModelIndex(index >= 0 ? index : 0);
|
|
636
648
|
});
|
|
637
649
|
return true;
|
|
650
|
+
case 'skills':
|
|
651
|
+
// issue #204: 列出当前 agent 的完整技能目录(名称 + 来源 + 简述),
|
|
652
|
+
// Enter 把可直调技能以 `/name ` 填回输入行(completion-only 分发的
|
|
653
|
+
// 同一路径)。注册表读取走 channel(快照 scoped 到 live agent)。
|
|
654
|
+
setHelpOpen(false);
|
|
655
|
+
setSkillsList(null);
|
|
656
|
+
setSkillsIndex(0);
|
|
657
|
+
setSkillsPickerOpen(true);
|
|
658
|
+
void channel.listSkills().then((list) => {
|
|
659
|
+
if (list === undefined) {
|
|
660
|
+
setSkillsPickerOpen(false);
|
|
661
|
+
channel.notify(t('skills-load-failed'), { color: 'error' });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
setSkillsList(list);
|
|
665
|
+
});
|
|
666
|
+
return true;
|
|
638
667
|
case 'provider': {
|
|
639
668
|
// Interactive add-provider wizard (/provider): drives the shared
|
|
640
669
|
// question panel, persists profile + key via the channel's settings/
|
|
@@ -788,6 +817,13 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
788
817
|
channel.pushLocal('/cost', lines);
|
|
789
818
|
return true;
|
|
790
819
|
}
|
|
820
|
+
case 'settings': {
|
|
821
|
+
// Plugin settings screen (issue #165): opens immediately; the screen
|
|
822
|
+
// reads sections + namespaces from the channel itself.
|
|
823
|
+
setHelpOpen(false);
|
|
824
|
+
setSettingsOpen(true);
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
791
827
|
case 'config': {
|
|
792
828
|
const userHome = process.env.USERPROFILE ?? '';
|
|
793
829
|
const lines = [
|
|
@@ -888,13 +924,6 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
888
924
|
setHelpOpen(false);
|
|
889
925
|
channel.pushLocal('/mcp', channel.mcpStatus());
|
|
890
926
|
return true;
|
|
891
|
-
case 'memory':
|
|
892
|
-
setHelpOpen(false);
|
|
893
|
-
channel.pushLocal('/memory', [
|
|
894
|
-
t('memory-none'),
|
|
895
|
-
t('memory-hint'),
|
|
896
|
-
]);
|
|
897
|
-
return true;
|
|
898
927
|
case 'update':
|
|
899
928
|
setHelpOpen(false);
|
|
900
929
|
if (onUpdate === undefined) {
|
|
@@ -1202,6 +1231,16 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1202
1231
|
// search box, which Chat would otherwise route into the prompt.
|
|
1203
1232
|
if (browserOpen)
|
|
1204
1233
|
return;
|
|
1234
|
+
// Same for the settings screen: plain letters (s save / d discard) and
|
|
1235
|
+
// the field draft editor belong to it alone.
|
|
1236
|
+
if (settingsOpen)
|
|
1237
|
+
return;
|
|
1238
|
+
// A plugin scene (dsh-tui-scenes) or the trajectory scene owns the whole
|
|
1239
|
+
// screen while open: every key belongs to it. Unguarded, an Esc meant to
|
|
1240
|
+
// CLOSE the scene also reached the chat:cancel branch below whenever a
|
|
1241
|
+
// turn was in flight — closing the view and killing the turn in one key.
|
|
1242
|
+
if (sceneOpen || channel.pluginScene !== undefined)
|
|
1243
|
+
return;
|
|
1205
1244
|
// The questionnaire / approval panel owns the keyboard while one is
|
|
1206
1245
|
// pending (the panel's own useInput handles ↑/↓/Space/Tab/Enter/Esc;
|
|
1207
1246
|
// the prompt input is unmounted, so nothing else should see these keys).
|
|
@@ -1457,6 +1496,30 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1457
1496
|
}
|
|
1458
1497
|
return;
|
|
1459
1498
|
}
|
|
1499
|
+
if (skillsPickerOpen) {
|
|
1500
|
+
const list = skillsList ?? [];
|
|
1501
|
+
if (key.upArrow) {
|
|
1502
|
+
if (list.length > 0)
|
|
1503
|
+
setSkillsIndex(index => (index <= 0 ? list.length - 1 : index - 1));
|
|
1504
|
+
}
|
|
1505
|
+
else if (key.downArrow) {
|
|
1506
|
+
if (list.length > 0)
|
|
1507
|
+
setSkillsIndex(index => (index >= list.length - 1 ? 0 : index + 1));
|
|
1508
|
+
}
|
|
1509
|
+
else if (plainReturn) {
|
|
1510
|
+
const skill = list[skillsIndex];
|
|
1511
|
+
setSkillsPickerOpen(false);
|
|
1512
|
+
// 可直调技能 Enter 填入 `/name `——与 / 菜单选中技能同一条
|
|
1513
|
+
// completion-only 分发路径;模型专用技能(userInvocable=false)只关闭。
|
|
1514
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty list
|
|
1515
|
+
if (skill?.userInvocable)
|
|
1516
|
+
setHistoryFill(`/${skill.name} `);
|
|
1517
|
+
}
|
|
1518
|
+
else if (key.escape) {
|
|
1519
|
+
setSkillsPickerOpen(false);
|
|
1520
|
+
}
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1460
1523
|
if (activityPickerOpen) {
|
|
1461
1524
|
if (key.upArrow) {
|
|
1462
1525
|
setActivityIndex(index => (index <= 0 ? PRESET_NAMES.length - 1 : index - 1));
|
|
@@ -1744,6 +1807,32 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1744
1807
|
// Working-activity line (spinner slot): context-pressure prefix shares the
|
|
1745
1808
|
// StatusLine thresholds (amber ≥ 80, red ≥ 95).
|
|
1746
1809
|
const activityWarnPct = contextPressurePct(channel.lastUsage, channel.contextWindow);
|
|
1810
|
+
// A plugin scene (dsh-tui-scenes) takes the whole terminal the same way
|
|
1811
|
+
// the trajectory scene does, and sits at the TOP of this return chain:
|
|
1812
|
+
// an open() landing while the session browser or the trajectory scene is
|
|
1813
|
+
// up must still take the screen (and the keyboard, via the useInput guard
|
|
1814
|
+
// above), not queue silently behind them. Closing the plugin scene lands
|
|
1815
|
+
// back on whatever screen was up before, so these early returns read as a
|
|
1816
|
+
// stack. The component comes from the registry, so its identity is stable
|
|
1817
|
+
// across renders and its hook state survives re-renders; it receives the
|
|
1818
|
+
// TUI's own React + ui kit because a plugin importing its own React copy
|
|
1819
|
+
// would die on the first hook call under this reconciler.
|
|
1820
|
+
// The scene is third-party code, so it renders inside a boundary: a render
|
|
1821
|
+
// crash reports to the transcript and closes the scene instead of taking
|
|
1822
|
+
// the whole TUI down through ink's app-level boundary.
|
|
1823
|
+
const pluginScene = channel.pluginScene;
|
|
1824
|
+
if (pluginScene !== undefined) {
|
|
1825
|
+
const node = (_jsx(PluginSceneBoundary, { id: pluginScene.id, onError: (id, error) => {
|
|
1826
|
+
channel.notify(t('plugin-scene-crashed', { id, err: error.message }), { color: 'error' });
|
|
1827
|
+
channel.closePluginScene();
|
|
1828
|
+
}, children: React.createElement(pluginScene.component, {
|
|
1829
|
+
React,
|
|
1830
|
+
ui: tuiKit,
|
|
1831
|
+
channel,
|
|
1832
|
+
close: () => channel.closePluginScene(),
|
|
1833
|
+
}) }));
|
|
1834
|
+
return fullscreen ? node : _jsx(AlternateScreen, { children: node });
|
|
1835
|
+
}
|
|
1747
1836
|
// The browser is a screen, not an overlay: it REPLACES the conversation
|
|
1748
1837
|
// rather than floating above it. Rendering it as an early return (after
|
|
1749
1838
|
// every hook above has run) is what makes that literal — there is no
|
|
@@ -1754,8 +1843,15 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1754
1843
|
// hosts are already in it and must not nest a second one.
|
|
1755
1844
|
return fullscreen ? browser : _jsx(AlternateScreen, { children: browser });
|
|
1756
1845
|
}
|
|
1846
|
+
// The settings screen follows the browser's rule exactly: it REPLACES the
|
|
1847
|
+
// conversation (an early return after every hook above has run), so there
|
|
1848
|
+
// is no transcript underneath to be repainted or bled through.
|
|
1849
|
+
if (settingsOpen) {
|
|
1850
|
+
const screen = _jsx(Settings, { channel: channel, onClose: () => setSettingsOpen(false) });
|
|
1851
|
+
return fullscreen ? screen : _jsx(AlternateScreen, { children: screen });
|
|
1852
|
+
}
|
|
1757
1853
|
/** Prompt input is inert while a modal dialog owns the keyboard. */
|
|
1758
|
-
const promptSelectionActive = selectionActive || modelPickerOpen || workspacePickerOpen || workspaceFlow !== null || activityPickerOpen ||
|
|
1854
|
+
const promptSelectionActive = selectionActive || modelPickerOpen || skillsPickerOpen || workspacePickerOpen || workspaceFlow !== null || activityPickerOpen ||
|
|
1759
1855
|
effortSliderOpen || presetPickerOpen || themePickerOpen || thinkingOpen || historyOpen || rewindOpen || searchOpen ||
|
|
1760
1856
|
btw !== null;
|
|
1761
1857
|
// The trajectory scene replaces the conversation for as long as it is open.
|
|
@@ -1776,7 +1872,7 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1776
1872
|
// 常驻、只移除其普通子节点,blit 解毒不触发,被覆盖的转录行会在
|
|
1777
1873
|
// blit-skip 后留空(Esc 关 picker 一片空白的根因)。
|
|
1778
1874
|
const dialogOverlayOpen = thinkingOpen || (workspacePickerOpen && workspaceTargets.length > 0) || workspaceFlow !== null ||
|
|
1779
|
-
modelPickerOpen ||
|
|
1875
|
+
modelPickerOpen || skillsPickerOpen ||
|
|
1780
1876
|
activityPickerOpen || (effortSliderOpen && effortOptions.length > 1) ||
|
|
1781
1877
|
(presetPickerOpen && presetOptions.length > 0) || themePickerOpen || historyOpen ||
|
|
1782
1878
|
rewindOpen || searchOpen;
|
|
@@ -1787,7 +1883,7 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1787
1883
|
seekRow(lastUser.id);
|
|
1788
1884
|
else
|
|
1789
1885
|
handle?.scrollToBottom();
|
|
1790
|
-
} })), _jsxs(ScrollBox, { ref: setHandle, flexDirection: "column", flexGrow: 1, flexShrink: 1, stickyScroll: true, children: [_jsx(LogoHeader, { model: channel.model, effort: channel.reasoningEffort, cwd: channel.displayCwd }), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: () => { setLoadedContextOpen(previous => !previous); } })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, model: channel.model, showAll: showAllMessages, thinkingVisible: thinkingVisible, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [showPill && (_jsx(NewMessagesPill, { count: unseenCount, onClick: () => handle?.scrollToBottom() })), channel.working &&
|
|
1886
|
+
} })), _jsxs(ScrollBox, { ref: setHandle, flexDirection: "column", flexGrow: 1, flexShrink: 1, stickyScroll: true, children: [_jsx(LogoHeader, { model: channel.model, effort: channel.reasoningEffort, cwd: channel.displayCwd }), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: () => { setLoadedContextOpen(previous => !previous); } })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, model: channel.model, diffLayout: channel.diffLayout, showAll: showAllMessages, thinkingVisible: thinkingVisible, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [showPill && (_jsx(NewMessagesPill, { count: unseenCount, onClick: () => handle?.scrollToBottom() })), channel.working &&
|
|
1791
1887
|
(channel.activityEnabled &&
|
|
1792
1888
|
channel.workingActivity !== undefined &&
|
|
1793
1889
|
channel.workingActivity.line !== '' &&
|
|
@@ -1806,7 +1902,7 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1806
1902
|
band: wakeBand,
|
|
1807
1903
|
hint: trajectorySeen ? undefined : `${modLabel}t`,
|
|
1808
1904
|
tick: Math.floor(wakeTime / 120),
|
|
1809
|
-
} }), dialogOverlayOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 8, 8), children: [thinkingOpen && (_jsx(ThinkingToggle, { currentValue: thinkingVisible, focusIndex: thinkingFocus, confirmationPending: thinkingConfirm })), workspacePickerOpen && workspaceTargets.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspacePicker, { targets: workspaceTargets, focusIndex: workspaceIndex, currentCwd: channel.cwd }) })), workspaceFlow !== null && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspaceFlowPicker, { title: workspaceFlow.title, choices: workspaceFlow.choices, focusIndex: workspaceFlowIndex, busy: workspaceFlowBusy, input: workspaceFlowInput }) })), modelPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: models.length === 0 ? (_jsx(ModelPickerLoading, {})) : (_jsx(ModelPicker, { models: models, focusIndex: modelIndex, currentModel: `${channel.provider}/${channel.model}` })) })), activityPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ActivityPicker, { focusIndex: activityIndex, currentPreset: channel.activityFrames }) })), effortSliderOpen && effortOptions.length > 1 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EffortSlider, { options: effortOptions, focusIndex: effortIndex, currentId: channel.reasoningEffort }) })), presetPickerOpen && presetOptions.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(PresetPicker, { presets: presetOptions, focusIndex: presetIndex, currentPreset: channel.agentPreset }) })), themePickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ThemePicker, { focusIndex: themeIndex, currentTheme: themeName }) })), historyOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(HistorySearchDialog, { query: historyQuery, cursorOffset: historyCursor, matches: historyMatches, focusIndex: historyFocus }) })), rewindOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RewindPicker, { rows: rewindRows, focusIndex: rewindIndex, confirmRow: rewindConfirm }) })), searchOpen && _jsx(TranscriptSearchBar, { query: searchQuery, cursorOffset: searchCursor, count: searchCount, current: searchCurrent })] }))] })] }));
|
|
1905
|
+
} }), dialogOverlayOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 8, 8), children: [thinkingOpen && (_jsx(ThinkingToggle, { currentValue: thinkingVisible, focusIndex: thinkingFocus, confirmationPending: thinkingConfirm })), workspacePickerOpen && workspaceTargets.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspacePicker, { targets: workspaceTargets, focusIndex: workspaceIndex, currentCwd: channel.cwd }) })), workspaceFlow !== null && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspaceFlowPicker, { title: workspaceFlow.title, choices: workspaceFlow.choices, focusIndex: workspaceFlowIndex, busy: workspaceFlowBusy, input: workspaceFlowInput }) })), modelPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: models.length === 0 ? (_jsx(ModelPickerLoading, {})) : (_jsx(ModelPicker, { models: models, focusIndex: modelIndex, currentModel: `${channel.provider}/${channel.model}` })) })), skillsPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: skillsList === null ? (_jsx(SkillsPickerLoading, {})) : (_jsx(SkillsPicker, { skills: skillsList, focusIndex: skillsIndex })) })), activityPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ActivityPicker, { focusIndex: activityIndex, currentPreset: channel.activityFrames }) })), effortSliderOpen && effortOptions.length > 1 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EffortSlider, { options: effortOptions, focusIndex: effortIndex, currentId: channel.reasoningEffort }) })), presetPickerOpen && presetOptions.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(PresetPicker, { presets: presetOptions, focusIndex: presetIndex, currentPreset: channel.agentPreset }) })), themePickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ThemePicker, { focusIndex: themeIndex, currentTheme: themeName }) })), historyOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(HistorySearchDialog, { query: historyQuery, cursorOffset: historyCursor, matches: historyMatches, focusIndex: historyFocus }) })), rewindOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RewindPicker, { rows: rewindRows, focusIndex: rewindIndex, confirmRow: rewindConfirm }) })), searchOpen && _jsx(TranscriptSearchBar, { query: searchQuery, cursorOffset: searchCursor, count: searchCount, current: searchCurrent })] }))] })] }));
|
|
1810
1906
|
}
|
|
1811
1907
|
/**
|
|
1812
1908
|
* The pinned prompt header shown above the ScrollBox while the user has
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Channel } from '../dsh-adapter/channel.js';
|
|
3
|
+
/**
|
|
4
|
+
* The settings screen — `/settings` as a screen of its own (issue #165).
|
|
5
|
+
*
|
|
6
|
+
* The TUI owns only presentation here: plugin-declared sections from the
|
|
7
|
+
* `tuiSettingsSections` seam render as editable forms; every write goes back
|
|
8
|
+
* through the dsh settings service (revision-fenced `mutate` path ops) or the
|
|
9
|
+
* credentials seam (secret fields, blank-until-typed). Namespaces no plugin
|
|
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.
|
|
12
|
+
*
|
|
13
|
+
* Edits are staged, never settled live: typing changes a draft, and only the
|
|
14
|
+
* explicit save writes the durable document (see settingsEditor.ts for why).
|
|
15
|
+
*/
|
|
16
|
+
export declare function Settings({ channel, onClose, }: {
|
|
17
|
+
channel: Channel;
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
}): React.ReactNode;
|
|
20
|
+
//# sourceMappingURL=Settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Settings.d.ts","sourceRoot":"","sources":["../../../src/screens/Settings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAUzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AA2CxD;;;;;;;;;;;;GAYG;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,CA0VlB"}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text, useInput, useTerminalSize } from '../ui.js';
|
|
4
|
+
import { Divider } from '../components/design-system/Divider.js';
|
|
5
|
+
import { HintLine } from '../components/design-system/HintLine.js';
|
|
6
|
+
import { isMod, isPlainReturn } from '../utils/modifiers.js';
|
|
7
|
+
import { truncateWidth } from '../sessions/format.js';
|
|
8
|
+
import { getLang, t } from '../i18n.js';
|
|
9
|
+
import { SettingsForm } from '../dsh-adapter/settingsEditor.js';
|
|
10
|
+
/** Pick the provider-owned translation for the active language. */
|
|
11
|
+
function pick(text, descriptions) {
|
|
12
|
+
return descriptions?.[getLang()] ?? text;
|
|
13
|
+
}
|
|
14
|
+
/** Compact one-line preview of a read-only namespace's resolved value. */
|
|
15
|
+
function valuePreview(value, budget) {
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = JSON.stringify(value) ?? 'undefined';
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
raw = String(value);
|
|
22
|
+
}
|
|
23
|
+
return truncateWidth(raw, Math.max(8, budget));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The settings screen — `/settings` as a screen of its own (issue #165).
|
|
27
|
+
*
|
|
28
|
+
* The TUI owns only presentation here: plugin-declared sections from the
|
|
29
|
+
* `tuiSettingsSections` seam render as editable forms; every write goes back
|
|
30
|
+
* through the dsh settings service (revision-fenced `mutate` path ops) or the
|
|
31
|
+
* credentials seam (secret fields, blank-until-typed). Namespaces no plugin
|
|
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.
|
|
34
|
+
*
|
|
35
|
+
* Edits are staged, never settled live: typing changes a draft, and only the
|
|
36
|
+
* explicit save writes the durable document (see settingsEditor.ts for why).
|
|
37
|
+
*/
|
|
38
|
+
export function Settings({ channel, onClose, }) {
|
|
39
|
+
// Explicit terminal size (not flexGrow) — the same rule SessionBrowser's
|
|
40
|
+
// root follows inside the alternate screen's fixed-height box.
|
|
41
|
+
const { columns, rows } = useTerminalSize();
|
|
42
|
+
// channel caches the host — a fresh object per call would re-fire the
|
|
43
|
+
// host-keyed effects below on every render (an endless render loop).
|
|
44
|
+
const host = channel.settingsHost();
|
|
45
|
+
const [namespaces, setNamespaces] = React.useState(() => host?.listNamespaces() ?? []);
|
|
46
|
+
const [sections, setSections] = React.useState(() => channel.settingsSections());
|
|
47
|
+
const [mode, setMode] = React.useState('list');
|
|
48
|
+
const [editing, setEditing] = React.useState(null);
|
|
49
|
+
const [focusIndex, setFocusIndex] = React.useState(0);
|
|
50
|
+
const [notice, setNotice] = React.useState(undefined);
|
|
51
|
+
/** Configured status of every secret field's credential ref. */
|
|
52
|
+
const [secrets, setSecrets] = React.useState(new Map());
|
|
53
|
+
/** First line of the windowed entry list (focus-follow scrolling). */
|
|
54
|
+
const [windowStart, setWindowStart] = React.useState(0);
|
|
55
|
+
/** Repaint after a form mutation — staged drafts live in the (React-free)
|
|
56
|
+
* SettingsForm, so editing one changes no React state by itself. */
|
|
57
|
+
const [, bump] = React.useReducer((count) => count + 1, 0);
|
|
58
|
+
/** Async save completions must not touch state after the screen closes. */
|
|
59
|
+
const mountedRef = React.useRef(true);
|
|
60
|
+
React.useEffect(() => () => {
|
|
61
|
+
mountedRef.current = false;
|
|
62
|
+
}, []);
|
|
63
|
+
React.useEffect(() => channel.subscribeSettingsSections(() => {
|
|
64
|
+
setSections(channel.settingsSections());
|
|
65
|
+
}), [channel]);
|
|
66
|
+
// One form per section namespace. A fresh namespace view replaces the form
|
|
67
|
+
// only while it holds no edits — replacing a dirty form would discard the
|
|
68
|
+
// drafts the user is still typing.
|
|
69
|
+
const formsRef = React.useRef(new Map());
|
|
70
|
+
const forms = new Map();
|
|
71
|
+
if (host !== undefined) {
|
|
72
|
+
for (const section of sections) {
|
|
73
|
+
const view = namespaces.find(entry => entry.ns === section.ns);
|
|
74
|
+
const kept = formsRef.current.get(section.ns);
|
|
75
|
+
const reuse = kept !== undefined && (kept.namespace === view || kept.shell().dirty);
|
|
76
|
+
const form = reuse ? kept : new SettingsForm(host, view, section.fields);
|
|
77
|
+
forms.set(section.ns, form);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
formsRef.current = forms;
|
|
81
|
+
const refresh = () => {
|
|
82
|
+
setNamespaces(host?.listNamespaces() ?? []);
|
|
83
|
+
};
|
|
84
|
+
// Secret fields report only configured/unconfigured; resolve each ref once
|
|
85
|
+
// per section-list change (and after every save, which may have set one).
|
|
86
|
+
const [secretProbe, setSecretProbe] = React.useState(0);
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
if (host === undefined)
|
|
89
|
+
return;
|
|
90
|
+
let stale = false;
|
|
91
|
+
const pending = sections.flatMap(section => section.fields
|
|
92
|
+
.filter((field) => field.secret !== undefined)
|
|
93
|
+
.map(async (field) => [`${section.ns}:${field.path.join('.')}`, await host.credentialConfigured(field.secret.ref)]));
|
|
94
|
+
void Promise.all(pending).then(entries => {
|
|
95
|
+
if (!stale && mountedRef.current)
|
|
96
|
+
setSecrets(new Map(entries));
|
|
97
|
+
});
|
|
98
|
+
return () => {
|
|
99
|
+
stale = true;
|
|
100
|
+
};
|
|
101
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
|
+
}, [host, sections, secretProbe]);
|
|
103
|
+
/** Focusable rows in display order: every field of every section. */
|
|
104
|
+
const focusable = sections.flatMap(section => section.fields.map(field => ({ ns: section.ns, field })));
|
|
105
|
+
const effFocus = Math.min(focusIndex, Math.max(0, focusable.length - 1));
|
|
106
|
+
const focused = focusable.length === 0 ? undefined : focusable[effFocus];
|
|
107
|
+
const focusedForm = focused === undefined ? undefined : forms.get(focused.ns);
|
|
108
|
+
const commitSave = (ns) => {
|
|
109
|
+
const form = forms.get(ns);
|
|
110
|
+
if (form === undefined || form.saving || !form.shell().dirty)
|
|
111
|
+
return;
|
|
112
|
+
void form.save().then(ok => {
|
|
113
|
+
if (!mountedRef.current)
|
|
114
|
+
return;
|
|
115
|
+
if (ok) {
|
|
116
|
+
setNotice({ text: t('settings-saved', { ns }), tone: 'success' });
|
|
117
|
+
refresh();
|
|
118
|
+
setSecretProbe(count => count + 1);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
setNotice({ text: t('settings-save-failed', { ns }), tone: 'error' });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
/** Stage the next choice for a boolean/select field (Enter cycles). */
|
|
126
|
+
const cycleField = (ns, field) => {
|
|
127
|
+
const form = forms.get(ns);
|
|
128
|
+
if (form === undefined || !form.available || form.saving)
|
|
129
|
+
return;
|
|
130
|
+
const current = form.field(field).text;
|
|
131
|
+
if (field.kind === 'boolean') {
|
|
132
|
+
form.edit(field, current === 'true' ? 'false' : 'true');
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const options = field.options ?? [];
|
|
136
|
+
if (options.length === 0)
|
|
137
|
+
return;
|
|
138
|
+
const index = options.findIndex(option => option.value === current);
|
|
139
|
+
form.edit(field, options[(index + 1) % options.length]?.value ?? options[0]?.value ?? '');
|
|
140
|
+
}
|
|
141
|
+
setNotice(undefined);
|
|
142
|
+
bump();
|
|
143
|
+
};
|
|
144
|
+
useInput((input, key) => {
|
|
145
|
+
if (mode === 'edit' && editing !== null) {
|
|
146
|
+
if (isPlainReturn(key)) {
|
|
147
|
+
const form = forms.get(editing.ns);
|
|
148
|
+
form?.edit(editing.field, editing.draft);
|
|
149
|
+
bump();
|
|
150
|
+
setMode('list');
|
|
151
|
+
setEditing(null);
|
|
152
|
+
}
|
|
153
|
+
else if (key.escape) {
|
|
154
|
+
setMode('list');
|
|
155
|
+
setEditing(null);
|
|
156
|
+
}
|
|
157
|
+
else if (key.backspace || key.delete) {
|
|
158
|
+
setEditing(state => state === null ? null : { ...state, draft: state.draft.slice(0, -1) });
|
|
159
|
+
}
|
|
160
|
+
else if (!isMod(key) && !key.meta && !key.super && input && !key.return) {
|
|
161
|
+
// Only real characters reach the draft (see SessionBrowser's query).
|
|
162
|
+
const typed = input.replace(/\p{Cc}/gu, '');
|
|
163
|
+
if (typed.length > 0) {
|
|
164
|
+
setEditing(state => state === null ? null : { ...state, draft: state.draft + typed });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (key.upArrow) {
|
|
170
|
+
setFocusIndex(Math.max(0, effFocus - 1));
|
|
171
|
+
}
|
|
172
|
+
else if (key.downArrow) {
|
|
173
|
+
setFocusIndex(Math.min(Math.max(0, focusable.length - 1), effFocus + 1));
|
|
174
|
+
}
|
|
175
|
+
else if (isPlainReturn(key) && focused !== undefined) {
|
|
176
|
+
// A save in flight owns the section's drafts; starting an edit mid-write
|
|
177
|
+
// is exactly the lost-draft race the staged model exists to prevent.
|
|
178
|
+
if (focusedForm === undefined || !focusedForm.available || focusedForm.saving)
|
|
179
|
+
return;
|
|
180
|
+
if (focused.field.kind === 'boolean' || focused.field.kind === 'select') {
|
|
181
|
+
cycleField(focused.ns, focused.field);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
setEditing({ ns: focused.ns, field: focused.field, draft: focusedForm.field(focused.field).text });
|
|
185
|
+
setMode('edit');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (input === 's' && focused !== undefined) {
|
|
189
|
+
commitSave(focused.ns);
|
|
190
|
+
}
|
|
191
|
+
else if (input === 'd' && focused !== undefined) {
|
|
192
|
+
if (focusedForm?.saving)
|
|
193
|
+
return;
|
|
194
|
+
focusedForm?.discard();
|
|
195
|
+
setNotice(undefined);
|
|
196
|
+
bump();
|
|
197
|
+
}
|
|
198
|
+
else if (key.escape) {
|
|
199
|
+
// Esc backs out one layer at a time: staged drafts first — ANY dirty
|
|
200
|
+
// section, not just the focused one, so leaving never silently drops
|
|
201
|
+
// an edit made two sections ago — the screen second. A save in flight
|
|
202
|
+
// cannot be undone from here; let it settle instead of discarding
|
|
203
|
+
// around it.
|
|
204
|
+
const dirty = [...forms.values()].filter(form => form.shell().dirty);
|
|
205
|
+
if (dirty.length > 0) {
|
|
206
|
+
if (dirty.some(form => form.saving))
|
|
207
|
+
return;
|
|
208
|
+
for (const form of dirty)
|
|
209
|
+
form.discard();
|
|
210
|
+
setNotice({ text: t('settings-discarded'), tone: 'success' });
|
|
211
|
+
bump();
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
onClose();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
const renderField = (section, field) => {
|
|
219
|
+
const ns = section.ns;
|
|
220
|
+
const form = forms.get(ns);
|
|
221
|
+
const state = form?.field(field) ?? { text: '', overridden: false, invalid: false };
|
|
222
|
+
const isFocused = focused !== undefined && focused.ns === ns && focused.field === field;
|
|
223
|
+
const isEditing = isFocused && mode === 'edit' && editing !== null;
|
|
224
|
+
const label = pick(field.label, field.descriptions);
|
|
225
|
+
const hint = field.hint !== undefined ? pick(field.hint, field.hintDescriptions) : undefined;
|
|
226
|
+
let value;
|
|
227
|
+
if (field.secret !== undefined) {
|
|
228
|
+
const configured = secrets.get(`${ns}:${field.path.join('.')}`) === true;
|
|
229
|
+
if (isEditing) {
|
|
230
|
+
value = '•'.repeat(editing?.draft.length ?? 0) + '▌';
|
|
231
|
+
}
|
|
232
|
+
else if (state.text !== '') {
|
|
233
|
+
value = `${'•'.repeat(state.text.length)} ${t('settings-secret-staged')}`;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
value = configured ? t('settings-secret-set') : t('settings-secret-unset');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (isEditing) {
|
|
240
|
+
value = `${editing?.draft ?? ''}▌`;
|
|
241
|
+
}
|
|
242
|
+
else if (state.text === '') {
|
|
243
|
+
value = t('settings-field-empty');
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
value = state.text;
|
|
247
|
+
}
|
|
248
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isFocused ? 'suggestion' : undefined, children: isFocused ? '❯ ' : ' ' }), _jsx(Text, { bold: isFocused, children: label }), _jsx(Box, { flexGrow: 1 }), state.invalid && _jsxs(Text, { color: "error", children: [t('settings-field-invalid'), " "] }), state.overridden && _jsxs(Text, { dimColor: true, children: ["[", t('settings-badge-override'), "] "] }), form?.isStaged(field) === true && !isEditing && _jsx(Text, { color: "suggestion", children: "* " }), _jsx(Text, { color: isEditing || isFocused ? 'suggestion' : undefined, dimColor: !isFocused && !isEditing && state.text === '', children: value }), hint !== undefined && isFocused && (_jsxs(Text, { dimColor: true, children: ['\n ', hint] }))] }));
|
|
249
|
+
};
|
|
250
|
+
// ── Layout: a flat entry list with accounted line heights, windowed so the
|
|
251
|
+
// focused field is always on screen no matter how long the list gets. ────
|
|
252
|
+
const entries = [];
|
|
253
|
+
let focusCursor = 0;
|
|
254
|
+
sections.forEach((section, sectionIndex) => {
|
|
255
|
+
const form = forms.get(section.ns);
|
|
256
|
+
const view = form?.namespace;
|
|
257
|
+
const shell = form?.shell();
|
|
258
|
+
entries.push({
|
|
259
|
+
key: `section:${section.ns}`,
|
|
260
|
+
lines: sectionIndex === 0 ? 1 : 2,
|
|
261
|
+
node: (_jsxs(Box, { flexDirection: "column", children: [sectionIndex > 0 && _jsx(Text, { children: " " }), _jsxs(Box, { children: [_jsx(Text, { bold: true, color: "permission", children: pick(section.title, section.descriptions) }), _jsxs(Text, { dimColor: true, children: [" (", section.ns, ")"] }), view?.applies === 'restart' && _jsxs(Text, { color: "warning", children: [" [", t('settings-badge-restart'), "]"] }), view === undefined && _jsxs(Text, { color: "warning", children: [" [", t('settings-section-unavailable'), "]"] }), shell?.dirty === true && _jsxs(Text, { color: "suggestion", children: [" [", t('settings-badge-dirty'), "]"] }), shell?.saving === true && _jsxs(Text, { dimColor: true, children: [" [", t('settings-badge-saving'), "]"] }), shell?.failed === true && _jsxs(Text, { color: "error", children: [" [", t('settings-badge-failed'), "]"] })] })] })),
|
|
262
|
+
});
|
|
263
|
+
for (const field of section.fields) {
|
|
264
|
+
const isFocused = focused !== undefined && focused.ns === section.ns && focused.field === field;
|
|
265
|
+
const hasHint = field.hint !== undefined && isFocused;
|
|
266
|
+
const index = focusCursor;
|
|
267
|
+
focusCursor += 1;
|
|
268
|
+
entries.push({
|
|
269
|
+
key: `field:${section.ns}:${field.path.join('.')}`,
|
|
270
|
+
lines: hasHint ? 2 : 1,
|
|
271
|
+
focus: index,
|
|
272
|
+
node: renderField(section, field),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
const registeredNs = new Set(sections.map(section => section.ns));
|
|
277
|
+
const readonlyNamespaces = namespaces.filter(entry => !registeredNs.has(entry.ns));
|
|
278
|
+
if (readonlyNamespaces.length > 0) {
|
|
279
|
+
entries.push({
|
|
280
|
+
key: 'readonly:heading',
|
|
281
|
+
lines: 2,
|
|
282
|
+
node: (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, dimColor: true, children: t('settings-readonly-heading') })] })),
|
|
283
|
+
});
|
|
284
|
+
for (const entry of readonlyNamespaces) {
|
|
285
|
+
entries.push({
|
|
286
|
+
key: `readonly:${entry.ns}`,
|
|
287
|
+
lines: 1,
|
|
288
|
+
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)] })] })),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
entries.push({
|
|
292
|
+
key: 'readonly:hint',
|
|
293
|
+
lines: 1,
|
|
294
|
+
node: _jsxs(Text, { dimColor: true, children: [' ', t('settings-readonly-hint', { path: '~/.dsh/settings.yaml' })] }),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (sections.length === 0 && readonlyNamespaces.length === 0) {
|
|
298
|
+
entries.push({
|
|
299
|
+
key: 'empty',
|
|
300
|
+
lines: 1,
|
|
301
|
+
node: _jsx(Text, { dimColor: true, children: t('settings-empty') }),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// Focus-follow window: keep the focused entry fully inside the viewport.
|
|
305
|
+
let totalLines = 0;
|
|
306
|
+
let focusedOffset = 0;
|
|
307
|
+
let focusedLines = 1;
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (entry.focus === effFocus) {
|
|
310
|
+
focusedOffset = totalLines;
|
|
311
|
+
focusedLines = entry.lines;
|
|
312
|
+
}
|
|
313
|
+
totalLines += entry.lines;
|
|
314
|
+
}
|
|
315
|
+
// Chrome: title row, top rule, bottom rule, hint row, plus the notice.
|
|
316
|
+
const viewport = Math.max(1, rows - 4 - (notice === undefined ? 0 : 1));
|
|
317
|
+
React.useEffect(() => {
|
|
318
|
+
setWindowStart(start => {
|
|
319
|
+
if (focusedOffset < start)
|
|
320
|
+
return focusedOffset;
|
|
321
|
+
if (focusedOffset + focusedLines > start + viewport)
|
|
322
|
+
return focusedOffset + focusedLines - viewport;
|
|
323
|
+
return start;
|
|
324
|
+
});
|
|
325
|
+
}, [focusedOffset, focusedLines, viewport]);
|
|
326
|
+
let entryOffset = 0;
|
|
327
|
+
const visible = entries.filter(entry => {
|
|
328
|
+
const start = entryOffset;
|
|
329
|
+
entryOffset += entry.lines;
|
|
330
|
+
return start >= windowStart && start + entry.lines <= windowStart + viewport;
|
|
331
|
+
});
|
|
332
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: t('settings-title') }), _jsx(Box, { flexGrow: 1 }), host === undefined && _jsx(Text, { color: "warning", children: t('settings-unavailable') })] }), _jsx(Divider, {}), visible.map(entry => (_jsx(React.Fragment, { children: entry.node }, entry.key))), _jsx(Box, { flexGrow: 1 }), notice !== undefined && (_jsx(Text, { color: notice.tone === 'error' ? 'error' : 'success', children: notice.text })), _jsx(Divider, {}), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: mode === 'edit' ? t('settings-hint-edit') : t('settings-hint-list') }) })] }));
|
|
333
|
+
}
|
|
@@ -105,7 +105,7 @@ export function StatusLine({ channel, selectionActive = false, helpOpen = false,
|
|
|
105
105
|
if (channel.contextBarEnabled && barWidth >= 14 && channel.contextWindow !== undefined) {
|
|
106
106
|
bar = renderContextBar(channel.contextSegments, usage !== undefined ? usage.input + usage.cacheRead + usage.cacheWrite : 0, channel.contextWindow, barWidth, barColors);
|
|
107
107
|
}
|
|
108
|
-
return (_jsx(Box, { paddingX:
|
|
108
|
+
return (_jsx(Box, { paddingX: 1, width: columns, flexShrink: 0, children: _jsxs(Box, { flexDirection: "column", width: "100%", children: [bar ? _jsx(Text, { children: bar }) : null, _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", gap: 2, children: [_jsx(Text, { wrap: "truncate", children: _jsx(Byline, { children: leftParts }) }), _jsx(Box, { justifyContent: "flex-end", flexShrink: 2, children: _jsx(Text, { wrap: "truncate", children: _jsx(Byline, { children: rightParts }) }) })] }), _jsxs(Box, { height: 1, overflow: "hidden", flexDirection: "row", justifyContent: "space-between", gap: 2, children: [showActivity && activity !== undefined ? (_jsx(ActivityLine, { activity: activity, activityFrames: channel.activityFrames, warnPct: contextPressurePct(usage, channel.contextWindow), warnDanger: (contextPressurePct(usage, channel.contextWindow) ?? 0) >= 95 })) : hint ? (_jsx(Text, { color: "inactiveShimmer", children: hint })) : null, showActivity && hint ? (_jsx(Text, { color: "inactiveShimmer", wrap: "truncate", children: hint })) : null, wake !== undefined ? (_jsx(MiniWake, { band: wake.band, hint: wake.hint, tick: wake.tick })) : null] })] }) }));
|
|
109
109
|
}
|
|
110
110
|
function basename(path) {
|
|
111
111
|
const parts = path.split(/[\\/]/);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-sections.d.ts","sourceRoot":"","sources":["../../src/settings-sections.ts"],"names":[],"mappings":"AAEA,cAAc,oCAAoC,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAA"}
|
package/lib/types/theme.d.ts
CHANGED
|
@@ -47,6 +47,18 @@ export type Theme = {
|
|
|
47
47
|
diffRemovedDimmed: string;
|
|
48
48
|
diffAddedWord: string;
|
|
49
49
|
diffRemovedWord: string;
|
|
50
|
+
toolCardBackground: string;
|
|
51
|
+
toolCardBackgroundDim: string;
|
|
52
|
+
syntaxKeyword: string;
|
|
53
|
+
syntaxString: string;
|
|
54
|
+
syntaxComment: string;
|
|
55
|
+
syntaxNumber: string;
|
|
56
|
+
syntaxFunction: string;
|
|
57
|
+
syntaxType: string;
|
|
58
|
+
syntaxVariable: string;
|
|
59
|
+
syntaxOperator: string;
|
|
60
|
+
syntaxPunctuation: string;
|
|
61
|
+
syntaxConstant: string;
|
|
50
62
|
red_FOR_SUBAGENTS_ONLY: string;
|
|
51
63
|
blue_FOR_SUBAGENTS_ONLY: string;
|
|
52
64
|
green_FOR_SUBAGENTS_ONLY: string;
|
package/lib/types/theme.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,KAAK,GAAG;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,6BAA6B,EAAE,MAAM,CAAA;IACrC,oCAAoC,EAAE,MAAM,CAAA;IAC5C,UAAU,EAAE,MAAM,CAAA;IAClB,iBAAiB,EAAE,MAAM,CAAA;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,MAAM,CAAA;IACpB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAElB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,CAAA;IAEtB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/theme.ts"],"names":[],"mappings":"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"}
|