@aiwayds/dsh-tui-pi 2.19.0 → 2.20.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 +12 -48
- package/README.zh-CN.md +11 -47
- package/lib/agents.js +67 -52
- package/lib/agents.js.map +1 -1
- package/lib/ask-user.js +30 -30
- package/lib/ask-user.js.map +1 -1
- package/lib/attachments.js +8 -3
- package/lib/attachments.js.map +1 -1
- package/lib/btw.d.ts +4 -2
- package/lib/btw.js +22 -16
- package/lib/btw.js.map +1 -1
- package/lib/custom-provider.js +34 -28
- package/lib/custom-provider.js.map +1 -1
- package/lib/footer.d.ts +9 -8
- package/lib/footer.js +36 -18
- package/lib/footer.js.map +1 -1
- package/lib/hard-exit.d.ts +36 -0
- package/lib/hard-exit.js +64 -0
- package/lib/hard-exit.js.map +1 -0
- package/lib/history-fork.d.ts +2 -2
- package/lib/history-fork.js +12 -9
- package/lib/history-fork.js.map +1 -1
- package/lib/history-turns.d.ts +4 -2
- package/lib/history-turns.js +7 -3
- package/lib/history-turns.js.map +1 -1
- package/lib/history.js +57 -45
- package/lib/history.js.map +1 -1
- package/lib/hotkeys.js +44 -33
- package/lib/hotkeys.js.map +1 -1
- package/lib/i18n/index.d.ts +54 -0
- package/lib/i18n/index.js +98 -0
- package/lib/i18n/index.js.map +1 -0
- package/lib/i18n/registry.d.ts +65 -0
- package/lib/i18n/registry.js +129 -0
- package/lib/i18n/registry.js.map +1 -0
- package/lib/index.js +216 -108
- package/lib/index.js.map +1 -1
- package/lib/language-ask.d.ts +98 -0
- package/lib/language-ask.js +118 -0
- package/lib/language-ask.js.map +1 -0
- package/lib/live-widgets.js +9 -8
- package/lib/live-widgets.js.map +1 -1
- package/lib/login.js +5 -4
- package/lib/login.js.map +1 -1
- package/lib/messages.d.ts +0 -4
- package/lib/messages.js +14 -15
- package/lib/messages.js.map +1 -1
- package/lib/model-profiles.js +32 -20
- package/lib/model-profiles.js.map +1 -1
- package/lib/panels.js +18 -17
- package/lib/panels.js.map +1 -1
- package/lib/preflight-projcache.js +9 -3
- package/lib/preflight-projcache.js.map +1 -1
- package/lib/preset-dialog.d.ts +3 -3
- package/lib/preset-dialog.js +20 -17
- package/lib/preset-dialog.js.map +1 -1
- package/lib/provider-catalog.d.ts +23 -7
- package/lib/provider-catalog.js +90 -68
- package/lib/provider-catalog.js.map +1 -1
- package/lib/queue-panel.d.ts +2 -2
- package/lib/queue-panel.js +13 -9
- package/lib/queue-panel.js.map +1 -1
- package/lib/repair-dialog.d.ts +4 -4
- package/lib/repair-dialog.js +23 -16
- package/lib/repair-dialog.js.map +1 -1
- package/lib/route-dialog.d.ts +3 -3
- package/lib/route-dialog.js +20 -14
- package/lib/route-dialog.js.map +1 -1
- package/lib/selectors.d.ts +15 -1
- package/lib/selectors.js +57 -23
- package/lib/selectors.js.map +1 -1
- package/lib/sessions.d.ts +47 -14
- package/lib/sessions.js +162 -53
- package/lib/sessions.js.map +1 -1
- package/lib/settings.d.ts +68 -9
- package/lib/settings.js +327 -78
- package/lib/settings.js.map +1 -1
- package/lib/skills-manager.js +27 -21
- package/lib/skills-manager.js.map +1 -1
- package/lib/skills.js +5 -4
- package/lib/skills.js.map +1 -1
- package/lib/startup-info.js +9 -3
- package/lib/startup-info.js.map +1 -1
- package/lib/steer-flow.d.ts +14 -4
- package/lib/steer-flow.js +24 -9
- package/lib/steer-flow.js.map +1 -1
- package/lib/stop-dialog.d.ts +2 -2
- package/lib/stop-dialog.js +16 -12
- package/lib/stop-dialog.js.map +1 -1
- package/lib/subagent-viewer.d.ts +3 -6
- package/lib/subagent-viewer.js +38 -36
- package/lib/subagent-viewer.js.map +1 -1
- package/lib/theme-preview.js +12 -11
- package/lib/theme-preview.js.map +1 -1
- package/lib/theme-settings.d.ts +23 -2
- package/lib/theme-settings.js +68 -61
- package/lib/theme-settings.js.map +1 -1
- package/lib/tui.js +2 -1
- package/lib/tui.js.map +1 -1
- package/locales/en.json +646 -0
- package/locales/ja.json +646 -0
- package/locales/ko.json +646 -0
- package/locales/zh-CN.json +646 -0
- package/package.json +2 -1
- package/skills/dsh-tui-pi-config/SKILL.md +9 -2
package/lib/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import { dirname, join, resolve } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { hardExit } from "./hard-exit.js";
|
|
15
16
|
/** Mirrors dsh-skill's bundled-skill rank (a non-load-bearing ordering hint;
|
|
16
17
|
* the constant is hardcoded there too). Local copy — see the type-import
|
|
17
18
|
* note above for why dsh-skill is not loaded at runtime here. */
|
|
@@ -27,7 +28,8 @@ import { AGENT_TICK_MS, LiveWidgets } from "./live-widgets.js";
|
|
|
27
28
|
import { displayPermissionPreset } from "./permission.js";
|
|
28
29
|
import { pickEffort, pickModel, pickPermission, pickPreset, pickTheme } from "./selectors.js";
|
|
29
30
|
import { DshSessionBridge, persistDefaultModel, stashSessionIdForReload, takeStashedSessionId } from "./session.js";
|
|
30
|
-
import { currentCacheHitMode, currentRememberPreset, currentThemePreference, readAskUserExplicit, readFooterHintsPreference, readIconSetPreference, readPanelHeightPreference, readRememberPreset, readSessionManagementExplicit, readSubagentLimits, readThemePreference, registerThemeSettings, writeThemePreference, } from "./theme-settings.js";
|
|
31
|
+
import { currentCacheHitMode, currentRememberPreset, currentThemePreference, readAskUserExplicit, readFooterHintsPreference, readIconSetPreference, readLanguagePreference, readPanelHeightPreference, readRememberPreset, readSessionManagementExplicit, readSubagentLimits, readThemePreference, registerThemeSettings, writeLanguagePreference, writeThemePreference, } from "./theme-settings.js";
|
|
32
|
+
import { currentLocale, initI18n, listLocales, setLocale, t } from "./i18n/index.js";
|
|
31
33
|
import { loadWorkspacePresets, rememberedPresetFor, saveWorkspacePresets, withRememberedPreset, workspacePresetsPath, } from "./workspace-presets.js";
|
|
32
34
|
import { applyIconSet, resolveIconSet, stopIcon } from "./icons.js";
|
|
33
35
|
import { readAgentMaxRounds } from "./agent-runtime.js";
|
|
@@ -52,9 +54,9 @@ import { openSubagentViewer } from "./subagent-viewer.js";
|
|
|
52
54
|
import { openHistoryBrowser } from "./history.js";
|
|
53
55
|
import { openForkAtTurnDialog } from "./history-fork.js";
|
|
54
56
|
import { commandUsagePath, CommandUsageTracker } from "./usage.js";
|
|
55
|
-
import { buildUserPrompt, decideSubmitPath, deliverToAgent, promotePending, removeFromInbox,
|
|
57
|
+
import { buildUserPrompt, decideSubmitPath, deliverToAgent, promotePending, removeFromInbox, steerUnavailableNotice, turnEndedQueuedNotice, } from "./steer-flow.js";
|
|
56
58
|
import { openSubmitRouteDialog } from "./route-dialog.js";
|
|
57
|
-
import { BtwController,
|
|
59
|
+
import { BtwController, btwIdleNotice, btwUsage, buildBtwSnapshot, parseBtwInput, resolveSnapshotLimit, } from "./btw.js";
|
|
58
60
|
import { BtwOverlayWire } from "./btw-overlay.js";
|
|
59
61
|
import { openPendingQueuePanel } from "./queue-panel.js";
|
|
60
62
|
import { ansiFg, BOLD, RESET, resolveTheme } from "./theme/index.js";
|
|
@@ -68,6 +70,7 @@ import { currentPreset, fetchPresetRoster, findPresetByName, formatPresetLabel,
|
|
|
68
70
|
import { completedTurnSeed, openPresetConfirmDialog, performPresetSwitch } from "./preset-dialog.js";
|
|
69
71
|
import { openStopConfirmDialog } from "./stop-dialog.js";
|
|
70
72
|
import { registerAskUserProvider, resolveAskUserTimeouts } from "./ask-user.js";
|
|
73
|
+
import { askLanguageChoice } from "./language-ask.js";
|
|
71
74
|
import { checkHostSupport } from "./host-version.js";
|
|
72
75
|
export const name = 'dsh-tui-pi';
|
|
73
76
|
/**
|
|
@@ -99,7 +102,7 @@ const SKILL_RESOURCE_BASE = {
|
|
|
99
102
|
};
|
|
100
103
|
const SKILL_INVOCATION = { modelInvocable: true, userInvocable: true };
|
|
101
104
|
/** Routing description; must stay identical to the SKILL.md frontmatter (asserted in tests). */
|
|
102
|
-
const SKILL_DESCRIPTION = 'dsh TUI
|
|
105
|
+
const SKILL_DESCRIPTION = 'dsh TUI 增强套件使用与配置指南。凡涉及 TUI 主题/面板/footer、界面语言、子代理并发与轮数限制、模型收藏与隐藏、会话保留与 /resume 过滤、ask_user 超时、preset 记忆,或要配置 dsh-tui 段时先读本指南:settings.yaml 顶层 `dsh-tui:` 段 18 键(language/theme/panelHeight/maxAgents/maxRounds/maxRoundsGrace/disableSubagent/registeredOnly/footerHints/cacheHitMode/iconSet/rememberPreset/favoriteModels/hiddenModels/retention/resume/askUser)、DSH_TUI_* 环境变量、快速上手向导、keybindings.json 与 /hotkeys。触发词:tui、主题、theme、面板、footer、语言、language、收藏模型、隐藏模型、保留策略、panelHeight、resume、preset。';
|
|
103
106
|
const SKILL_CANDIDATE = {
|
|
104
107
|
name: SKILL_PROVIDER_NAME,
|
|
105
108
|
description: SKILL_DESCRIPTION,
|
|
@@ -164,7 +167,7 @@ export function apply(ctx) {
|
|
|
164
167
|
const check = checkHostSupport();
|
|
165
168
|
if (!check.ok) {
|
|
166
169
|
console.warn(check.message);
|
|
167
|
-
|
|
170
|
+
hardExit(1);
|
|
168
171
|
}
|
|
169
172
|
if (check.version === undefined) {
|
|
170
173
|
console.warn('[dsh-tui-pi] could not resolve the dsh host version next to the plugin; skipping the host floor check.');
|
|
@@ -247,7 +250,8 @@ export function apply(ctx) {
|
|
|
247
250
|
// setTheme replay that follows already renders at that new height.
|
|
248
251
|
// applyTheme carries the theme-bundle identity guard, so a height-only
|
|
249
252
|
// commit never triggers a second rebuild.
|
|
250
|
-
registerThemeSettings(ctx, (pref, height, footerHints, iconSet) => {
|
|
253
|
+
registerThemeSettings(ctx, (pref, height, footerHints, iconSet, language) => {
|
|
254
|
+
setLocale(language);
|
|
251
255
|
applyPanelHeightRef?.(height);
|
|
252
256
|
applyThemeRef?.(pref);
|
|
253
257
|
applyFooterHintsRef?.(footerHints);
|
|
@@ -286,6 +290,12 @@ export function apply(ctx) {
|
|
|
286
290
|
const themePreference = await readThemePreference(ctx);
|
|
287
291
|
const panelHeight = await readPanelHeightPreference(ctx);
|
|
288
292
|
const footerHints = await readFooterHintsPreference(ctx);
|
|
293
|
+
// UI language (`dsh-tui.language`, default 'en'): discover the language
|
|
294
|
+
// files and activate the persisted id BEFORE the TUI's first frame, so
|
|
295
|
+
// the boot UI speaks it from the start. The registration's watch hook
|
|
296
|
+
// re-applies later commits live via setLocale. An unknown id degrades to
|
|
297
|
+
// 'en' inside initI18n.
|
|
298
|
+
initI18n({ id: await readLanguagePreference(ctx), warn: message => emitNotice(message) });
|
|
289
299
|
// Icon-set self-adaptation: probe the platform once at startup (the
|
|
290
300
|
// memoised snapshot shared with every later 'auto' resolution), resolve
|
|
291
301
|
// the persisted mode against it and push the result into src/icons.ts
|
|
@@ -401,7 +411,7 @@ export function apply(ctx) {
|
|
|
401
411
|
// so a stray Esc can never kill a running task. The notice keeps
|
|
402
412
|
// the armed first press from feeling dead and states the
|
|
403
413
|
// contract explicitly.
|
|
404
|
-
renderer.renderNotice('
|
|
414
|
+
renderer.renderNotice(t('tui.esc.armStop'), 'info');
|
|
405
415
|
break;
|
|
406
416
|
case 'interrupt-cancel': {
|
|
407
417
|
// Second Esc while LLM work runs (main turn and/or subagents):
|
|
@@ -444,7 +454,7 @@ export function apply(ctx) {
|
|
|
444
454
|
void disposeAndExit(0);
|
|
445
455
|
break;
|
|
446
456
|
}
|
|
447
|
-
renderer.renderNotice(
|
|
457
|
+
renderer.renderNotice(t('tui.esc.quitArmed', { icon: stopIcon() }), 'info');
|
|
448
458
|
quitConfirmTimer = setTimeout(() => {
|
|
449
459
|
quitConfirmTimer = undefined;
|
|
450
460
|
void disposeAndExit(0);
|
|
@@ -459,7 +469,7 @@ export function apply(ctx) {
|
|
|
459
469
|
if (action.key === 'ctrl-c' && quitConfirmTimer !== undefined) {
|
|
460
470
|
clearTimeout(quitConfirmTimer);
|
|
461
471
|
quitConfirmTimer = undefined;
|
|
462
|
-
renderer.renderNotice('
|
|
472
|
+
renderer.renderNotice(t('tui.esc.quitAborted'), 'info');
|
|
463
473
|
}
|
|
464
474
|
break;
|
|
465
475
|
case 'ctrl-d-quit':
|
|
@@ -486,7 +496,7 @@ export function apply(ctx) {
|
|
|
486
496
|
onRemove: item => {
|
|
487
497
|
const agent = bridge.getAgent();
|
|
488
498
|
if (agent === undefined)
|
|
489
|
-
return { kind: 'error', error: '
|
|
499
|
+
return { kind: 'error', error: t('tui.session.none') };
|
|
490
500
|
const result = removeFromInbox(agent.inbox, item);
|
|
491
501
|
// Review B1: a successful revoke must retire the transcript
|
|
492
502
|
// badge — the echo bubble becomes an explicit canceled line
|
|
@@ -499,7 +509,7 @@ export function apply(ctx) {
|
|
|
499
509
|
onPromote: item => {
|
|
500
510
|
const agent = bridge.getAgent();
|
|
501
511
|
if (agent === undefined)
|
|
502
|
-
return { kind: 'error', error: '
|
|
512
|
+
return { kind: 'error', error: t('tui.session.none') };
|
|
503
513
|
const result = promotePending(agent, item);
|
|
504
514
|
if (result.kind === 'promoted' && result.degraded) {
|
|
505
515
|
// Review S3: the steer degraded back into the queue — flip
|
|
@@ -516,7 +526,7 @@ export function apply(ctx) {
|
|
|
516
526
|
},
|
|
517
527
|
onOutcome: (result) => {
|
|
518
528
|
if (result.kind === 'promoted' && result.degraded) {
|
|
519
|
-
renderer.renderNotice(
|
|
529
|
+
renderer.renderNotice(steerUnavailableNotice(), 'info');
|
|
520
530
|
}
|
|
521
531
|
else if (result.kind === 'error') {
|
|
522
532
|
renderer.renderNotice(result.error, 'error');
|
|
@@ -586,7 +596,7 @@ export function apply(ctx) {
|
|
|
586
596
|
* dialog's Stop choice (double-Esc) and the first Ctrl+C mid-task.
|
|
587
597
|
*/
|
|
588
598
|
const stopTask = async () => {
|
|
589
|
-
renderer.renderNotice(
|
|
599
|
+
renderer.renderNotice(t('tui.stop.stopping', { icon: stopIcon() }), 'info');
|
|
590
600
|
// The stop gesture is the everything-stop: side calls die with the turn.
|
|
591
601
|
btwController.cancelAll();
|
|
592
602
|
const cancelled = await bridge.cancelActiveTurn();
|
|
@@ -594,7 +604,7 @@ export function apply(ctx) {
|
|
|
594
604
|
// State raced idle between the decision and the cancel calls — nothing
|
|
595
605
|
// to cancel (e.g. the task settled while the dialog was up).
|
|
596
606
|
if (!cancelled && stoppedChildren === 0)
|
|
597
|
-
renderer.renderNotice('
|
|
607
|
+
renderer.renderNotice(t('tui.stop.nothingRunning'), 'info');
|
|
598
608
|
};
|
|
599
609
|
// Arm the settings watch sink now that the renderer exists (see apply()).
|
|
600
610
|
applyThemeRef = (pref) => {
|
|
@@ -608,7 +618,7 @@ export function apply(ctx) {
|
|
|
608
618
|
// Startup notices for a broken/misleading keybindings file — the TUI keeps
|
|
609
619
|
// running with defaults; the panel shows the same warnings via /hotkeys.
|
|
610
620
|
for (const warning of keyBindings.warnings) {
|
|
611
|
-
renderer.renderNotice(
|
|
621
|
+
renderer.renderNotice(t('tui.keys.warningPrefix', { warning }), 'error');
|
|
612
622
|
}
|
|
613
623
|
// Arm the shared notice sink now that the TUI is up. Every operator
|
|
614
624
|
// trace that fired before this point — the settings-namespace
|
|
@@ -663,7 +673,7 @@ export function apply(ctx) {
|
|
|
663
673
|
agentStatus = status;
|
|
664
674
|
if (status === 'running') {
|
|
665
675
|
if (loader === undefined) {
|
|
666
|
-
loader = new Loader(ui.tui, text => BOLD + ansiFg(ui.theme.palette.accent) + text + RESET, text => BOLD + ansiFg(ui.theme.palette.accent) + text + RESET, 'working
|
|
676
|
+
loader = new Loader(ui.tui, text => BOLD + ansiFg(ui.theme.palette.accent) + text + RESET, text => BOLD + ansiFg(ui.theme.palette.accent) + text + RESET, t('tui.status.working'));
|
|
667
677
|
ui.status.addChild(loader);
|
|
668
678
|
loader.start();
|
|
669
679
|
}
|
|
@@ -738,7 +748,7 @@ export function apply(ctx) {
|
|
|
738
748
|
for (const text of bridge.takePendingRemoteFollowups()) {
|
|
739
749
|
await bridge.prompt(text);
|
|
740
750
|
}
|
|
741
|
-
emitNotice('
|
|
751
|
+
emitNotice(t('tui.takeover.writeLock'));
|
|
742
752
|
ui.requestRender();
|
|
743
753
|
},
|
|
744
754
|
};
|
|
@@ -895,7 +905,7 @@ export function apply(ctx) {
|
|
|
895
905
|
stream: options => {
|
|
896
906
|
const llm = ctx.get('llm');
|
|
897
907
|
if (llm === undefined)
|
|
898
|
-
throw new Error('
|
|
908
|
+
throw new Error(t('tui.btw.noLlm'));
|
|
899
909
|
return llm.stream({
|
|
900
910
|
provider: options.provider,
|
|
901
911
|
model: options.model,
|
|
@@ -929,10 +939,10 @@ export function apply(ctx) {
|
|
|
929
939
|
if (parsed.kind === 'empty') {
|
|
930
940
|
const opened = btwController.openReview();
|
|
931
941
|
return opened === 'live'
|
|
932
|
-
? { kind: 'success', text: 'btw
|
|
942
|
+
? { kind: 'success', text: t('tui.btw.backOnScreen') }
|
|
933
943
|
: opened === 'review'
|
|
934
|
-
? { kind: 'success', text: '
|
|
935
|
-
: { kind: 'success', text:
|
|
944
|
+
? { kind: 'success', text: t('tui.btw.lastShown') }
|
|
945
|
+
: { kind: 'success', text: btwUsage() };
|
|
936
946
|
}
|
|
937
947
|
if (parsed.kind === 'error')
|
|
938
948
|
return { kind: 'error', text: parsed.error };
|
|
@@ -940,7 +950,7 @@ export function apply(ctx) {
|
|
|
940
950
|
// this must be the live view, not a read-only remote one) — when idle,
|
|
941
951
|
// a normal prompt is strictly better (tools, history, full context).
|
|
942
952
|
if (!(bridge.isRunning() && !bridge.isReadOnlyView())) {
|
|
943
|
-
return { kind: 'error', text:
|
|
953
|
+
return { kind: 'error', text: btwIdleNotice() };
|
|
944
954
|
}
|
|
945
955
|
const result = btwController.submit({
|
|
946
956
|
question: parsed.question,
|
|
@@ -948,14 +958,14 @@ export function apply(ctx) {
|
|
|
948
958
|
});
|
|
949
959
|
switch (result.kind) {
|
|
950
960
|
case 'started':
|
|
951
|
-
return { kind: 'success', text: 'btw
|
|
961
|
+
return { kind: 'success', text: t('tui.btw.started') };
|
|
952
962
|
case 'queued':
|
|
953
|
-
return { kind: 'success', text:
|
|
963
|
+
return { kind: 'success', text: t('tui.btw.queued', { position: result.position }) };
|
|
954
964
|
case 'rejected':
|
|
955
|
-
return { kind: 'error', text:
|
|
965
|
+
return { kind: 'error', text: t('tui.btw.rejected', { reason: result.reason }) };
|
|
956
966
|
}
|
|
957
967
|
};
|
|
958
|
-
registerLocalCommand('btw', '
|
|
968
|
+
registerLocalCommand('btw', t('tui.cmd.btw.description'), btwHandler);
|
|
959
969
|
// ------------------------------------------- TUI-owned slash commands --
|
|
960
970
|
// Web-surface parity: `model` is a browser client contribution there and
|
|
961
971
|
// `export` a web-only download plugin — the terminal gets native
|
|
@@ -966,7 +976,7 @@ export function apply(ctx) {
|
|
|
966
976
|
const modelHandler = async () => {
|
|
967
977
|
const picked = await pickModel(ctx, ui.tui, ui.theme, bridge.getSelection(), refocusEditor);
|
|
968
978
|
if (picked === undefined)
|
|
969
|
-
return { kind: 'success', text: '
|
|
979
|
+
return { kind: 'success', text: t('tui.model.unchanged') };
|
|
970
980
|
const llm = ctx.get('llm');
|
|
971
981
|
if (llm !== undefined) {
|
|
972
982
|
await llm.resolveCallConfig({ provider: picked.provider, model: picked.model });
|
|
@@ -975,14 +985,14 @@ export function apply(ctx) {
|
|
|
975
985
|
const persistError = await persistDefaultModel(ctx, picked);
|
|
976
986
|
ui.requestRender();
|
|
977
987
|
const modelText = picked.reasoningEffort === undefined
|
|
978
|
-
?
|
|
979
|
-
:
|
|
988
|
+
? t('tui.model.selected', { id: `${picked.provider}/${picked.model}` })
|
|
989
|
+
: t('tui.model.selectedThink', { id: `${picked.provider}/${picked.model}`, effort: String(picked.reasoningEffort) });
|
|
980
990
|
return {
|
|
981
991
|
kind: 'success',
|
|
982
|
-
text: persistError === undefined ? modelText :
|
|
992
|
+
text: persistError === undefined ? modelText : t('tui.persist.failed', { text: modelText, error: persistError }),
|
|
983
993
|
};
|
|
984
994
|
};
|
|
985
|
-
registerLocalCommand('model', '
|
|
995
|
+
registerLocalCommand('model', t('tui.cmd.model.description'), modelHandler);
|
|
986
996
|
// /agents: manage agent definition markdown files (model, think level,
|
|
987
997
|
// spawn depth) — the terminal counterpart of pi's /fun-agent-cfg. An
|
|
988
998
|
// optional argument preselects one agent by name.
|
|
@@ -990,44 +1000,44 @@ export function apply(ctx) {
|
|
|
990
1000
|
const trimmed = rawInput?.trim() ?? '';
|
|
991
1001
|
const result = await openAgentManager(ctx, ui.tui, ui.theme, refocusEditor, trimmed === '' ? undefined : trimmed, () => subagentPolicy.getStats());
|
|
992
1002
|
if (result === undefined)
|
|
993
|
-
return { kind: 'success', text: '
|
|
1003
|
+
return { kind: 'success', text: t('tui.agents.unchanged') };
|
|
994
1004
|
return { kind: 'success', text: result };
|
|
995
1005
|
};
|
|
996
|
-
registerLocalCommand('agents', '
|
|
1006
|
+
registerLocalCommand('agents', t('tui.cmd.agents.description'), agentsHandler);
|
|
997
1007
|
// /subagents: the command twin of Ctrl+G — pick a running (or recently
|
|
998
1008
|
// settled) subagent and inspect its live transcript in the 80% viewer.
|
|
999
1009
|
// Same flow as the key path; empty board closes immediately.
|
|
1000
1010
|
const subagentsHandler = async () => {
|
|
1001
1011
|
await openSubagentViewer(ctx, ui.tui, ui.theme, bridge, refocusEditor);
|
|
1002
|
-
return { kind: 'success', text: '
|
|
1012
|
+
return { kind: 'success', text: t('tui.subagents.viewerClosed') };
|
|
1003
1013
|
};
|
|
1004
|
-
registerLocalCommand('subagents', '
|
|
1014
|
+
registerLocalCommand('subagents', t('tui.cmd.subagents.description'), subagentsHandler);
|
|
1005
1015
|
// /think: cycle the current model's reasoning effort without re-picking
|
|
1006
1016
|
// the model. A no-session /think still lands in the selection ref and
|
|
1007
1017
|
// survives the lazy session creation (bridge seeds only an empty ref).
|
|
1008
1018
|
const thinkHandler = async () => {
|
|
1009
1019
|
const current = bridge.getSelection();
|
|
1010
1020
|
if (current === undefined) {
|
|
1011
|
-
return { kind: 'error', text: '
|
|
1021
|
+
return { kind: 'error', text: t('tui.think.noModel') };
|
|
1012
1022
|
}
|
|
1013
1023
|
const result = await pickEffort(ctx, ui.tui, ui.theme, current, refocusEditor);
|
|
1014
1024
|
if (result.kind === 'unsupported') {
|
|
1015
1025
|
return {
|
|
1016
1026
|
kind: 'error',
|
|
1017
|
-
text: `${current.provider}/${current.model}
|
|
1027
|
+
text: t('tui.think.unsupported', { id: `${current.provider}/${current.model}` }),
|
|
1018
1028
|
};
|
|
1019
1029
|
}
|
|
1020
1030
|
if (result.kind === 'cancelled')
|
|
1021
|
-
return { kind: 'success', text: '
|
|
1031
|
+
return { kind: 'success', text: t('tui.think.unchanged') };
|
|
1022
1032
|
if (result.effort === 'default') {
|
|
1023
1033
|
const next = { provider: current.provider, model: current.model };
|
|
1024
1034
|
bridge.setSelection(next);
|
|
1025
1035
|
const persistError = await persistDefaultModel(ctx, next);
|
|
1026
1036
|
ui.requestRender();
|
|
1027
|
-
const thinkText =
|
|
1037
|
+
const thinkText = t('tui.think.providerDefault', { id: `${current.provider}/${current.model}` });
|
|
1028
1038
|
return {
|
|
1029
1039
|
kind: 'success',
|
|
1030
|
-
text: persistError === undefined ? thinkText :
|
|
1040
|
+
text: persistError === undefined ? thinkText : t('tui.persist.failed', { text: thinkText, error: persistError }),
|
|
1031
1041
|
};
|
|
1032
1042
|
}
|
|
1033
1043
|
const next = {
|
|
@@ -1038,13 +1048,13 @@ export function apply(ctx) {
|
|
|
1038
1048
|
bridge.setSelection(next);
|
|
1039
1049
|
const persistError = await persistDefaultModel(ctx, next);
|
|
1040
1050
|
ui.requestRender();
|
|
1041
|
-
const thinkText =
|
|
1051
|
+
const thinkText = t('tui.think.set', { effort: String(result.effort), id: `${current.provider}/${current.model}` });
|
|
1042
1052
|
return {
|
|
1043
1053
|
kind: 'success',
|
|
1044
|
-
text: persistError === undefined ? thinkText :
|
|
1054
|
+
text: persistError === undefined ? thinkText : t('tui.persist.failed', { text: thinkText, error: persistError }),
|
|
1045
1055
|
};
|
|
1046
1056
|
};
|
|
1047
|
-
registerLocalCommand('think', '
|
|
1057
|
+
registerLocalCommand('think', t('tui.cmd.think.description'), thinkHandler);
|
|
1048
1058
|
/**
|
|
1049
1059
|
* Record one committed preset as THIS workspace's remembered selection
|
|
1050
1060
|
* (`$DSH_HOME/workspace-presets.json`, keyed by the session backend's
|
|
@@ -1062,7 +1072,7 @@ export function apply(ctx) {
|
|
|
1062
1072
|
const doc = withRememberedPreset(loadWorkspacePresets(path), projectKeyFor(process.cwd()), presetId);
|
|
1063
1073
|
const error = saveWorkspacePresets(path, doc);
|
|
1064
1074
|
if (error !== undefined)
|
|
1065
|
-
renderer.renderNotice(
|
|
1075
|
+
renderer.renderNotice(t('tui.preset.memorySaveFailed', { error }), 'error');
|
|
1066
1076
|
}
|
|
1067
1077
|
catch {
|
|
1068
1078
|
// A broken store must never fail the switch itself.
|
|
@@ -1077,7 +1087,7 @@ export function apply(ctx) {
|
|
|
1077
1087
|
// directly (the first submit already creates the session on it).
|
|
1078
1088
|
const presetHandler = async (rawInput) => {
|
|
1079
1089
|
if (presetState.roster.length === 0) {
|
|
1080
|
-
return { kind: 'error', text: '
|
|
1090
|
+
return { kind: 'error', text: t('tui.preset.none') };
|
|
1081
1091
|
}
|
|
1082
1092
|
const arg = rawInput?.trim() ?? '';
|
|
1083
1093
|
let target;
|
|
@@ -1095,11 +1105,11 @@ export function apply(ctx) {
|
|
|
1095
1105
|
else {
|
|
1096
1106
|
target = findPresetByName(presetState, arg);
|
|
1097
1107
|
if (target === undefined) {
|
|
1098
|
-
return { kind: 'error', text:
|
|
1108
|
+
return { kind: 'error', text: t('tui.preset.unknown', { name: arg }) };
|
|
1099
1109
|
}
|
|
1100
1110
|
}
|
|
1101
1111
|
if (target === undefined)
|
|
1102
|
-
return { kind: 'success', text: '
|
|
1112
|
+
return { kind: 'success', text: t('tui.preset.unchanged') };
|
|
1103
1113
|
const outcome = await performPresetSwitch(presetState, target, { hasLiveSession: () => bridge.getAgent() !== undefined,
|
|
1104
1114
|
confirmSwitch: (name, restart) => openPresetConfirmDialog(ui.tui, ui.theme, name, restart, refocusEditor)
|
|
1105
1115
|
.then(outcome => (outcome === 'cancelled' ? 'cancel' : outcome)),
|
|
@@ -1181,7 +1191,7 @@ export function apply(ctx) {
|
|
|
1181
1191
|
persistRememberedPreset(target.id);
|
|
1182
1192
|
return { kind: 'success', text: outcome.message };
|
|
1183
1193
|
};
|
|
1184
|
-
registerLocalCommand('preset', '
|
|
1194
|
+
registerLocalCommand('preset', t('tui.cmd.preset.description'), presetHandler);
|
|
1185
1195
|
// Model profiles moved to the dsh-profile-switch plugin (ask-user based,
|
|
1186
1196
|
// surface-agnostic). What stays HERE is the live-selection bridge it
|
|
1187
1197
|
// consumes: the TUI's /model + /think ride the same selection ref, and
|
|
@@ -1198,18 +1208,37 @@ export function apply(ctx) {
|
|
|
1198
1208
|
},
|
|
1199
1209
|
current: () => bridge.getSelection(),
|
|
1200
1210
|
});
|
|
1211
|
+
/**
|
|
1212
|
+
* The host session-title seam (`@deepseek-ai/dsh-session-title`), read
|
|
1213
|
+
* structurally like every other optional host service: the host base
|
|
1214
|
+
* bundle mounts it (with the first-prompt LLM provider), but a stripped
|
|
1215
|
+
* profile without it must only degrade the /session panel to read-only,
|
|
1216
|
+
* never crash it. The rename error contract is duck-typed by name — the
|
|
1217
|
+
* class itself lives behind a module graph (zod) this plugin
|
|
1218
|
+
* deliberately does not import.
|
|
1219
|
+
*/
|
|
1220
|
+
const sessionTitleSeam = (() => {
|
|
1221
|
+
try {
|
|
1222
|
+
return ctx.get('sessionTitle');
|
|
1223
|
+
}
|
|
1224
|
+
catch {
|
|
1225
|
+
return undefined;
|
|
1226
|
+
}
|
|
1227
|
+
})();
|
|
1201
1228
|
const sessionHandler = async () => {
|
|
1202
1229
|
const agent = bridge.getAgent();
|
|
1203
1230
|
const stats = bridge.getStats();
|
|
1204
1231
|
const selection = bridge.getSelection();
|
|
1205
1232
|
const header = agent?.session.header;
|
|
1233
|
+
const liveSession = agent?.session;
|
|
1234
|
+
const titleOf = liveSession === undefined ? undefined : sessionTitleSeam?.get(liveSession)?.title;
|
|
1206
1235
|
await showSessionInfo(ui.tui, ui.theme, {
|
|
1207
1236
|
id: agent === undefined ? undefined : String(agent.session.id),
|
|
1208
1237
|
cwd: header?.cwd,
|
|
1209
1238
|
createdAt: header?.createdAt,
|
|
1210
1239
|
model: selection === undefined ? undefined : `${selection.provider}/${selection.model}`,
|
|
1211
1240
|
effort: selection === undefined || selection.reasoningEffort === undefined
|
|
1212
|
-
? (selection === undefined ? undefined : '
|
|
1241
|
+
? (selection === undefined ? undefined : t('tui.session.effortDefault'))
|
|
1213
1242
|
: String(selection.reasoningEffort),
|
|
1214
1243
|
msgCount: stats.msgCount,
|
|
1215
1244
|
toolCallCount: stats.toolCallCount,
|
|
@@ -1220,10 +1249,28 @@ export function apply(ctx) {
|
|
|
1220
1249
|
status: agent === undefined ? 'none' : agentStatus,
|
|
1221
1250
|
eventCount: agent === undefined ? undefined : agent.session.seq,
|
|
1222
1251
|
parentSession: header?.parentSession === undefined ? undefined : String(header.parentSession),
|
|
1223
|
-
|
|
1224
|
-
|
|
1252
|
+
title: titleOf,
|
|
1253
|
+
canRename: liveSession !== undefined && sessionTitleSeam !== undefined,
|
|
1254
|
+
}, refocusEditor, liveSession === undefined || sessionTitleSeam === undefined
|
|
1255
|
+
? undefined
|
|
1256
|
+
: (title) => {
|
|
1257
|
+
try {
|
|
1258
|
+
const snapshot = sessionTitleSeam.rename(liveSession, title);
|
|
1259
|
+
return { ok: true, note: t('tui.session.renamed', { title: snapshot.title }), title: snapshot.title };
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
// The one input-blaming failure gets a panel-friendly phrase;
|
|
1263
|
+
// anything else (disposed service, non-live session) rides its
|
|
1264
|
+
// own message into the panel's note line.
|
|
1265
|
+
if (error instanceof Error && error.name === 'SessionTitleInvalidError') {
|
|
1266
|
+
return { ok: false, note: t('tui.session.titleEmpty') };
|
|
1267
|
+
}
|
|
1268
|
+
throw error;
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
return { kind: 'success', text: agent === undefined ? t('tui.session.none') : t('tui.session.infoShown') };
|
|
1225
1272
|
};
|
|
1226
|
-
registerLocalCommand('session', '
|
|
1273
|
+
registerLocalCommand('session', t('tui.cmd.session.description'), sessionHandler);
|
|
1227
1274
|
// /resume: pick a persisted session, validate its log, swap the live
|
|
1228
1275
|
// agent for it, and rebuild transcript + stats from the stored events.
|
|
1229
1276
|
const resumeHandler = async () => {
|
|
@@ -1241,7 +1288,7 @@ export function apply(ctx) {
|
|
|
1241
1288
|
return { kind: 'error', text: message };
|
|
1242
1289
|
}
|
|
1243
1290
|
if (picked.kind === 'empty') {
|
|
1244
|
-
return { kind: 'error', text: '
|
|
1291
|
+
return { kind: 'error', text: t('tui.resume.noneOther') };
|
|
1245
1292
|
}
|
|
1246
1293
|
if (picked.kind === 'empty-filtered') {
|
|
1247
1294
|
// Sessions exist but the display window hid them all — name the
|
|
@@ -1252,11 +1299,11 @@ export function apply(ctx) {
|
|
|
1252
1299
|
: `${picked.minBytes}B`;
|
|
1253
1300
|
return {
|
|
1254
1301
|
kind: 'error',
|
|
1255
|
-
text:
|
|
1302
|
+
text: t('tui.resume.windowEmpty', { days: picked.maxAgeDays, floor }),
|
|
1256
1303
|
};
|
|
1257
1304
|
}
|
|
1258
1305
|
if (picked.kind === 'cancelled')
|
|
1259
|
-
return { kind: 'success', text: '
|
|
1306
|
+
return { kind: 'success', text: t('tui.resume.cancelled') };
|
|
1260
1307
|
// Narrowed shared handle for the closures below (const narrowing
|
|
1261
1308
|
// propagates into async closures; `picked`'s does not).
|
|
1262
1309
|
const target = picked;
|
|
@@ -1288,19 +1335,19 @@ export function apply(ctx) {
|
|
|
1288
1335
|
ui.requestRender();
|
|
1289
1336
|
return {
|
|
1290
1337
|
kind: 'success',
|
|
1291
|
-
text:
|
|
1338
|
+
text: t('tui.resume.watching', { id: clipToWidth(String(target.id), 8) }),
|
|
1292
1339
|
};
|
|
1293
1340
|
}
|
|
1294
1341
|
catch (watchError) {
|
|
1295
1342
|
return {
|
|
1296
1343
|
kind: 'error',
|
|
1297
|
-
text:
|
|
1344
|
+
text: t('tui.resume.watchFailed', { id: clipToWidth(String(target.id), 8), error: messageOf(watchError) }),
|
|
1298
1345
|
};
|
|
1299
1346
|
}
|
|
1300
1347
|
}
|
|
1301
1348
|
return {
|
|
1302
1349
|
kind: 'error',
|
|
1303
|
-
text:
|
|
1350
|
+
text: t('tui.resume.failed', { error: message }),
|
|
1304
1351
|
};
|
|
1305
1352
|
}
|
|
1306
1353
|
// Seed the badge cache for the resumed session (its pin event may have
|
|
@@ -1328,7 +1375,7 @@ export function apply(ctx) {
|
|
|
1328
1375
|
ui.requestRender();
|
|
1329
1376
|
return {
|
|
1330
1377
|
kind: 'success',
|
|
1331
|
-
text:
|
|
1378
|
+
text: t('tui.resume.done', { id: clipToWidth(String(target.id), 8), count: session.seq }),
|
|
1332
1379
|
};
|
|
1333
1380
|
};
|
|
1334
1381
|
// Corrupt-log branch: the repair rewrites user data on disk, so it is
|
|
@@ -1342,13 +1389,13 @@ export function apply(ctx) {
|
|
|
1342
1389
|
if (logPath === undefined) {
|
|
1343
1390
|
return {
|
|
1344
1391
|
kind: 'error',
|
|
1345
|
-
text:
|
|
1392
|
+
text: t('tui.repair.logMissing', { id: clipToWidth(String(target.id), 8) }),
|
|
1346
1393
|
};
|
|
1347
1394
|
}
|
|
1348
1395
|
if (await openRepairConfirmDialog(ui.tui, ui.theme, refocusEditor) !== 'repair') {
|
|
1349
1396
|
return {
|
|
1350
1397
|
kind: 'error',
|
|
1351
|
-
text:
|
|
1398
|
+
text: t('tui.resume.cannot', { id: clipToWidth(String(target.id), 8), error: message }),
|
|
1352
1399
|
};
|
|
1353
1400
|
}
|
|
1354
1401
|
// repairSessionLog maps its own failures to results; the catch is a
|
|
@@ -1359,7 +1406,7 @@ export function apply(ctx) {
|
|
|
1359
1406
|
}
|
|
1360
1407
|
catch (error) {
|
|
1361
1408
|
const detail = messageOf(error);
|
|
1362
|
-
notice =
|
|
1409
|
+
notice = t('tui.repair.failed', { error: detail });
|
|
1363
1410
|
}
|
|
1364
1411
|
if (notice !== undefined)
|
|
1365
1412
|
return { kind: 'error', text: notice };
|
|
@@ -1377,11 +1424,11 @@ export function apply(ctx) {
|
|
|
1377
1424
|
if (isCorruptLogError(message)) {
|
|
1378
1425
|
return offerCorruptedLogRepair(message);
|
|
1379
1426
|
}
|
|
1380
|
-
return { kind: 'error', text:
|
|
1427
|
+
return { kind: 'error', text: t('tui.resume.cannot', { id: clipToWidth(String(picked.id), 8), error: message }) };
|
|
1381
1428
|
}
|
|
1382
1429
|
return resumeAndReplay();
|
|
1383
1430
|
};
|
|
1384
|
-
registerLocalCommand('resume', '
|
|
1431
|
+
registerLocalCommand('resume', t('tui.cmd.resume.description'), resumeHandler);
|
|
1385
1432
|
// /history: the read-only two-pane look-back (ADR 0003) — left pane lists
|
|
1386
1433
|
// the browsed session's completed turns, right pane shows the selected
|
|
1387
1434
|
// turn's replies; Enter/c refills the editor with the turn's prompt (a
|
|
@@ -1447,7 +1494,7 @@ export function apply(ctx) {
|
|
|
1447
1494
|
}, arg === '' ? undefined : arg);
|
|
1448
1495
|
return { kind: result.error ? 'error' : 'success', text: result.text };
|
|
1449
1496
|
};
|
|
1450
|
-
registerLocalCommand('history', '
|
|
1497
|
+
registerLocalCommand('history', t('tui.cmd.history.description'), historyHandler);
|
|
1451
1498
|
// Auto-resume after a hot-reload: a `/reload` stashes the previously
|
|
1452
1499
|
// current session id before this fiber's teardown, and the freshly
|
|
1453
1500
|
// re-imported module consumes it best-effort here so the next prompt
|
|
@@ -1496,7 +1543,7 @@ export function apply(ctx) {
|
|
|
1496
1543
|
}
|
|
1497
1544
|
catch (error) {
|
|
1498
1545
|
const message = messageOf(error);
|
|
1499
|
-
renderer.renderNotice(
|
|
1546
|
+
renderer.renderNotice(t('tui.boot.resumeFailed', { id: clipToWidth(bootResumeId, 8), error: message }), 'error');
|
|
1500
1547
|
}
|
|
1501
1548
|
})();
|
|
1502
1549
|
}
|
|
@@ -1506,11 +1553,11 @@ export function apply(ctx) {
|
|
|
1506
1553
|
// falls back to the dsh path when one does (behavior unchanged there).
|
|
1507
1554
|
commands.registerLocal('export', async () => ({
|
|
1508
1555
|
kind: 'error',
|
|
1509
|
-
text: '
|
|
1556
|
+
text: t('tui.export.noSession'),
|
|
1510
1557
|
}));
|
|
1511
1558
|
ctx.effect(() => ctx.commands.register({
|
|
1512
1559
|
name: 'export',
|
|
1513
|
-
description: '
|
|
1560
|
+
description: t('tui.cmd.export.description'),
|
|
1514
1561
|
input: { hint: '[path]' },
|
|
1515
1562
|
handler: async (invocation) => {
|
|
1516
1563
|
const events = invocation.agent.session.snapshotEvents();
|
|
@@ -1518,7 +1565,7 @@ export function apply(ctx) {
|
|
|
1518
1565
|
const target = invocation.rawInput.trim() === '' ? fallback : resolve(invocation.rawInput.trim());
|
|
1519
1566
|
await mkdir(dirname(target), { recursive: true });
|
|
1520
1567
|
await writeFile(target, events.map(event => JSON.stringify(event)).join('\n') + '\n');
|
|
1521
|
-
return { kind: 'success', text:
|
|
1568
|
+
return { kind: 'success', text: t('tui.export.done', { count: events.length, path: target }) };
|
|
1522
1569
|
},
|
|
1523
1570
|
}), 'dsh-tui-pi: /export');
|
|
1524
1571
|
// /new: detach the live agent (and clear the on-screen transcript) so
|
|
@@ -1575,16 +1622,16 @@ export function apply(ctx) {
|
|
|
1575
1622
|
};
|
|
1576
1623
|
const newHandler = async () => {
|
|
1577
1624
|
await startNewSession();
|
|
1578
|
-
return { kind: 'success', text: '
|
|
1625
|
+
return { kind: 'success', text: t('tui.new.done') };
|
|
1579
1626
|
};
|
|
1580
|
-
registerLocalCommand('new', '
|
|
1627
|
+
registerLocalCommand('new', t('tui.cmd.new.description'), newHandler);
|
|
1581
1628
|
// /settings: text-based configuration browser — the terminal counterpart
|
|
1582
1629
|
// of the web GUI's settings surface. Enumerates ctx.settings.describe()
|
|
1583
1630
|
// and walks each namespace's schema (drill-ins, cycle rows, inline editors,
|
|
1584
1631
|
// reset-to-defaults), writing through settings.mutate path ops.
|
|
1585
1632
|
const settingsHandler = async () => {
|
|
1586
1633
|
if (ctx.get('settings') === undefined) {
|
|
1587
|
-
return { kind: 'error', text: '
|
|
1634
|
+
return { kind: 'error', text: t('tui.settings.unavailable') };
|
|
1588
1635
|
}
|
|
1589
1636
|
const changes = await openSettingsBrowser({
|
|
1590
1637
|
ctx,
|
|
@@ -1598,23 +1645,25 @@ export function apply(ctx) {
|
|
|
1598
1645
|
},
|
|
1599
1646
|
});
|
|
1600
1647
|
if (changes < 0)
|
|
1601
|
-
return { kind: 'error', text: '
|
|
1648
|
+
return { kind: 'error', text: t('tui.settings.noNamespaces') };
|
|
1602
1649
|
return {
|
|
1603
1650
|
kind: 'success',
|
|
1604
1651
|
text: changes === 0
|
|
1605
|
-
? '
|
|
1606
|
-
:
|
|
1652
|
+
? t('tui.settings.noChanges')
|
|
1653
|
+
: changes === 1
|
|
1654
|
+
? t('tui.settings.appliedOne', { count: changes })
|
|
1655
|
+
: t('tui.settings.appliedMany', { count: changes }),
|
|
1607
1656
|
};
|
|
1608
1657
|
};
|
|
1609
|
-
registerLocalCommand('settings', '
|
|
1658
|
+
registerLocalCommand('settings', t('tui.cmd.settings.description'), settingsHandler);
|
|
1610
1659
|
// /skills: standalone skill browser with Installed/Available dual mode.
|
|
1611
1660
|
// Enter/Space toggles or symlinks; Tab switches views; Esc exits.
|
|
1612
1661
|
const skillsHandler = async () => {
|
|
1613
1662
|
const agent = bridge.getAgent();
|
|
1614
1663
|
openSkillsManagerPanel(ctx, ui.tui, ui.theme, refocusEditor, agent ?? undefined, () => { });
|
|
1615
|
-
return { kind: 'success', text: '
|
|
1664
|
+
return { kind: 'success', text: t('tui.skills.opened') };
|
|
1616
1665
|
};
|
|
1617
|
-
registerLocalCommand('skills', '
|
|
1666
|
+
registerLocalCommand('skills', t('tui.cmd.skills.description'), skillsHandler);
|
|
1618
1667
|
// /theme: pick a color scheme and apply it immediately — the choice is
|
|
1619
1668
|
// persisted to the dsh-tui settings namespace (`applies: 'live'`, so the
|
|
1620
1669
|
// watch hook would re-apply the same change anyway) and hot-swapped into
|
|
@@ -1623,7 +1672,7 @@ export function apply(ctx) {
|
|
|
1623
1672
|
// mirrors /settings: without the service there is nowhere to write.
|
|
1624
1673
|
const themeHandler = async () => {
|
|
1625
1674
|
if (ctx.get('settings') === undefined) {
|
|
1626
|
-
return { kind: 'error', text: '
|
|
1675
|
+
return { kind: 'error', text: t('tui.settings.unavailable') };
|
|
1627
1676
|
}
|
|
1628
1677
|
// Preselect from the live settings value (which may have changed since
|
|
1629
1678
|
// startup via the /settings browser), not the startup snapshot. The
|
|
@@ -1634,7 +1683,7 @@ export function apply(ctx) {
|
|
|
1634
1683
|
userNames: themeUserNames,
|
|
1635
1684
|
});
|
|
1636
1685
|
if (picked === undefined)
|
|
1637
|
-
return { kind: 'success', text: '
|
|
1686
|
+
return { kind: 'success', text: t('tui.theme.unchanged') };
|
|
1638
1687
|
const writeError = await writeThemePreference(ctx, picked);
|
|
1639
1688
|
if (writeError !== undefined)
|
|
1640
1689
|
return { kind: 'error', text: writeError };
|
|
@@ -1649,12 +1698,70 @@ export function apply(ctx) {
|
|
|
1649
1698
|
if (expected !== undefined && applied !== expected) {
|
|
1650
1699
|
return {
|
|
1651
1700
|
kind: 'success',
|
|
1652
|
-
text:
|
|
1701
|
+
text: t('tui.theme.pinned', { value: String(process.env.DSH_TUI_THEME) }),
|
|
1653
1702
|
};
|
|
1654
1703
|
}
|
|
1655
|
-
return { kind: 'success', text:
|
|
1704
|
+
return { kind: 'success', text: t('tui.theme.applied', { name: picked }) };
|
|
1705
|
+
};
|
|
1706
|
+
registerLocalCommand('theme', t('tui.cmd.theme.description'), themeHandler);
|
|
1707
|
+
// /language — switch the UI language. Language files live in the bundled
|
|
1708
|
+
// locales/ directory plus the user's ~/.dsh/locales (one JSON file per
|
|
1709
|
+
// language; adding one needs no code). Bare `/language` asks through the
|
|
1710
|
+
// native ask-user panel (src/language-ask.ts) — the same docked question
|
|
1711
|
+
// UI the model uses, so it answers on any registered surface (the TUI, a
|
|
1712
|
+
// phone through dsh-feishu) with first answer wins. `/language <id>`
|
|
1713
|
+
// switches directly. Either way the switch applies live (the next repaint
|
|
1714
|
+
// speaks it) and persists the choice to `dsh-tui.language`.
|
|
1715
|
+
const installedLanguages = () => listLocales().map(locale => `${locale.id} (${locale.name})`).join(', ');
|
|
1716
|
+
const applyLanguage = async (id) => {
|
|
1717
|
+
if (!setLocale(id)) {
|
|
1718
|
+
return { kind: 'error', text: t('command.language.unknown', { id, list: installedLanguages() }) };
|
|
1719
|
+
}
|
|
1720
|
+
const persistError = await writeLanguagePreference(ctx, id);
|
|
1721
|
+
const active = currentLocale();
|
|
1722
|
+
const switched = t('command.language.switched', { name: active.name, id: active.id });
|
|
1723
|
+
return {
|
|
1724
|
+
kind: 'success',
|
|
1725
|
+
text: persistError === undefined
|
|
1726
|
+
? switched
|
|
1727
|
+
: `${switched} ${t('command.language.persistFailed', { error: persistError })}`,
|
|
1728
|
+
};
|
|
1729
|
+
};
|
|
1730
|
+
const languageHandler = async (rawInput, signal) => {
|
|
1731
|
+
const arg = rawInput.trim();
|
|
1732
|
+
if (arg !== '')
|
|
1733
|
+
return applyLanguage(arg);
|
|
1734
|
+
const ask = ctx.get('userQuestions');
|
|
1735
|
+
if (ask === undefined) {
|
|
1736
|
+
return {
|
|
1737
|
+
kind: 'error',
|
|
1738
|
+
text: t('command.language.unavailable', { list: installedLanguages() }),
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
// Scope the ask to the live session when there is one (only the
|
|
1742
|
+
// surfaces driving it are asked); a fresh TUI asks agentless.
|
|
1743
|
+
let outcome;
|
|
1744
|
+
try {
|
|
1745
|
+
outcome = await askLanguageChoice(ask, { agent: bridge.getAgent(), signal });
|
|
1746
|
+
}
|
|
1747
|
+
catch (error) {
|
|
1748
|
+
return {
|
|
1749
|
+
kind: 'error',
|
|
1750
|
+
text: t('command.language.askFailed', {
|
|
1751
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1752
|
+
}),
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
if (outcome.kind === 'picked')
|
|
1756
|
+
return applyLanguage(outcome.id);
|
|
1757
|
+
if (outcome.kind === 'unknown') {
|
|
1758
|
+
return { kind: 'error', text: t('command.language.unknown', { id: outcome.text, list: installedLanguages() }) };
|
|
1759
|
+
}
|
|
1760
|
+
// Picked the active language, declined the question, or it was never
|
|
1761
|
+
// answered — nothing changed.
|
|
1762
|
+
return { kind: 'success', text: t('command.language.unchanged') };
|
|
1656
1763
|
};
|
|
1657
|
-
registerLocalCommand('
|
|
1764
|
+
registerLocalCommand('language', t('command.language.description'), languageHandler);
|
|
1658
1765
|
// /reload: hot-reload this plugin from the current source — re-imports the
|
|
1659
1766
|
// module and its dependencies (picking up src changes after `pnpm build`)
|
|
1660
1767
|
// and swaps the plugin runtime. The old fiber is disposed, so the TUI and
|
|
@@ -1664,7 +1771,7 @@ export function apply(ctx) {
|
|
|
1664
1771
|
kind: 'success',
|
|
1665
1772
|
text: await reloadPlugin(ctx, import.meta.url),
|
|
1666
1773
|
});
|
|
1667
|
-
registerLocalCommand('reload', '
|
|
1774
|
+
registerLocalCommand('reload', t('tui.cmd.reload.description'), reloadHandler);
|
|
1668
1775
|
// /hotkeys — pi's keybinding browser, in the /agents select-panel style:
|
|
1669
1776
|
// a field list of the app keys, Enter opens an editor that writes
|
|
1670
1777
|
// ~/.dsh/keybindings.json and live-applies the change (no /reload).
|
|
@@ -1674,9 +1781,9 @@ export function apply(ctx) {
|
|
|
1674
1781
|
apply: bindings => ui.setKeyBindings(bindings),
|
|
1675
1782
|
restoreFocus: refocusEditor,
|
|
1676
1783
|
});
|
|
1677
|
-
return { kind: 'success', text: summary ?? '
|
|
1784
|
+
return { kind: 'success', text: summary ?? t('tui.hotkeys.unchanged') };
|
|
1678
1785
|
};
|
|
1679
|
-
registerLocalCommand('hotkeys', '
|
|
1786
|
+
registerLocalCommand('hotkeys', t('tui.cmd.hotkeys.description'), hotkeysHandler);
|
|
1680
1787
|
// /login: register provider credentials — the terminal counterpart of
|
|
1681
1788
|
// pi-agent's /login, on the Models category's add-provider flow. Opens a
|
|
1682
1789
|
// searchable provider directory (including already-configured routes, so
|
|
@@ -1687,7 +1794,7 @@ export function apply(ctx) {
|
|
|
1687
1794
|
// filtered picker otherwise.
|
|
1688
1795
|
const loginHandler = async (rawInput) => {
|
|
1689
1796
|
if (ctx.get('settings') === undefined) {
|
|
1690
|
-
return { kind: 'error', text: '
|
|
1797
|
+
return { kind: 'error', text: t('tui.settings.unavailable') };
|
|
1691
1798
|
}
|
|
1692
1799
|
const result = await openLoginFlow({
|
|
1693
1800
|
ctx,
|
|
@@ -1700,14 +1807,14 @@ export function apply(ctx) {
|
|
|
1700
1807
|
if (result.kind === 'unknown') {
|
|
1701
1808
|
return {
|
|
1702
1809
|
kind: 'error',
|
|
1703
|
-
text:
|
|
1810
|
+
text: t('tui.login.unknownProvider', { name: result.target }),
|
|
1704
1811
|
};
|
|
1705
1812
|
}
|
|
1706
1813
|
if (result.kind === 'cancelled')
|
|
1707
|
-
return { kind: 'success', text: '
|
|
1708
|
-
return { kind: 'success', text:
|
|
1814
|
+
return { kind: 'success', text: t('tui.login.cancelled') };
|
|
1815
|
+
return { kind: 'success', text: t('tui.login.configured', { name: result.name }) };
|
|
1709
1816
|
};
|
|
1710
|
-
registerLocalCommand('login', '
|
|
1817
|
+
registerLocalCommand('login', t('tui.cmd.login.description'), loginHandler);
|
|
1711
1818
|
// /logout: unsubscribe a provider — pi-agent's /logout on the dsh side.
|
|
1712
1819
|
// Lists the providers with a stored credential; on selection removes the
|
|
1713
1820
|
// key AND the settings.yaml provider entry, so the provider's models
|
|
@@ -1716,7 +1823,7 @@ export function apply(ctx) {
|
|
|
1716
1823
|
// the installed catalog's current model list.
|
|
1717
1824
|
const logoutHandler = async () => {
|
|
1718
1825
|
if (ctx.get('settings') === undefined) {
|
|
1719
|
-
return { kind: 'error', text: '
|
|
1826
|
+
return { kind: 'error', text: t('tui.settings.unavailable') };
|
|
1720
1827
|
}
|
|
1721
1828
|
const result = await openLogoutFlow({
|
|
1722
1829
|
ctx,
|
|
@@ -1726,34 +1833,32 @@ export function apply(ctx) {
|
|
|
1726
1833
|
onError: message => renderer.renderNotice(message, 'error'),
|
|
1727
1834
|
});
|
|
1728
1835
|
if (result.kind === 'none') {
|
|
1729
|
-
return { kind: 'success', text: '
|
|
1836
|
+
return { kind: 'success', text: t('tui.logout.none') };
|
|
1730
1837
|
}
|
|
1731
1838
|
if (result.kind === 'cancelled')
|
|
1732
|
-
return { kind: 'success', text: '
|
|
1839
|
+
return { kind: 'success', text: t('tui.logout.cancelled') };
|
|
1733
1840
|
if (result.kind === 'failed') {
|
|
1734
1841
|
const cause = result.cause === undefined ? '' : `: ${result.cause}`;
|
|
1735
|
-
return { kind: 'error', text:
|
|
1842
|
+
return { kind: 'error', text: t('tui.logout.failed', { name: result.name, cause }) };
|
|
1736
1843
|
}
|
|
1737
1844
|
if (result.kind === 'removed-incomplete') {
|
|
1738
1845
|
return {
|
|
1739
1846
|
kind: 'error',
|
|
1740
|
-
text:
|
|
1741
|
-
+ ' — remove the provider in /settings to finish the logout.',
|
|
1847
|
+
text: t('tui.logout.incomplete', { name: result.name, error: result.error }),
|
|
1742
1848
|
};
|
|
1743
1849
|
}
|
|
1744
1850
|
if (result.kind === 'removed-key-only') {
|
|
1745
1851
|
return {
|
|
1746
1852
|
kind: 'success',
|
|
1747
|
-
text:
|
|
1748
|
-
+ ' remove the provider in /settings to also drop its models.',
|
|
1853
|
+
text: t('tui.logout.keyOnly', { name: result.name }),
|
|
1749
1854
|
};
|
|
1750
1855
|
}
|
|
1751
1856
|
return {
|
|
1752
1857
|
kind: 'success',
|
|
1753
|
-
text:
|
|
1858
|
+
text: t('tui.logout.done', { name: result.name }),
|
|
1754
1859
|
};
|
|
1755
1860
|
};
|
|
1756
|
-
registerLocalCommand('logout', '
|
|
1861
|
+
registerLocalCommand('logout', t('tui.cmd.logout.description'), logoutHandler);
|
|
1757
1862
|
// ------------------------------------------------- powerline footer + git --
|
|
1758
1863
|
const git = new GitBranchWatcher(process.cwd());
|
|
1759
1864
|
git.onChange = () => ui.requestRender();
|
|
@@ -1892,6 +1997,9 @@ export function apply(ctx) {
|
|
|
1892
1997
|
* (model/effort pickers, settings browser, session panel, resume list),
|
|
1893
1998
|
* and plugin flows hold the editor just as long: /wiki onboard parks the
|
|
1894
1999
|
* user in ask-user panels, /vault backup|restore push over the network.
|
|
2000
|
+
* /language rides the same ask-user panel (src/language-ask.ts) — its
|
|
2001
|
+
* auto-answer window is minutes, and with the timeouts disabled it waits
|
|
2002
|
+
* indefinitely, so the generic guard must not cut it off mid-question.
|
|
1895
2003
|
* The generic guard would fire mid-flow and echo a spurious
|
|
1896
2004
|
* "aborted due to timeout" — those run with a never-aborting signal
|
|
1897
2005
|
* instead.
|
|
@@ -1901,7 +2009,7 @@ export function apply(ctx) {
|
|
|
1901
2009
|
* through this dispatcher, so their names belong here all the same
|
|
1902
2010
|
* (dropping them in 2.16.0 re-introduced the timeout).
|
|
1903
2011
|
*/
|
|
1904
|
-
const MODAL_COMMANDS = new Set(['settings', 'model', 'think', 'session', 'resume', 'history', 'theme', 'permission', 'agents', 'subagents', 'login', 'logout', 'skills', 'preset', 'profile-switch', 'profile-cfg', 'wiki', 'vault']);
|
|
2012
|
+
const MODAL_COMMANDS = new Set(['settings', 'model', 'think', 'session', 'resume', 'history', 'theme', 'language', 'permission', 'agents', 'subagents', 'login', 'logout', 'skills', 'preset', 'profile-switch', 'profile-cfg', 'wiki', 'vault']);
|
|
1905
2013
|
/** Route one submitted line: dsh slash command first, model prompt second. */
|
|
1906
2014
|
const submit = async (text) => {
|
|
1907
2015
|
const line = text.trim();
|
|
@@ -1943,7 +2051,7 @@ export function apply(ctx) {
|
|
|
1943
2051
|
// Cancelled — or the derived custom state, which is display-only
|
|
1944
2052
|
// and not a switch target (mirrors the web client's popup
|
|
1945
2053
|
// filtering). Either way nothing changed.
|
|
1946
|
-
renderer.renderCommandEcho(line, undefined, '
|
|
2054
|
+
renderer.renderCommandEcho(line, undefined, t('tui.permission.unchanged'));
|
|
1947
2055
|
return;
|
|
1948
2056
|
}
|
|
1949
2057
|
executeLine = `/permission ${picked}`;
|
|
@@ -1993,7 +2101,7 @@ export function apply(ctx) {
|
|
|
1993
2101
|
try {
|
|
1994
2102
|
await bridge.prompt(line);
|
|
1995
2103
|
if (wasWatching)
|
|
1996
|
-
emitNotice('
|
|
2104
|
+
emitNotice(t('tui.steer.queuedFollowup'));
|
|
1997
2105
|
}
|
|
1998
2106
|
catch (error) {
|
|
1999
2107
|
// Buffered notice: the failure line is the only on-screen record and
|
|
@@ -2021,7 +2129,7 @@ export function apply(ctx) {
|
|
|
2021
2129
|
if (agent === undefined) {
|
|
2022
2130
|
// Running without a handle cannot deliver — say so instead of
|
|
2023
2131
|
// dropping the message silently.
|
|
2024
|
-
renderer.renderNotice('
|
|
2132
|
+
renderer.renderNotice(t('tui.steer.noSession'), 'error');
|
|
2025
2133
|
return;
|
|
2026
2134
|
}
|
|
2027
2135
|
const message = buildUserPrompt(raw);
|
|
@@ -2031,7 +2139,7 @@ export function apply(ctx) {
|
|
|
2031
2139
|
const outcome = deliverToAgent(agent, message, route);
|
|
2032
2140
|
if (outcome.outcome === 'degraded') {
|
|
2033
2141
|
renderer.rebadgePendingEcho({ id: message.id }, 'queued');
|
|
2034
|
-
renderer.renderNotice(
|
|
2142
|
+
renderer.renderNotice(turnEndedQueuedNotice(), 'info');
|
|
2035
2143
|
}
|
|
2036
2144
|
else if (outcome.outcome === 'error') {
|
|
2037
2145
|
renderer.resolvePendingEcho({ id: message.id }, 'failed');
|
|
@@ -2072,9 +2180,9 @@ export function apply(ctx) {
|
|
|
2072
2180
|
if (exitSessionId !== undefined) {
|
|
2073
2181
|
// DIM SGR (not a theme color): the hint prints after the TUI's
|
|
2074
2182
|
// theme machinery is gone; dim is universally supported.
|
|
2075
|
-
process.stdout.write(`\n\x1b[
|
|
2183
|
+
process.stdout.write(`\n\x1b[2m${t('tui.exit.resumeHint', { command: formatResumeCommand(resolveProfileName(ctx), String(exitSessionId)) })}\x1b[0m\n`);
|
|
2076
2184
|
}
|
|
2077
|
-
|
|
2185
|
+
hardExit(code);
|
|
2078
2186
|
})();
|
|
2079
2187
|
return exitTask;
|
|
2080
2188
|
};
|