@aiwayds/dsh-tui-pi 2.18.2 → 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 -43
- package/README.zh-CN.md +11 -42
- package/lib/agents.js +67 -52
- package/lib/agents.js.map +1 -1
- package/lib/ask-user.d.ts +122 -3
- package/lib/ask-user.js +335 -41
- 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 +219 -109
- 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 -8
- package/lib/subagent-viewer.js +38 -62
- 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 +44 -2
- package/lib/theme-settings.js +97 -54
- 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 +14 -4
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, 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";
|
|
@@ -67,7 +69,8 @@ import { startTui } from "./tui.js";
|
|
|
67
69
|
import { currentPreset, fetchPresetRoster, findPresetByName, formatPresetLabel, initialPresetIndex, peekNextPreset } from "./preset.js";
|
|
68
70
|
import { completedTurnSeed, openPresetConfirmDialog, performPresetSwitch } from "./preset-dialog.js";
|
|
69
71
|
import { openStopConfirmDialog } from "./stop-dialog.js";
|
|
70
|
-
import { registerAskUserProvider } from "./ask-user.js";
|
|
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
|
};
|
|
@@ -802,6 +812,8 @@ export function apply(ctx) {
|
|
|
802
812
|
// Ask-surface claim routing: the panel answers questions asked by the
|
|
803
813
|
// session this bridge drives (dsh-ask-router fans out, first answer wins).
|
|
804
814
|
getSessionId: () => bridge.getSessionId(),
|
|
815
|
+
// Auto-answer timeouts (settings > env > defaults), re-read per ask.
|
|
816
|
+
resolveTimeouts: async () => resolveAskUserTimeouts(await readAskUserExplicit(ctx)),
|
|
805
817
|
restoreFocus: refocusEditor,
|
|
806
818
|
mount: component => {
|
|
807
819
|
ui.askUser.addChild(component);
|
|
@@ -893,7 +905,7 @@ export function apply(ctx) {
|
|
|
893
905
|
stream: options => {
|
|
894
906
|
const llm = ctx.get('llm');
|
|
895
907
|
if (llm === undefined)
|
|
896
|
-
throw new Error('
|
|
908
|
+
throw new Error(t('tui.btw.noLlm'));
|
|
897
909
|
return llm.stream({
|
|
898
910
|
provider: options.provider,
|
|
899
911
|
model: options.model,
|
|
@@ -927,10 +939,10 @@ export function apply(ctx) {
|
|
|
927
939
|
if (parsed.kind === 'empty') {
|
|
928
940
|
const opened = btwController.openReview();
|
|
929
941
|
return opened === 'live'
|
|
930
|
-
? { kind: 'success', text: 'btw
|
|
942
|
+
? { kind: 'success', text: t('tui.btw.backOnScreen') }
|
|
931
943
|
: opened === 'review'
|
|
932
|
-
? { kind: 'success', text: '
|
|
933
|
-
: { kind: 'success', text:
|
|
944
|
+
? { kind: 'success', text: t('tui.btw.lastShown') }
|
|
945
|
+
: { kind: 'success', text: btwUsage() };
|
|
934
946
|
}
|
|
935
947
|
if (parsed.kind === 'error')
|
|
936
948
|
return { kind: 'error', text: parsed.error };
|
|
@@ -938,7 +950,7 @@ export function apply(ctx) {
|
|
|
938
950
|
// this must be the live view, not a read-only remote one) — when idle,
|
|
939
951
|
// a normal prompt is strictly better (tools, history, full context).
|
|
940
952
|
if (!(bridge.isRunning() && !bridge.isReadOnlyView())) {
|
|
941
|
-
return { kind: 'error', text:
|
|
953
|
+
return { kind: 'error', text: btwIdleNotice() };
|
|
942
954
|
}
|
|
943
955
|
const result = btwController.submit({
|
|
944
956
|
question: parsed.question,
|
|
@@ -946,14 +958,14 @@ export function apply(ctx) {
|
|
|
946
958
|
});
|
|
947
959
|
switch (result.kind) {
|
|
948
960
|
case 'started':
|
|
949
|
-
return { kind: 'success', text: 'btw
|
|
961
|
+
return { kind: 'success', text: t('tui.btw.started') };
|
|
950
962
|
case 'queued':
|
|
951
|
-
return { kind: 'success', text:
|
|
963
|
+
return { kind: 'success', text: t('tui.btw.queued', { position: result.position }) };
|
|
952
964
|
case 'rejected':
|
|
953
|
-
return { kind: 'error', text:
|
|
965
|
+
return { kind: 'error', text: t('tui.btw.rejected', { reason: result.reason }) };
|
|
954
966
|
}
|
|
955
967
|
};
|
|
956
|
-
registerLocalCommand('btw', '
|
|
968
|
+
registerLocalCommand('btw', t('tui.cmd.btw.description'), btwHandler);
|
|
957
969
|
// ------------------------------------------- TUI-owned slash commands --
|
|
958
970
|
// Web-surface parity: `model` is a browser client contribution there and
|
|
959
971
|
// `export` a web-only download plugin — the terminal gets native
|
|
@@ -964,7 +976,7 @@ export function apply(ctx) {
|
|
|
964
976
|
const modelHandler = async () => {
|
|
965
977
|
const picked = await pickModel(ctx, ui.tui, ui.theme, bridge.getSelection(), refocusEditor);
|
|
966
978
|
if (picked === undefined)
|
|
967
|
-
return { kind: 'success', text: '
|
|
979
|
+
return { kind: 'success', text: t('tui.model.unchanged') };
|
|
968
980
|
const llm = ctx.get('llm');
|
|
969
981
|
if (llm !== undefined) {
|
|
970
982
|
await llm.resolveCallConfig({ provider: picked.provider, model: picked.model });
|
|
@@ -973,14 +985,14 @@ export function apply(ctx) {
|
|
|
973
985
|
const persistError = await persistDefaultModel(ctx, picked);
|
|
974
986
|
ui.requestRender();
|
|
975
987
|
const modelText = picked.reasoningEffort === undefined
|
|
976
|
-
?
|
|
977
|
-
:
|
|
988
|
+
? t('tui.model.selected', { id: `${picked.provider}/${picked.model}` })
|
|
989
|
+
: t('tui.model.selectedThink', { id: `${picked.provider}/${picked.model}`, effort: String(picked.reasoningEffort) });
|
|
978
990
|
return {
|
|
979
991
|
kind: 'success',
|
|
980
|
-
text: persistError === undefined ? modelText :
|
|
992
|
+
text: persistError === undefined ? modelText : t('tui.persist.failed', { text: modelText, error: persistError }),
|
|
981
993
|
};
|
|
982
994
|
};
|
|
983
|
-
registerLocalCommand('model', '
|
|
995
|
+
registerLocalCommand('model', t('tui.cmd.model.description'), modelHandler);
|
|
984
996
|
// /agents: manage agent definition markdown files (model, think level,
|
|
985
997
|
// spawn depth) — the terminal counterpart of pi's /fun-agent-cfg. An
|
|
986
998
|
// optional argument preselects one agent by name.
|
|
@@ -988,44 +1000,44 @@ export function apply(ctx) {
|
|
|
988
1000
|
const trimmed = rawInput?.trim() ?? '';
|
|
989
1001
|
const result = await openAgentManager(ctx, ui.tui, ui.theme, refocusEditor, trimmed === '' ? undefined : trimmed, () => subagentPolicy.getStats());
|
|
990
1002
|
if (result === undefined)
|
|
991
|
-
return { kind: 'success', text: '
|
|
1003
|
+
return { kind: 'success', text: t('tui.agents.unchanged') };
|
|
992
1004
|
return { kind: 'success', text: result };
|
|
993
1005
|
};
|
|
994
|
-
registerLocalCommand('agents', '
|
|
1006
|
+
registerLocalCommand('agents', t('tui.cmd.agents.description'), agentsHandler);
|
|
995
1007
|
// /subagents: the command twin of Ctrl+G — pick a running (or recently
|
|
996
1008
|
// settled) subagent and inspect its live transcript in the 80% viewer.
|
|
997
1009
|
// Same flow as the key path; empty board closes immediately.
|
|
998
1010
|
const subagentsHandler = async () => {
|
|
999
1011
|
await openSubagentViewer(ctx, ui.tui, ui.theme, bridge, refocusEditor);
|
|
1000
|
-
return { kind: 'success', text: '
|
|
1012
|
+
return { kind: 'success', text: t('tui.subagents.viewerClosed') };
|
|
1001
1013
|
};
|
|
1002
|
-
registerLocalCommand('subagents', '
|
|
1014
|
+
registerLocalCommand('subagents', t('tui.cmd.subagents.description'), subagentsHandler);
|
|
1003
1015
|
// /think: cycle the current model's reasoning effort without re-picking
|
|
1004
1016
|
// the model. A no-session /think still lands in the selection ref and
|
|
1005
1017
|
// survives the lazy session creation (bridge seeds only an empty ref).
|
|
1006
1018
|
const thinkHandler = async () => {
|
|
1007
1019
|
const current = bridge.getSelection();
|
|
1008
1020
|
if (current === undefined) {
|
|
1009
|
-
return { kind: 'error', text: '
|
|
1021
|
+
return { kind: 'error', text: t('tui.think.noModel') };
|
|
1010
1022
|
}
|
|
1011
1023
|
const result = await pickEffort(ctx, ui.tui, ui.theme, current, refocusEditor);
|
|
1012
1024
|
if (result.kind === 'unsupported') {
|
|
1013
1025
|
return {
|
|
1014
1026
|
kind: 'error',
|
|
1015
|
-
text: `${current.provider}/${current.model}
|
|
1027
|
+
text: t('tui.think.unsupported', { id: `${current.provider}/${current.model}` }),
|
|
1016
1028
|
};
|
|
1017
1029
|
}
|
|
1018
1030
|
if (result.kind === 'cancelled')
|
|
1019
|
-
return { kind: 'success', text: '
|
|
1031
|
+
return { kind: 'success', text: t('tui.think.unchanged') };
|
|
1020
1032
|
if (result.effort === 'default') {
|
|
1021
1033
|
const next = { provider: current.provider, model: current.model };
|
|
1022
1034
|
bridge.setSelection(next);
|
|
1023
1035
|
const persistError = await persistDefaultModel(ctx, next);
|
|
1024
1036
|
ui.requestRender();
|
|
1025
|
-
const thinkText =
|
|
1037
|
+
const thinkText = t('tui.think.providerDefault', { id: `${current.provider}/${current.model}` });
|
|
1026
1038
|
return {
|
|
1027
1039
|
kind: 'success',
|
|
1028
|
-
text: persistError === undefined ? thinkText :
|
|
1040
|
+
text: persistError === undefined ? thinkText : t('tui.persist.failed', { text: thinkText, error: persistError }),
|
|
1029
1041
|
};
|
|
1030
1042
|
}
|
|
1031
1043
|
const next = {
|
|
@@ -1036,13 +1048,13 @@ export function apply(ctx) {
|
|
|
1036
1048
|
bridge.setSelection(next);
|
|
1037
1049
|
const persistError = await persistDefaultModel(ctx, next);
|
|
1038
1050
|
ui.requestRender();
|
|
1039
|
-
const thinkText =
|
|
1051
|
+
const thinkText = t('tui.think.set', { effort: String(result.effort), id: `${current.provider}/${current.model}` });
|
|
1040
1052
|
return {
|
|
1041
1053
|
kind: 'success',
|
|
1042
|
-
text: persistError === undefined ? thinkText :
|
|
1054
|
+
text: persistError === undefined ? thinkText : t('tui.persist.failed', { text: thinkText, error: persistError }),
|
|
1043
1055
|
};
|
|
1044
1056
|
};
|
|
1045
|
-
registerLocalCommand('think', '
|
|
1057
|
+
registerLocalCommand('think', t('tui.cmd.think.description'), thinkHandler);
|
|
1046
1058
|
/**
|
|
1047
1059
|
* Record one committed preset as THIS workspace's remembered selection
|
|
1048
1060
|
* (`$DSH_HOME/workspace-presets.json`, keyed by the session backend's
|
|
@@ -1060,7 +1072,7 @@ export function apply(ctx) {
|
|
|
1060
1072
|
const doc = withRememberedPreset(loadWorkspacePresets(path), projectKeyFor(process.cwd()), presetId);
|
|
1061
1073
|
const error = saveWorkspacePresets(path, doc);
|
|
1062
1074
|
if (error !== undefined)
|
|
1063
|
-
renderer.renderNotice(
|
|
1075
|
+
renderer.renderNotice(t('tui.preset.memorySaveFailed', { error }), 'error');
|
|
1064
1076
|
}
|
|
1065
1077
|
catch {
|
|
1066
1078
|
// A broken store must never fail the switch itself.
|
|
@@ -1075,7 +1087,7 @@ export function apply(ctx) {
|
|
|
1075
1087
|
// directly (the first submit already creates the session on it).
|
|
1076
1088
|
const presetHandler = async (rawInput) => {
|
|
1077
1089
|
if (presetState.roster.length === 0) {
|
|
1078
|
-
return { kind: 'error', text: '
|
|
1090
|
+
return { kind: 'error', text: t('tui.preset.none') };
|
|
1079
1091
|
}
|
|
1080
1092
|
const arg = rawInput?.trim() ?? '';
|
|
1081
1093
|
let target;
|
|
@@ -1093,11 +1105,11 @@ export function apply(ctx) {
|
|
|
1093
1105
|
else {
|
|
1094
1106
|
target = findPresetByName(presetState, arg);
|
|
1095
1107
|
if (target === undefined) {
|
|
1096
|
-
return { kind: 'error', text:
|
|
1108
|
+
return { kind: 'error', text: t('tui.preset.unknown', { name: arg }) };
|
|
1097
1109
|
}
|
|
1098
1110
|
}
|
|
1099
1111
|
if (target === undefined)
|
|
1100
|
-
return { kind: 'success', text: '
|
|
1112
|
+
return { kind: 'success', text: t('tui.preset.unchanged') };
|
|
1101
1113
|
const outcome = await performPresetSwitch(presetState, target, { hasLiveSession: () => bridge.getAgent() !== undefined,
|
|
1102
1114
|
confirmSwitch: (name, restart) => openPresetConfirmDialog(ui.tui, ui.theme, name, restart, refocusEditor)
|
|
1103
1115
|
.then(outcome => (outcome === 'cancelled' ? 'cancel' : outcome)),
|
|
@@ -1179,7 +1191,7 @@ export function apply(ctx) {
|
|
|
1179
1191
|
persistRememberedPreset(target.id);
|
|
1180
1192
|
return { kind: 'success', text: outcome.message };
|
|
1181
1193
|
};
|
|
1182
|
-
registerLocalCommand('preset', '
|
|
1194
|
+
registerLocalCommand('preset', t('tui.cmd.preset.description'), presetHandler);
|
|
1183
1195
|
// Model profiles moved to the dsh-profile-switch plugin (ask-user based,
|
|
1184
1196
|
// surface-agnostic). What stays HERE is the live-selection bridge it
|
|
1185
1197
|
// consumes: the TUI's /model + /think ride the same selection ref, and
|
|
@@ -1196,18 +1208,37 @@ export function apply(ctx) {
|
|
|
1196
1208
|
},
|
|
1197
1209
|
current: () => bridge.getSelection(),
|
|
1198
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
|
+
})();
|
|
1199
1228
|
const sessionHandler = async () => {
|
|
1200
1229
|
const agent = bridge.getAgent();
|
|
1201
1230
|
const stats = bridge.getStats();
|
|
1202
1231
|
const selection = bridge.getSelection();
|
|
1203
1232
|
const header = agent?.session.header;
|
|
1233
|
+
const liveSession = agent?.session;
|
|
1234
|
+
const titleOf = liveSession === undefined ? undefined : sessionTitleSeam?.get(liveSession)?.title;
|
|
1204
1235
|
await showSessionInfo(ui.tui, ui.theme, {
|
|
1205
1236
|
id: agent === undefined ? undefined : String(agent.session.id),
|
|
1206
1237
|
cwd: header?.cwd,
|
|
1207
1238
|
createdAt: header?.createdAt,
|
|
1208
1239
|
model: selection === undefined ? undefined : `${selection.provider}/${selection.model}`,
|
|
1209
1240
|
effort: selection === undefined || selection.reasoningEffort === undefined
|
|
1210
|
-
? (selection === undefined ? undefined : '
|
|
1241
|
+
? (selection === undefined ? undefined : t('tui.session.effortDefault'))
|
|
1211
1242
|
: String(selection.reasoningEffort),
|
|
1212
1243
|
msgCount: stats.msgCount,
|
|
1213
1244
|
toolCallCount: stats.toolCallCount,
|
|
@@ -1218,10 +1249,28 @@ export function apply(ctx) {
|
|
|
1218
1249
|
status: agent === undefined ? 'none' : agentStatus,
|
|
1219
1250
|
eventCount: agent === undefined ? undefined : agent.session.seq,
|
|
1220
1251
|
parentSession: header?.parentSession === undefined ? undefined : String(header.parentSession),
|
|
1221
|
-
|
|
1222
|
-
|
|
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') };
|
|
1223
1272
|
};
|
|
1224
|
-
registerLocalCommand('session', '
|
|
1273
|
+
registerLocalCommand('session', t('tui.cmd.session.description'), sessionHandler);
|
|
1225
1274
|
// /resume: pick a persisted session, validate its log, swap the live
|
|
1226
1275
|
// agent for it, and rebuild transcript + stats from the stored events.
|
|
1227
1276
|
const resumeHandler = async () => {
|
|
@@ -1239,7 +1288,7 @@ export function apply(ctx) {
|
|
|
1239
1288
|
return { kind: 'error', text: message };
|
|
1240
1289
|
}
|
|
1241
1290
|
if (picked.kind === 'empty') {
|
|
1242
|
-
return { kind: 'error', text: '
|
|
1291
|
+
return { kind: 'error', text: t('tui.resume.noneOther') };
|
|
1243
1292
|
}
|
|
1244
1293
|
if (picked.kind === 'empty-filtered') {
|
|
1245
1294
|
// Sessions exist but the display window hid them all — name the
|
|
@@ -1250,11 +1299,11 @@ export function apply(ctx) {
|
|
|
1250
1299
|
: `${picked.minBytes}B`;
|
|
1251
1300
|
return {
|
|
1252
1301
|
kind: 'error',
|
|
1253
|
-
text:
|
|
1302
|
+
text: t('tui.resume.windowEmpty', { days: picked.maxAgeDays, floor }),
|
|
1254
1303
|
};
|
|
1255
1304
|
}
|
|
1256
1305
|
if (picked.kind === 'cancelled')
|
|
1257
|
-
return { kind: 'success', text: '
|
|
1306
|
+
return { kind: 'success', text: t('tui.resume.cancelled') };
|
|
1258
1307
|
// Narrowed shared handle for the closures below (const narrowing
|
|
1259
1308
|
// propagates into async closures; `picked`'s does not).
|
|
1260
1309
|
const target = picked;
|
|
@@ -1286,19 +1335,19 @@ export function apply(ctx) {
|
|
|
1286
1335
|
ui.requestRender();
|
|
1287
1336
|
return {
|
|
1288
1337
|
kind: 'success',
|
|
1289
|
-
text:
|
|
1338
|
+
text: t('tui.resume.watching', { id: clipToWidth(String(target.id), 8) }),
|
|
1290
1339
|
};
|
|
1291
1340
|
}
|
|
1292
1341
|
catch (watchError) {
|
|
1293
1342
|
return {
|
|
1294
1343
|
kind: 'error',
|
|
1295
|
-
text:
|
|
1344
|
+
text: t('tui.resume.watchFailed', { id: clipToWidth(String(target.id), 8), error: messageOf(watchError) }),
|
|
1296
1345
|
};
|
|
1297
1346
|
}
|
|
1298
1347
|
}
|
|
1299
1348
|
return {
|
|
1300
1349
|
kind: 'error',
|
|
1301
|
-
text:
|
|
1350
|
+
text: t('tui.resume.failed', { error: message }),
|
|
1302
1351
|
};
|
|
1303
1352
|
}
|
|
1304
1353
|
// Seed the badge cache for the resumed session (its pin event may have
|
|
@@ -1326,7 +1375,7 @@ export function apply(ctx) {
|
|
|
1326
1375
|
ui.requestRender();
|
|
1327
1376
|
return {
|
|
1328
1377
|
kind: 'success',
|
|
1329
|
-
text:
|
|
1378
|
+
text: t('tui.resume.done', { id: clipToWidth(String(target.id), 8), count: session.seq }),
|
|
1330
1379
|
};
|
|
1331
1380
|
};
|
|
1332
1381
|
// Corrupt-log branch: the repair rewrites user data on disk, so it is
|
|
@@ -1340,13 +1389,13 @@ export function apply(ctx) {
|
|
|
1340
1389
|
if (logPath === undefined) {
|
|
1341
1390
|
return {
|
|
1342
1391
|
kind: 'error',
|
|
1343
|
-
text:
|
|
1392
|
+
text: t('tui.repair.logMissing', { id: clipToWidth(String(target.id), 8) }),
|
|
1344
1393
|
};
|
|
1345
1394
|
}
|
|
1346
1395
|
if (await openRepairConfirmDialog(ui.tui, ui.theme, refocusEditor) !== 'repair') {
|
|
1347
1396
|
return {
|
|
1348
1397
|
kind: 'error',
|
|
1349
|
-
text:
|
|
1398
|
+
text: t('tui.resume.cannot', { id: clipToWidth(String(target.id), 8), error: message }),
|
|
1350
1399
|
};
|
|
1351
1400
|
}
|
|
1352
1401
|
// repairSessionLog maps its own failures to results; the catch is a
|
|
@@ -1357,7 +1406,7 @@ export function apply(ctx) {
|
|
|
1357
1406
|
}
|
|
1358
1407
|
catch (error) {
|
|
1359
1408
|
const detail = messageOf(error);
|
|
1360
|
-
notice =
|
|
1409
|
+
notice = t('tui.repair.failed', { error: detail });
|
|
1361
1410
|
}
|
|
1362
1411
|
if (notice !== undefined)
|
|
1363
1412
|
return { kind: 'error', text: notice };
|
|
@@ -1375,11 +1424,11 @@ export function apply(ctx) {
|
|
|
1375
1424
|
if (isCorruptLogError(message)) {
|
|
1376
1425
|
return offerCorruptedLogRepair(message);
|
|
1377
1426
|
}
|
|
1378
|
-
return { kind: 'error', text:
|
|
1427
|
+
return { kind: 'error', text: t('tui.resume.cannot', { id: clipToWidth(String(picked.id), 8), error: message }) };
|
|
1379
1428
|
}
|
|
1380
1429
|
return resumeAndReplay();
|
|
1381
1430
|
};
|
|
1382
|
-
registerLocalCommand('resume', '
|
|
1431
|
+
registerLocalCommand('resume', t('tui.cmd.resume.description'), resumeHandler);
|
|
1383
1432
|
// /history: the read-only two-pane look-back (ADR 0003) — left pane lists
|
|
1384
1433
|
// the browsed session's completed turns, right pane shows the selected
|
|
1385
1434
|
// turn's replies; Enter/c refills the editor with the turn's prompt (a
|
|
@@ -1445,7 +1494,7 @@ export function apply(ctx) {
|
|
|
1445
1494
|
}, arg === '' ? undefined : arg);
|
|
1446
1495
|
return { kind: result.error ? 'error' : 'success', text: result.text };
|
|
1447
1496
|
};
|
|
1448
|
-
registerLocalCommand('history', '
|
|
1497
|
+
registerLocalCommand('history', t('tui.cmd.history.description'), historyHandler);
|
|
1449
1498
|
// Auto-resume after a hot-reload: a `/reload` stashes the previously
|
|
1450
1499
|
// current session id before this fiber's teardown, and the freshly
|
|
1451
1500
|
// re-imported module consumes it best-effort here so the next prompt
|
|
@@ -1494,7 +1543,7 @@ export function apply(ctx) {
|
|
|
1494
1543
|
}
|
|
1495
1544
|
catch (error) {
|
|
1496
1545
|
const message = messageOf(error);
|
|
1497
|
-
renderer.renderNotice(
|
|
1546
|
+
renderer.renderNotice(t('tui.boot.resumeFailed', { id: clipToWidth(bootResumeId, 8), error: message }), 'error');
|
|
1498
1547
|
}
|
|
1499
1548
|
})();
|
|
1500
1549
|
}
|
|
@@ -1504,11 +1553,11 @@ export function apply(ctx) {
|
|
|
1504
1553
|
// falls back to the dsh path when one does (behavior unchanged there).
|
|
1505
1554
|
commands.registerLocal('export', async () => ({
|
|
1506
1555
|
kind: 'error',
|
|
1507
|
-
text: '
|
|
1556
|
+
text: t('tui.export.noSession'),
|
|
1508
1557
|
}));
|
|
1509
1558
|
ctx.effect(() => ctx.commands.register({
|
|
1510
1559
|
name: 'export',
|
|
1511
|
-
description: '
|
|
1560
|
+
description: t('tui.cmd.export.description'),
|
|
1512
1561
|
input: { hint: '[path]' },
|
|
1513
1562
|
handler: async (invocation) => {
|
|
1514
1563
|
const events = invocation.agent.session.snapshotEvents();
|
|
@@ -1516,7 +1565,7 @@ export function apply(ctx) {
|
|
|
1516
1565
|
const target = invocation.rawInput.trim() === '' ? fallback : resolve(invocation.rawInput.trim());
|
|
1517
1566
|
await mkdir(dirname(target), { recursive: true });
|
|
1518
1567
|
await writeFile(target, events.map(event => JSON.stringify(event)).join('\n') + '\n');
|
|
1519
|
-
return { kind: 'success', text:
|
|
1568
|
+
return { kind: 'success', text: t('tui.export.done', { count: events.length, path: target }) };
|
|
1520
1569
|
},
|
|
1521
1570
|
}), 'dsh-tui-pi: /export');
|
|
1522
1571
|
// /new: detach the live agent (and clear the on-screen transcript) so
|
|
@@ -1573,16 +1622,16 @@ export function apply(ctx) {
|
|
|
1573
1622
|
};
|
|
1574
1623
|
const newHandler = async () => {
|
|
1575
1624
|
await startNewSession();
|
|
1576
|
-
return { kind: 'success', text: '
|
|
1625
|
+
return { kind: 'success', text: t('tui.new.done') };
|
|
1577
1626
|
};
|
|
1578
|
-
registerLocalCommand('new', '
|
|
1627
|
+
registerLocalCommand('new', t('tui.cmd.new.description'), newHandler);
|
|
1579
1628
|
// /settings: text-based configuration browser — the terminal counterpart
|
|
1580
1629
|
// of the web GUI's settings surface. Enumerates ctx.settings.describe()
|
|
1581
1630
|
// and walks each namespace's schema (drill-ins, cycle rows, inline editors,
|
|
1582
1631
|
// reset-to-defaults), writing through settings.mutate path ops.
|
|
1583
1632
|
const settingsHandler = async () => {
|
|
1584
1633
|
if (ctx.get('settings') === undefined) {
|
|
1585
|
-
return { kind: 'error', text: '
|
|
1634
|
+
return { kind: 'error', text: t('tui.settings.unavailable') };
|
|
1586
1635
|
}
|
|
1587
1636
|
const changes = await openSettingsBrowser({
|
|
1588
1637
|
ctx,
|
|
@@ -1596,23 +1645,25 @@ export function apply(ctx) {
|
|
|
1596
1645
|
},
|
|
1597
1646
|
});
|
|
1598
1647
|
if (changes < 0)
|
|
1599
|
-
return { kind: 'error', text: '
|
|
1648
|
+
return { kind: 'error', text: t('tui.settings.noNamespaces') };
|
|
1600
1649
|
return {
|
|
1601
1650
|
kind: 'success',
|
|
1602
1651
|
text: changes === 0
|
|
1603
|
-
? '
|
|
1604
|
-
:
|
|
1652
|
+
? t('tui.settings.noChanges')
|
|
1653
|
+
: changes === 1
|
|
1654
|
+
? t('tui.settings.appliedOne', { count: changes })
|
|
1655
|
+
: t('tui.settings.appliedMany', { count: changes }),
|
|
1605
1656
|
};
|
|
1606
1657
|
};
|
|
1607
|
-
registerLocalCommand('settings', '
|
|
1658
|
+
registerLocalCommand('settings', t('tui.cmd.settings.description'), settingsHandler);
|
|
1608
1659
|
// /skills: standalone skill browser with Installed/Available dual mode.
|
|
1609
1660
|
// Enter/Space toggles or symlinks; Tab switches views; Esc exits.
|
|
1610
1661
|
const skillsHandler = async () => {
|
|
1611
1662
|
const agent = bridge.getAgent();
|
|
1612
1663
|
openSkillsManagerPanel(ctx, ui.tui, ui.theme, refocusEditor, agent ?? undefined, () => { });
|
|
1613
|
-
return { kind: 'success', text: '
|
|
1664
|
+
return { kind: 'success', text: t('tui.skills.opened') };
|
|
1614
1665
|
};
|
|
1615
|
-
registerLocalCommand('skills', '
|
|
1666
|
+
registerLocalCommand('skills', t('tui.cmd.skills.description'), skillsHandler);
|
|
1616
1667
|
// /theme: pick a color scheme and apply it immediately — the choice is
|
|
1617
1668
|
// persisted to the dsh-tui settings namespace (`applies: 'live'`, so the
|
|
1618
1669
|
// watch hook would re-apply the same change anyway) and hot-swapped into
|
|
@@ -1621,7 +1672,7 @@ export function apply(ctx) {
|
|
|
1621
1672
|
// mirrors /settings: without the service there is nowhere to write.
|
|
1622
1673
|
const themeHandler = async () => {
|
|
1623
1674
|
if (ctx.get('settings') === undefined) {
|
|
1624
|
-
return { kind: 'error', text: '
|
|
1675
|
+
return { kind: 'error', text: t('tui.settings.unavailable') };
|
|
1625
1676
|
}
|
|
1626
1677
|
// Preselect from the live settings value (which may have changed since
|
|
1627
1678
|
// startup via the /settings browser), not the startup snapshot. The
|
|
@@ -1632,7 +1683,7 @@ export function apply(ctx) {
|
|
|
1632
1683
|
userNames: themeUserNames,
|
|
1633
1684
|
});
|
|
1634
1685
|
if (picked === undefined)
|
|
1635
|
-
return { kind: 'success', text: '
|
|
1686
|
+
return { kind: 'success', text: t('tui.theme.unchanged') };
|
|
1636
1687
|
const writeError = await writeThemePreference(ctx, picked);
|
|
1637
1688
|
if (writeError !== undefined)
|
|
1638
1689
|
return { kind: 'error', text: writeError };
|
|
@@ -1647,12 +1698,70 @@ export function apply(ctx) {
|
|
|
1647
1698
|
if (expected !== undefined && applied !== expected) {
|
|
1648
1699
|
return {
|
|
1649
1700
|
kind: 'success',
|
|
1650
|
-
text:
|
|
1701
|
+
text: t('tui.theme.pinned', { value: String(process.env.DSH_TUI_THEME) }),
|
|
1651
1702
|
};
|
|
1652
1703
|
}
|
|
1653
|
-
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') };
|
|
1654
1763
|
};
|
|
1655
|
-
registerLocalCommand('
|
|
1764
|
+
registerLocalCommand('language', t('command.language.description'), languageHandler);
|
|
1656
1765
|
// /reload: hot-reload this plugin from the current source — re-imports the
|
|
1657
1766
|
// module and its dependencies (picking up src changes after `pnpm build`)
|
|
1658
1767
|
// and swaps the plugin runtime. The old fiber is disposed, so the TUI and
|
|
@@ -1662,7 +1771,7 @@ export function apply(ctx) {
|
|
|
1662
1771
|
kind: 'success',
|
|
1663
1772
|
text: await reloadPlugin(ctx, import.meta.url),
|
|
1664
1773
|
});
|
|
1665
|
-
registerLocalCommand('reload', '
|
|
1774
|
+
registerLocalCommand('reload', t('tui.cmd.reload.description'), reloadHandler);
|
|
1666
1775
|
// /hotkeys — pi's keybinding browser, in the /agents select-panel style:
|
|
1667
1776
|
// a field list of the app keys, Enter opens an editor that writes
|
|
1668
1777
|
// ~/.dsh/keybindings.json and live-applies the change (no /reload).
|
|
@@ -1672,9 +1781,9 @@ export function apply(ctx) {
|
|
|
1672
1781
|
apply: bindings => ui.setKeyBindings(bindings),
|
|
1673
1782
|
restoreFocus: refocusEditor,
|
|
1674
1783
|
});
|
|
1675
|
-
return { kind: 'success', text: summary ?? '
|
|
1784
|
+
return { kind: 'success', text: summary ?? t('tui.hotkeys.unchanged') };
|
|
1676
1785
|
};
|
|
1677
|
-
registerLocalCommand('hotkeys', '
|
|
1786
|
+
registerLocalCommand('hotkeys', t('tui.cmd.hotkeys.description'), hotkeysHandler);
|
|
1678
1787
|
// /login: register provider credentials — the terminal counterpart of
|
|
1679
1788
|
// pi-agent's /login, on the Models category's add-provider flow. Opens a
|
|
1680
1789
|
// searchable provider directory (including already-configured routes, so
|
|
@@ -1685,7 +1794,7 @@ export function apply(ctx) {
|
|
|
1685
1794
|
// filtered picker otherwise.
|
|
1686
1795
|
const loginHandler = async (rawInput) => {
|
|
1687
1796
|
if (ctx.get('settings') === undefined) {
|
|
1688
|
-
return { kind: 'error', text: '
|
|
1797
|
+
return { kind: 'error', text: t('tui.settings.unavailable') };
|
|
1689
1798
|
}
|
|
1690
1799
|
const result = await openLoginFlow({
|
|
1691
1800
|
ctx,
|
|
@@ -1698,14 +1807,14 @@ export function apply(ctx) {
|
|
|
1698
1807
|
if (result.kind === 'unknown') {
|
|
1699
1808
|
return {
|
|
1700
1809
|
kind: 'error',
|
|
1701
|
-
text:
|
|
1810
|
+
text: t('tui.login.unknownProvider', { name: result.target }),
|
|
1702
1811
|
};
|
|
1703
1812
|
}
|
|
1704
1813
|
if (result.kind === 'cancelled')
|
|
1705
|
-
return { kind: 'success', text: '
|
|
1706
|
-
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 }) };
|
|
1707
1816
|
};
|
|
1708
|
-
registerLocalCommand('login', '
|
|
1817
|
+
registerLocalCommand('login', t('tui.cmd.login.description'), loginHandler);
|
|
1709
1818
|
// /logout: unsubscribe a provider — pi-agent's /logout on the dsh side.
|
|
1710
1819
|
// Lists the providers with a stored credential; on selection removes the
|
|
1711
1820
|
// key AND the settings.yaml provider entry, so the provider's models
|
|
@@ -1714,7 +1823,7 @@ export function apply(ctx) {
|
|
|
1714
1823
|
// the installed catalog's current model list.
|
|
1715
1824
|
const logoutHandler = async () => {
|
|
1716
1825
|
if (ctx.get('settings') === undefined) {
|
|
1717
|
-
return { kind: 'error', text: '
|
|
1826
|
+
return { kind: 'error', text: t('tui.settings.unavailable') };
|
|
1718
1827
|
}
|
|
1719
1828
|
const result = await openLogoutFlow({
|
|
1720
1829
|
ctx,
|
|
@@ -1724,34 +1833,32 @@ export function apply(ctx) {
|
|
|
1724
1833
|
onError: message => renderer.renderNotice(message, 'error'),
|
|
1725
1834
|
});
|
|
1726
1835
|
if (result.kind === 'none') {
|
|
1727
|
-
return { kind: 'success', text: '
|
|
1836
|
+
return { kind: 'success', text: t('tui.logout.none') };
|
|
1728
1837
|
}
|
|
1729
1838
|
if (result.kind === 'cancelled')
|
|
1730
|
-
return { kind: 'success', text: '
|
|
1839
|
+
return { kind: 'success', text: t('tui.logout.cancelled') };
|
|
1731
1840
|
if (result.kind === 'failed') {
|
|
1732
1841
|
const cause = result.cause === undefined ? '' : `: ${result.cause}`;
|
|
1733
|
-
return { kind: 'error', text:
|
|
1842
|
+
return { kind: 'error', text: t('tui.logout.failed', { name: result.name, cause }) };
|
|
1734
1843
|
}
|
|
1735
1844
|
if (result.kind === 'removed-incomplete') {
|
|
1736
1845
|
return {
|
|
1737
1846
|
kind: 'error',
|
|
1738
|
-
text:
|
|
1739
|
-
+ ' — remove the provider in /settings to finish the logout.',
|
|
1847
|
+
text: t('tui.logout.incomplete', { name: result.name, error: result.error }),
|
|
1740
1848
|
};
|
|
1741
1849
|
}
|
|
1742
1850
|
if (result.kind === 'removed-key-only') {
|
|
1743
1851
|
return {
|
|
1744
1852
|
kind: 'success',
|
|
1745
|
-
text:
|
|
1746
|
-
+ ' remove the provider in /settings to also drop its models.',
|
|
1853
|
+
text: t('tui.logout.keyOnly', { name: result.name }),
|
|
1747
1854
|
};
|
|
1748
1855
|
}
|
|
1749
1856
|
return {
|
|
1750
1857
|
kind: 'success',
|
|
1751
|
-
text:
|
|
1858
|
+
text: t('tui.logout.done', { name: result.name }),
|
|
1752
1859
|
};
|
|
1753
1860
|
};
|
|
1754
|
-
registerLocalCommand('logout', '
|
|
1861
|
+
registerLocalCommand('logout', t('tui.cmd.logout.description'), logoutHandler);
|
|
1755
1862
|
// ------------------------------------------------- powerline footer + git --
|
|
1756
1863
|
const git = new GitBranchWatcher(process.cwd());
|
|
1757
1864
|
git.onChange = () => ui.requestRender();
|
|
@@ -1890,6 +1997,9 @@ export function apply(ctx) {
|
|
|
1890
1997
|
* (model/effort pickers, settings browser, session panel, resume list),
|
|
1891
1998
|
* and plugin flows hold the editor just as long: /wiki onboard parks the
|
|
1892
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.
|
|
1893
2003
|
* The generic guard would fire mid-flow and echo a spurious
|
|
1894
2004
|
* "aborted due to timeout" — those run with a never-aborting signal
|
|
1895
2005
|
* instead.
|
|
@@ -1899,7 +2009,7 @@ export function apply(ctx) {
|
|
|
1899
2009
|
* through this dispatcher, so their names belong here all the same
|
|
1900
2010
|
* (dropping them in 2.16.0 re-introduced the timeout).
|
|
1901
2011
|
*/
|
|
1902
|
-
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']);
|
|
1903
2013
|
/** Route one submitted line: dsh slash command first, model prompt second. */
|
|
1904
2014
|
const submit = async (text) => {
|
|
1905
2015
|
const line = text.trim();
|
|
@@ -1941,7 +2051,7 @@ export function apply(ctx) {
|
|
|
1941
2051
|
// Cancelled — or the derived custom state, which is display-only
|
|
1942
2052
|
// and not a switch target (mirrors the web client's popup
|
|
1943
2053
|
// filtering). Either way nothing changed.
|
|
1944
|
-
renderer.renderCommandEcho(line, undefined, '
|
|
2054
|
+
renderer.renderCommandEcho(line, undefined, t('tui.permission.unchanged'));
|
|
1945
2055
|
return;
|
|
1946
2056
|
}
|
|
1947
2057
|
executeLine = `/permission ${picked}`;
|
|
@@ -1991,7 +2101,7 @@ export function apply(ctx) {
|
|
|
1991
2101
|
try {
|
|
1992
2102
|
await bridge.prompt(line);
|
|
1993
2103
|
if (wasWatching)
|
|
1994
|
-
emitNotice('
|
|
2104
|
+
emitNotice(t('tui.steer.queuedFollowup'));
|
|
1995
2105
|
}
|
|
1996
2106
|
catch (error) {
|
|
1997
2107
|
// Buffered notice: the failure line is the only on-screen record and
|
|
@@ -2019,7 +2129,7 @@ export function apply(ctx) {
|
|
|
2019
2129
|
if (agent === undefined) {
|
|
2020
2130
|
// Running without a handle cannot deliver — say so instead of
|
|
2021
2131
|
// dropping the message silently.
|
|
2022
|
-
renderer.renderNotice('
|
|
2132
|
+
renderer.renderNotice(t('tui.steer.noSession'), 'error');
|
|
2023
2133
|
return;
|
|
2024
2134
|
}
|
|
2025
2135
|
const message = buildUserPrompt(raw);
|
|
@@ -2029,7 +2139,7 @@ export function apply(ctx) {
|
|
|
2029
2139
|
const outcome = deliverToAgent(agent, message, route);
|
|
2030
2140
|
if (outcome.outcome === 'degraded') {
|
|
2031
2141
|
renderer.rebadgePendingEcho({ id: message.id }, 'queued');
|
|
2032
|
-
renderer.renderNotice(
|
|
2142
|
+
renderer.renderNotice(turnEndedQueuedNotice(), 'info');
|
|
2033
2143
|
}
|
|
2034
2144
|
else if (outcome.outcome === 'error') {
|
|
2035
2145
|
renderer.resolvePendingEcho({ id: message.id }, 'failed');
|
|
@@ -2070,9 +2180,9 @@ export function apply(ctx) {
|
|
|
2070
2180
|
if (exitSessionId !== undefined) {
|
|
2071
2181
|
// DIM SGR (not a theme color): the hint prints after the TUI's
|
|
2072
2182
|
// theme machinery is gone; dim is universally supported.
|
|
2073
|
-
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`);
|
|
2074
2184
|
}
|
|
2075
|
-
|
|
2185
|
+
hardExit(code);
|
|
2076
2186
|
})();
|
|
2077
2187
|
return exitTask;
|
|
2078
2188
|
};
|