@deepseek-harness-tui/dsh-tui 0.7.2 → 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 +3 -3
- package/bin/dsh-tui.js +24 -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/GoalTodoPanel.d.ts.map +1 -1
- package/lib/types/components/GoalTodoPanel.js +6 -1
- package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
- package/lib/types/components/HistorySearchDialog.js +5 -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 +9 -5
- 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/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +52 -74
- package/lib/types/components/SearchBox.d.ts +4 -0
- package/lib/types/components/SearchBox.d.ts.map +1 -1
- package/lib/types/components/SearchBox.js +101 -2
- 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/Spinner/SpinnerAnimationRow.d.ts +4 -1
- package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts.map +1 -1
- package/lib/types/components/Spinner/SpinnerAnimationRow.js +10 -5
- 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/WorkingSpinner.d.ts +3 -1
- package/lib/types/components/WorkingSpinner.d.ts.map +1 -1
- package/lib/types/components/WorkingSpinner.js +2 -2
- package/lib/types/components/design-system/ThemedText.d.ts +7 -1
- package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemedText.js +2 -2
- 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/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +52 -8
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
- package/lib/types/components/questions/PlanReviewPanel.js +12 -3
- package/lib/types/dsh-adapter/channel.d.ts +78 -0
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +165 -14
- 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/packaged-presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/packaged-presets.js +5 -2
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +66 -3
- 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/providerWizard.d.ts.map +1 -1
- package/lib/types/dsh-adapter/providerWizard.js +3 -1
- 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 +169 -18
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +45 -6
- package/lib/types/ink/components/Text.d.ts +9 -1
- package/lib/types/ink/components/Text.d.ts.map +1 -1
- package/lib/types/ink/components/Text.js +7 -6
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +4 -2
- package/lib/types/ink/log-update.d.ts.map +1 -1
- package/lib/types/ink/log-update.js +76 -9
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +4 -1
- package/lib/types/ink/terminal.d.ts +40 -4
- package/lib/types/ink/terminal.d.ts.map +1 -1
- package/lib/types/ink/terminal.js +57 -5
- package/lib/types/ink/termio/tokenize.d.ts +6 -0
- package/lib/types/ink/termio/tokenize.d.ts.map +1 -1
- package/lib/types/ink/termio/tokenize.js +18 -4
- 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 +182 -26
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
- package/lib/types/screens/SessionBrowser.js +30 -15
- 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/sessions/view.d.ts.map +1 -1
- package/lib/types/sessions/view.js +13 -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/lib/types/update.d.ts +21 -7
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +66 -16
- package/lib/types/utils/paths.d.ts.map +1 -1
- package/lib/types/utils/paths.js +4 -1
- package/package.json +49 -9
|
@@ -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);
|
|
@@ -243,6 +255,14 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
243
255
|
}, []);
|
|
244
256
|
/** Startup context panel: expanded by header click or Ctrl+T. */
|
|
245
257
|
const [loadedContextOpen, setLoadedContextOpen] = React.useState(false);
|
|
258
|
+
/**
|
|
259
|
+
* Whether the startup context panel is on screen. Derived once and read by
|
|
260
|
+
* BOTH the Ctrl+T handler and the render below: the key's meaning depends on
|
|
261
|
+
* the panel being visible, so a second copy of this condition is a place for
|
|
262
|
+
* the two to disagree — which is how the key came to advertise one thing and
|
|
263
|
+
* do another in the first place.
|
|
264
|
+
*/
|
|
265
|
+
const loadedContextVisible = channel.rows.length === 0 && channel.loadedContext !== undefined;
|
|
246
266
|
/** `/` transcript search (less-style incsearch, ported from CC's REPL). */
|
|
247
267
|
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
248
268
|
const [searchQuery, setSearchQuery] = React.useState('');
|
|
@@ -306,10 +326,17 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
306
326
|
// Spinner timing refs, fed from channel state each render (the spinner
|
|
307
327
|
// only mounts while working, so values are stable for the mount).
|
|
308
328
|
const responseLengthRef = React.useRef(0);
|
|
329
|
+
const uploadTokensRef = React.useRef(0);
|
|
309
330
|
const loadingStartTimeRef = React.useRef(0);
|
|
310
331
|
const totalPausedMsRef = React.useRef(0);
|
|
311
332
|
const pauseStartTimeRef = React.useRef(null);
|
|
312
333
|
responseLengthRef.current = channel.responseChars;
|
|
334
|
+
// Most recent request's real upload (input + cache read/write occupy the
|
|
335
|
+
// wire exactly like the context window); 0 until the first usage event.
|
|
336
|
+
const lastUploadTokens = channel.lastUsage === undefined
|
|
337
|
+
? 0
|
|
338
|
+
: channel.lastUsage.input + channel.lastUsage.cacheRead + channel.lastUsage.cacheWrite;
|
|
339
|
+
uploadTokensRef.current = lastUploadTokens;
|
|
313
340
|
loadingStartTimeRef.current = channel.turnStart;
|
|
314
341
|
const thinkingStatus = useThinkingStatus(channel.spinnerMode === 'thinking');
|
|
315
342
|
// Terminal tab title (ported from CC's AnimatedTerminalTitle): the session
|
|
@@ -620,6 +647,23 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
620
647
|
setModelIndex(index >= 0 ? index : 0);
|
|
621
648
|
});
|
|
622
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;
|
|
623
667
|
case 'provider': {
|
|
624
668
|
// Interactive add-provider wizard (/provider): drives the shared
|
|
625
669
|
// question panel, persists profile + key via the channel's settings/
|
|
@@ -773,6 +817,13 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
773
817
|
channel.pushLocal('/cost', lines);
|
|
774
818
|
return true;
|
|
775
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
|
+
}
|
|
776
827
|
case 'config': {
|
|
777
828
|
const userHome = process.env.USERPROFILE ?? '';
|
|
778
829
|
const lines = [
|
|
@@ -814,14 +865,28 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
814
865
|
});
|
|
815
866
|
return true;
|
|
816
867
|
case 'login': {
|
|
817
|
-
const key = process.env.DEEPSEEK_API_KEY;
|
|
818
|
-
const lines = [
|
|
819
|
-
`${t('login-api-key', { key: key ? key.slice(0, 6) + '…' + key.slice(-4) : t('login-key-missing') })}`,
|
|
820
|
-
`${t('login-base-url', { url: process.env.DEEPSEEK_BASE_URL ?? t('login-official-endpoint') })}`,
|
|
821
|
-
t('login-source-hint'),
|
|
822
|
-
];
|
|
823
868
|
setHelpOpen(false);
|
|
824
|
-
channel.
|
|
869
|
+
void channel.describeCredential('DEEPSEEK_API_KEY')
|
|
870
|
+
.catch(() => undefined)
|
|
871
|
+
.then(status => {
|
|
872
|
+
const keyStatus = status === undefined
|
|
873
|
+
? t('login-credentials-unavailable')
|
|
874
|
+
: status.configured
|
|
875
|
+
? t('login-key-configured', { ref: 'DEEPSEEK_API_KEY' })
|
|
876
|
+
: t('login-key-missing');
|
|
877
|
+
channel.pushLocal('/login', [
|
|
878
|
+
t('login-api-key', { status: keyStatus }),
|
|
879
|
+
...(status === undefined
|
|
880
|
+
? []
|
|
881
|
+
: [
|
|
882
|
+
t('login-credential-source', { source: status.source ?? t('login-source-none') }),
|
|
883
|
+
t('login-credential-storage', {
|
|
884
|
+
mode: t(status.writable ? 'login-storage-writable' : 'login-storage-read-only'),
|
|
885
|
+
}),
|
|
886
|
+
]),
|
|
887
|
+
t('login-base-url', { url: process.env.DEEPSEEK_BASE_URL ?? t('login-official-endpoint') }),
|
|
888
|
+
]);
|
|
889
|
+
});
|
|
825
890
|
return true;
|
|
826
891
|
}
|
|
827
892
|
case 'logout':
|
|
@@ -859,13 +924,6 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
859
924
|
setHelpOpen(false);
|
|
860
925
|
channel.pushLocal('/mcp', channel.mcpStatus());
|
|
861
926
|
return true;
|
|
862
|
-
case 'memory':
|
|
863
|
-
setHelpOpen(false);
|
|
864
|
-
channel.pushLocal('/memory', [
|
|
865
|
-
t('memory-none'),
|
|
866
|
-
t('memory-hint'),
|
|
867
|
-
]);
|
|
868
|
-
return true;
|
|
869
927
|
case 'update':
|
|
870
928
|
setHelpOpen(false);
|
|
871
929
|
if (onUpdate === undefined) {
|
|
@@ -1173,6 +1231,16 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1173
1231
|
// search box, which Chat would otherwise route into the prompt.
|
|
1174
1232
|
if (browserOpen)
|
|
1175
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;
|
|
1176
1244
|
// The questionnaire / approval panel owns the keyboard while one is
|
|
1177
1245
|
// pending (the panel's own useInput handles ↑/↓/Space/Tab/Enter/Esc;
|
|
1178
1246
|
// the prompt input is unmounted, so nothing else should see these keys).
|
|
@@ -1428,6 +1496,30 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1428
1496
|
}
|
|
1429
1497
|
return;
|
|
1430
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
|
+
}
|
|
1431
1523
|
if (activityPickerOpen) {
|
|
1432
1524
|
if (key.upArrow) {
|
|
1433
1525
|
setActivityIndex(index => (index <= 0 ? PRESET_NAMES.length - 1 : index - 1));
|
|
@@ -1538,10 +1630,22 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1538
1630
|
}
|
|
1539
1631
|
}
|
|
1540
1632
|
else if (key.leftArrow) {
|
|
1541
|
-
|
|
1633
|
+
// Step by code point, not UTF-16 unit: an emoji is two units, and
|
|
1634
|
+
// a mid-pair caret offset would split it in the SearchBox render.
|
|
1635
|
+
setHistoryCursor(cursor => {
|
|
1636
|
+
if (cursor <= 0)
|
|
1637
|
+
return 0;
|
|
1638
|
+
const ch = [...historyQuery.slice(0, cursor)].pop();
|
|
1639
|
+
return cursor - ch.length;
|
|
1640
|
+
});
|
|
1542
1641
|
}
|
|
1543
1642
|
else if (key.rightArrow) {
|
|
1544
|
-
setHistoryCursor(cursor =>
|
|
1643
|
+
setHistoryCursor(cursor => {
|
|
1644
|
+
if (cursor >= historyQuery.length)
|
|
1645
|
+
return historyQuery.length;
|
|
1646
|
+
const ch = [...historyQuery.slice(cursor)][0];
|
|
1647
|
+
return cursor + ch.length;
|
|
1648
|
+
});
|
|
1545
1649
|
}
|
|
1546
1650
|
else if (key.home) {
|
|
1547
1651
|
setHistoryCursor(0);
|
|
@@ -1589,11 +1693,25 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1589
1693
|
return;
|
|
1590
1694
|
}
|
|
1591
1695
|
if (isMod(key) && input === 't') {
|
|
1592
|
-
// Ctrl+T
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
1696
|
+
// Ctrl+T means "expand what is in front of you", and the two things it
|
|
1697
|
+
// can expand never share the screen.
|
|
1698
|
+
//
|
|
1699
|
+
// The startup loaded-context panel renders only while the transcript is
|
|
1700
|
+
// empty (see the render below) — and that is exactly the window where
|
|
1701
|
+
// the trajectory has nothing in it yet, since the trajectory is folded
|
|
1702
|
+
// from the session's own events. So the panel wins while it is up, and
|
|
1703
|
+
// the scene takes over for the rest of the session.
|
|
1704
|
+
//
|
|
1705
|
+
// Claiming the key outright for the scene was wrong even though the
|
|
1706
|
+
// panel binding looked dead: the panel prints its own `(Ctrl+T 展开)`
|
|
1707
|
+
// hint, so the key still had a visible promise attached to it, and the
|
|
1708
|
+
// scene it opened instead was necessarily empty at that moment. That
|
|
1709
|
+
// left the panel reachable only by clicking its header, which is not a
|
|
1710
|
+
// keyboard path at all.
|
|
1711
|
+
if (loadedContextVisible) {
|
|
1712
|
+
setLoadedContextOpen(previous => !previous);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1597
1715
|
openScene();
|
|
1598
1716
|
return;
|
|
1599
1717
|
}
|
|
@@ -1689,6 +1807,32 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1689
1807
|
// Working-activity line (spinner slot): context-pressure prefix shares the
|
|
1690
1808
|
// StatusLine thresholds (amber ≥ 80, red ≥ 95).
|
|
1691
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
|
+
}
|
|
1692
1836
|
// The browser is a screen, not an overlay: it REPLACES the conversation
|
|
1693
1837
|
// rather than floating above it. Rendering it as an early return (after
|
|
1694
1838
|
// every hook above has run) is what makes that literal — there is no
|
|
@@ -1699,8 +1843,15 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1699
1843
|
// hosts are already in it and must not nest a second one.
|
|
1700
1844
|
return fullscreen ? browser : _jsx(AlternateScreen, { children: browser });
|
|
1701
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
|
+
}
|
|
1702
1853
|
/** Prompt input is inert while a modal dialog owns the keyboard. */
|
|
1703
|
-
const promptSelectionActive = selectionActive || modelPickerOpen || workspacePickerOpen || workspaceFlow !== null || activityPickerOpen ||
|
|
1854
|
+
const promptSelectionActive = selectionActive || modelPickerOpen || skillsPickerOpen || workspacePickerOpen || workspaceFlow !== null || activityPickerOpen ||
|
|
1704
1855
|
effortSliderOpen || presetPickerOpen || themePickerOpen || thinkingOpen || historyOpen || rewindOpen || searchOpen ||
|
|
1705
1856
|
btw !== null;
|
|
1706
1857
|
// The trajectory scene replaces the conversation for as long as it is open.
|
|
@@ -1721,7 +1872,7 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1721
1872
|
// 常驻、只移除其普通子节点,blit 解毒不触发,被覆盖的转录行会在
|
|
1722
1873
|
// blit-skip 后留空(Esc 关 picker 一片空白的根因)。
|
|
1723
1874
|
const dialogOverlayOpen = thinkingOpen || (workspacePickerOpen && workspaceTargets.length > 0) || workspaceFlow !== null ||
|
|
1724
|
-
modelPickerOpen ||
|
|
1875
|
+
modelPickerOpen || skillsPickerOpen ||
|
|
1725
1876
|
activityPickerOpen || (effortSliderOpen && effortOptions.length > 1) ||
|
|
1726
1877
|
(presetPickerOpen && presetOptions.length > 0) || themePickerOpen || historyOpen ||
|
|
1727
1878
|
rewindOpen || searchOpen;
|
|
@@ -1732,11 +1883,16 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1732
1883
|
seekRow(lastUser.id);
|
|
1733
1884
|
else
|
|
1734
1885
|
handle?.scrollToBottom();
|
|
1735
|
-
} })), _jsxs(ScrollBox, { ref: setHandle, flexDirection: "column", flexGrow: 1, flexShrink: 1, stickyScroll: true, children: [_jsx(LogoHeader, { model: channel.model, effort: channel.reasoningEffort, cwd: channel.displayCwd }),
|
|
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 &&
|
|
1736
1887
|
(channel.activityEnabled &&
|
|
1737
1888
|
channel.workingActivity !== undefined &&
|
|
1738
1889
|
channel.workingActivity.line !== '' &&
|
|
1739
|
-
channel.workingActivity.phase !== 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(ActivityLine, { activity: channel.workingActivity, activityFrames: channel.activityFrames, warnPct: activityWarnPct, warnDanger: activityWarnPct !== undefined && activityWarnPct >= 95,
|
|
1890
|
+
channel.workingActivity.phase !== 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(ActivityLine, { activity: channel.workingActivity, activityFrames: channel.activityFrames, warnPct: activityWarnPct, warnDanger: activityWarnPct !== undefined && activityWarnPct >= 95,
|
|
1891
|
+
// Upload = real tokens of the last request; download =
|
|
1892
|
+
// the animated chars/4 estimate, matching the classic
|
|
1893
|
+
// spinner's counter (the suffix used raw chars before,
|
|
1894
|
+
// inflating the reading next to a real upload number).
|
|
1895
|
+
suffix: `${lastUploadTokens > 0 ? ` · ↑ ${formatTokens(lastUploadTokens)}` : ''} · ↓ ${formatTokens(Math.round(channel.responseChars / 4))} tokens` }) })) : (_jsx(WorkingSpinner, { mode: channel.spinnerMode, hasActiveTools: channel.activeToolCount > 0, responseLengthRef: responseLengthRef, uploadTokensRef: uploadTokensRef, loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, thinkingStatus: thinkingStatus }))), _jsx(GoalTodoPanel, { channel: channel }), approvalSnapshot !== null ? (_jsx(ApprovalPanel, { approval: approvalSnapshot, onDecide: outcome => approvals.decide(outcome) }, approvalSnapshot.key)) : btw !== null ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(BtwPanel, { question: btw.question, answer: btw.answer, error: btw.error, streaming: !btw.done, onClose: closeBtw, onCopy: () => {
|
|
1740
1896
|
void setClipboard(btw.answer ?? '').then(raw => { if (raw)
|
|
1741
1897
|
process.stdout.write(raw); });
|
|
1742
1898
|
channel.notify(t('copied-chars', { n: (btw.answer ?? '').length }), { timeoutMs: 1500 });
|
|
@@ -1746,7 +1902,7 @@ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate,
|
|
|
1746
1902
|
band: wakeBand,
|
|
1747
1903
|
hint: trajectorySeen ? undefined : `${modLabel}t`,
|
|
1748
1904
|
tick: Math.floor(wakeTime / 120),
|
|
1749
|
-
} }), 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 })] }))] })] }));
|
|
1750
1906
|
}
|
|
1751
1907
|
/**
|
|
1752
1908
|
* The pinned prompt header shown above the ScrollBox while the user has
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionBrowser.d.ts","sourceRoot":"","sources":["../../../src/screens/SessionBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAsBzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAoDxD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,IAAI,EACJ,WAAW,EACX,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"SessionBrowser.d.ts","sourceRoot":"","sources":["../../../src/screens/SessionBrowser.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAsBzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAoDxD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,IAAI,EACJ,WAAW,EACX,OAAO,GACR,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAA;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,GAAG,KAAK,CAAC,SAAS,CAselB"}
|
|
@@ -112,6 +112,10 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
|
|
|
112
112
|
const [previewOpen, setPreviewOpen] = React.useState(false);
|
|
113
113
|
const [preview, setPreview] = React.useState([]);
|
|
114
114
|
const [previewLoading, setPreviewLoading] = React.useState(false);
|
|
115
|
+
// React batches every parsed key from one stdin chunk. Lock actions in a
|
|
116
|
+
// ref so a repeated Enter cannot start the same async operation twice
|
|
117
|
+
// before the mode change is rendered.
|
|
118
|
+
const actionPendingRef = React.useRef(false);
|
|
115
119
|
// One clock per render pass: every relative time on screen must agree, and
|
|
116
120
|
// re-reading it per row would let two rows a millisecond apart round to
|
|
117
121
|
// different minutes.
|
|
@@ -237,9 +241,17 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
|
|
|
237
241
|
setFilters(current => ({ ...current, ...update(current) }));
|
|
238
242
|
topRef.current = 0;
|
|
239
243
|
};
|
|
244
|
+
const runAction = (action) => {
|
|
245
|
+
if (actionPendingRef.current)
|
|
246
|
+
return;
|
|
247
|
+
actionPendingRef.current = true;
|
|
248
|
+
void action().finally(() => {
|
|
249
|
+
actionPendingRef.current = false;
|
|
250
|
+
});
|
|
251
|
+
};
|
|
240
252
|
/** Run one mutation, report it, and re-list — reporting a failure either way. */
|
|
241
253
|
const mutate = (action, done, failed) => {
|
|
242
|
-
|
|
254
|
+
runAction(async () => {
|
|
243
255
|
let ok = false;
|
|
244
256
|
let reason;
|
|
245
257
|
try {
|
|
@@ -250,7 +262,7 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
|
|
|
250
262
|
}
|
|
251
263
|
report(ok ? done : reason === undefined ? failed : `${failed} · ${reason}`, ok ? 'info' : 'error');
|
|
252
264
|
await reload();
|
|
253
|
-
})
|
|
265
|
+
});
|
|
254
266
|
};
|
|
255
267
|
const runDelete = (target) => mutate(() => channel.deleteSession(target.id), t('resume-deleted', { name: target.title.text }), t('resume-delete-failed', { name: target.title.text }));
|
|
256
268
|
const runRename = (target, title) => mutate(() => channel.renameSessionTo(target.id, title), t('rename-done', { title }), t('resume-rename-failed', { name: target.title.text }));
|
|
@@ -259,7 +271,7 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
|
|
|
259
271
|
// below, and deleting from a list that moved under us would be a
|
|
260
272
|
// destructive action aimed at whatever happens to be there now.
|
|
261
273
|
const ids = [...view.emptyIds];
|
|
262
|
-
|
|
274
|
+
runAction(async () => {
|
|
263
275
|
let removed = 0;
|
|
264
276
|
for (const id of ids) {
|
|
265
277
|
try {
|
|
@@ -272,9 +284,11 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
|
|
|
272
284
|
}
|
|
273
285
|
report(t('session-cleaned', { n: removed }), 'info');
|
|
274
286
|
await reload();
|
|
275
|
-
})
|
|
287
|
+
});
|
|
276
288
|
};
|
|
277
289
|
useInput((input, key) => {
|
|
290
|
+
if (actionPendingRef.current)
|
|
291
|
+
return;
|
|
278
292
|
// A notice describes what the LAST action did; the next keystroke makes it
|
|
279
293
|
// stale, so it goes as soon as the user acts again.
|
|
280
294
|
if (notice !== undefined)
|
|
@@ -338,19 +352,20 @@ export function SessionBrowser({ channel, home, sameProject, onClose, }) {
|
|
|
338
352
|
// that explanation to the conversation the user is not looking at, and
|
|
339
353
|
// leave them staring at an unchanged transcript wondering what Enter
|
|
340
354
|
// did. `resumeTo` reports its own reasons; this only has to stay put.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
355
|
+
runAction(async () => {
|
|
356
|
+
try {
|
|
357
|
+
const ok = await channel.resumeTo(target.id);
|
|
358
|
+
if (ok) {
|
|
359
|
+
channel.notify(t('resume-resumed'));
|
|
360
|
+
onClose();
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
setNotice({ text: t('session-resume-refused'), tone: 'error' });
|
|
364
|
+
}
|
|
347
365
|
}
|
|
348
|
-
|
|
349
|
-
setNotice({ text: t('session-resume-
|
|
366
|
+
catch (error) {
|
|
367
|
+
setNotice({ text: t('session-resume-failed', { err: message(error) }), tone: 'error' });
|
|
350
368
|
}
|
|
351
|
-
})
|
|
352
|
-
.catch((error) => {
|
|
353
|
-
setNotice({ text: t('session-resume-failed', { err: message(error) }), tone: 'error' });
|
|
354
369
|
});
|
|
355
370
|
}
|
|
356
371
|
else if (key.escape) {
|
|
@@ -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"}
|