dsh-ssh-tui 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +16 -1
- package/README.md +14 -1
- package/cordis.patch.yml +15 -0
- package/docs/terminals.md +113 -0
- package/lib/diag.js +35 -0
- package/lib/diag.js.map +1 -1
- package/lib/display-sock.js +259 -42
- package/lib/display-sock.js.map +1 -1
- package/lib/doctor.js +71 -37
- package/lib/doctor.js.map +1 -1
- package/lib/dsh-compat.js +206 -88
- package/lib/dsh-compat.js.map +1 -1
- package/lib/footer.js +4 -2
- package/lib/footer.js.map +1 -1
- package/lib/i18n/en.js +18 -0
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/index.js +13 -7
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/zh.js +18 -0
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +36 -11
- package/lib/index.js.map +1 -1
- package/lib/paint.js +5 -2
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +5 -3
- package/lib/picker.js.map +1 -1
- package/lib/platform.js +296 -11
- package/lib/platform.js.map +1 -1
- package/lib/preset-authoring.js +10 -14
- package/lib/preset-authoring.js.map +1 -1
- package/lib/preset-compat.js +100 -0
- package/lib/preset-compat.js.map +1 -0
- package/lib/preset-picker.js +5 -1
- package/lib/preset-picker.js.map +1 -1
- package/lib/preset-rows.js +83 -13
- package/lib/preset-rows.js.map +1 -1
- package/lib/provider-catalog.js +4 -4
- package/lib/route-memory.js +3 -3
- package/lib/route-memory.js.map +1 -1
- package/lib/session-index.js +5 -0
- package/lib/session-index.js.map +1 -1
- package/lib/session-lock.js +4 -1
- package/lib/session-lock.js.map +1 -1
- package/lib/session-route.js +3 -0
- package/lib/session-route.js.map +1 -1
- package/lib/settings-routes.js +10 -0
- package/lib/settings-routes.js.map +1 -0
- package/lib/settings-subagent.js +10 -0
- package/lib/settings-subagent.js.map +1 -0
- package/lib/subagent-model.js +5 -5
- package/lib/subagent-model.js.map +1 -1
- package/lib/supergrok-token.js +4 -0
- package/lib/supergrok-token.js.map +1 -1
- package/lib/terminal-caps.js +358 -0
- package/lib/terminal-caps.js.map +1 -0
- package/lib/tui.js +184 -120
- package/lib/tui.js.map +1 -1
- package/lib/types/diag.d.ts +18 -1
- package/lib/types/display-sock.d.ts +63 -22
- package/lib/types/doctor.d.ts +8 -1
- package/lib/types/dsh-compat.d.ts +141 -44
- package/lib/types/footer.d.ts +3 -1
- package/lib/types/i18n/index.d.ts +18 -12
- package/lib/types/index.d.ts +45 -0
- package/lib/types/picker.d.ts +3 -0
- package/lib/types/platform.d.ts +175 -10
- package/lib/types/preset-authoring.d.ts +8 -14
- package/lib/types/preset-compat.d.ts +58 -0
- package/lib/types/preset-picker.d.ts +1 -1
- package/lib/types/preset-rows.d.ts +51 -9
- package/lib/types/settings-routes.d.ts +16 -0
- package/lib/types/settings-subagent.d.ts +24 -0
- package/lib/types/subagent-model.d.ts +7 -7
- package/lib/types/terminal-caps.d.ts +105 -0
- package/lib/types/tui.d.ts +41 -7
- package/package.json +82 -55
package/lib/tui.js
CHANGED
|
@@ -22,11 +22,11 @@ import { homedir } from 'node:os';
|
|
|
22
22
|
import { dirname, join } from 'node:path';
|
|
23
23
|
import { createRequire } from 'node:module';
|
|
24
24
|
import { StringDecoder } from 'node:string_decoder';
|
|
25
|
-
import { METADATA_FILE, readPresetMetadata } from '
|
|
25
|
+
import { METADATA_FILE, readPresetMetadata } from './preset-compat.js';
|
|
26
26
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
27
27
|
import { createUserMessage, errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
28
28
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
29
|
-
import { commandAcceptsAttachments, forEachSessionEventAsync,
|
|
29
|
+
import { commandAcceptsAttachments, forEachSessionEventAsync, hostSettingsGeneration, isTokenDeltaChunk, listenHostEvent, readSettingsSection, REPLAY_YIELD_EVERY, sessionEvents, settingsNamespace, streamChunkOf, streamFirstTokenTime, streamFrameAttemptId, streamFrameOwner, toolResultFailed, } from './dsh-compat.js';
|
|
30
30
|
import { classifyApprovalDetailed, commandForApprovalRequest, isApprovalStatusArg, parseAutoApprovalMode } from './auto-approval.js';
|
|
31
31
|
import { buildReviewUserMessage, parseReviewOutput, reviewSystemPrompt } from './approval-reviewer.js';
|
|
32
32
|
import { loadProviderCatalog, mergeProviderEntries } from './provider-catalog.js';
|
|
@@ -41,19 +41,20 @@ import { keymapReport, resolveKeymap } from './keymap.js';
|
|
|
41
41
|
import { presetLabel, profileFromArgv } from './preset-label.js';
|
|
42
42
|
import { flattenGroups, groupPresets, optionMatches } from './preset-picker.js';
|
|
43
43
|
import { isRefusal, planCopy, planDelete, planMetadata, presetDirectory, } from './preset-authoring.js';
|
|
44
|
-
import { ensureRosterRows, planDuplicateRepair, planRosterRepair, rosterPatchPath, writePatchWithBackup,
|
|
44
|
+
import { ensureRosterRows, planDuplicateRepair, planRosterRepair, formsRowsDeclared, rosterPatchPath, rosterRows, writePatchWithBackup, ALL_ROSTER_ROWS, } from './preset-rows.js';
|
|
45
45
|
import { SessionStatsTracker, statsRowOf } from './stats.js';
|
|
46
46
|
import { QUESTION_OPTION_KEYS, applyQuestionFilter, backspaceQuestionFilter, clearQuestionFilter, typeQuestionFilter, visibleQuestionIndexes, confirmAnswer, inspectClosesOn, moveQuestionCursor, optionsLength, questionOptionIndex, questionSubmit, selectQuestionOptionByKey, } from './dialogs.js';
|
|
47
47
|
import { commandSuggestions, localizedCommands } from './commands.js';
|
|
48
48
|
import { archiveStalePlans, boundTranscriptRows, findLivePlanRow, findMergeableToolRow, findToolRowByCallId, mergeToolCard, planShouldDefaultExpand, windowTranscript, } from './rows.js';
|
|
49
49
|
import { detachFromSshSession, DisplayHost, isTuiHostProcess, resolveDshHome, sessionSockPath } from './display-sock.js';
|
|
50
|
+
import { displayHomePath, envFileName, hostHasOwnConsole, IS_WINDOWS, restrictPathToUser, usesSigwinch, } from './platform.js';
|
|
51
|
+
import { bracketedPasteSequence, mouseDisableSequence, mouseEnableSequence, terminalCapabilities, } from './terminal-caps.js';
|
|
50
52
|
import { sameSessionRoute, sessionRouteInput } from './session-route.js';
|
|
51
53
|
import { GATEWAY_PROTOCOL_SUFFIX, GATEWAY_PROTOCOL_ENDPOINT, GATEWAY_PROTOCOL_PREFERENCE, advertisedProtocols, baseProviderIdOf, declaredProtocol, gatewayModelTable, gatewayServesModel, siblingProviderId, splitModelsByProtocol, } from './gateway-protocol.js';
|
|
52
54
|
import { applySavedLocale, getLocale, localeDisplayName, localeFromTag, setLocale, t, UI_LOCALE_NAMESPACE, } from './i18n/index.js';
|
|
53
55
|
import { defaultReasoningEffort } from './reasoning.js';
|
|
54
56
|
import { checkForPluginUpdate, installPluginLatest } from './update-check.js';
|
|
55
57
|
import { ROUTE_MEMORY_NAMESPACE, parseRouteMemory, rememberedRouteFor, upsertRememberedRoute, } from './route-memory.js';
|
|
56
|
-
import { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-default-model';
|
|
57
58
|
import { DEFAULT_SUBAGENT_MODEL, SUBAGENT_SETTINGS_NAMESPACE, defaultSubagentModelForProvider, subagentModelMatchesProvider, subagentProviderDiffers, subagentSettingsValue, } from './subagent-model.js';
|
|
58
59
|
import { resolveFreshSuperGrokToken } from './supergrok-token.js';
|
|
59
60
|
import { copyTextFromRow, copyTextFromTranscript } from './copy-text.js';
|
|
@@ -71,11 +72,11 @@ export { clipAnsiToWidth, cursorVisualPosition, displayWidth, foldInputView, hre
|
|
|
71
72
|
export { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, ignoreFurtherHangupSignals, isEscapePrefix, findCursorPositionReply, isHangupErrno, linkQualityOf, linkSignalPips, paintIntervalForRtt, paintLinkLabel, parseCursorPositionReply, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, writeBootSplash, } from './paint.js';
|
|
72
73
|
export { CONTEXT_IDLE_COMPACT_RATIO, CONTEXT_PRESSURE_DANGER_RATIO, CONTEXT_PRESSURE_WARN_RATIO, CONTEXT_RING_EMPTY, CONTEXT_RING_SEGMENTS, contextPressureAlertText, contextPressureRingColor, contextPressureUsedTokens, contextPressureView, describeProviderRoute, dropFooterQuotaPlanName, fitFooterStatsLine, fitFooterStatusLine, footerActivity, footerIdentityParts, footerStatsGroups, footerSubagentForeign, paintFooterSubagentChip, formatContextPressureChip, formatContextPressureRing, formatContextPressureStatusLine, formatDuration, formatFooterQuota, formatQuotaBar, formatQuotaUnknown, formatStatusReport, providerHasQuotaSurface, quotaWindowTag, shortModelName, shortQuotaPlanName, formatTokens, formatTokensPerSecond, parseContextPressure, promptPressureTokens, providerShortCode, providerUsesLocalOAuth, shouldIdleAutoCompact, subagentRouteLabel, } from './footer.js';
|
|
73
74
|
export { commandCodePeriodStart, commandCodeSourceFor, crossedQuotaThresholds, formatAccountBalance, formatFooterBalance, formatOpenCodeGoUsage, formatQuotaSnapshot, formatQuotaStatusLine, joinUrl, openCodeSourceFor, parseCommandCodeQuota, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, preferredQuotaWindow, quotaAlertText, quotaRefreshEverySteps, quotaRefreshEveryTurns, remainingPercentFromUsed, tightestQuotaWindow, } from './quota.js';
|
|
74
|
-
export { commandAcceptsAttachments, forEachSessionEvent,
|
|
75
|
+
export { commandAcceptsAttachments, forEachSessionEvent, isTokenDeltaChunk, listPersistenceHeaders, inspectPersistenceSession, sessionEvents, settingsNamespace, streamChunkOf, streamFirstTokenTime, streamFrameAttemptId, streamFrameOwner, } from './dsh-compat.js';
|
|
75
76
|
export { applyTurnEndToPlan, askSummary, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planDockNote, planIsLive, planTitleFromMarkdown, planTurnLeftOpen, promptInjectionSources, promptInjectionTitle, buildSubagentHeader, clipSubagentActivity, describeSubagentFailure, subagentChipSummary, subagentDisplayName, subagentInspectLines, subagentRowFromSpawnTool, foldSubagentUserLog, todoProgressLabel, todoSummary, } from './plan.js';
|
|
76
77
|
export { buildToolHeader, canMergeToolCall, compactEditPath, compactToolBursts, compactToolGroups, countDiffAddDel, countDiffLines, countOutputLines, diffMetaDiffs, diffStatToken, friendlyJsonLines, parseExitStatus, presentToolCall, READ_TOOL_NAMES, renderToolDiff, toolBodyFitsWorkspace, toolBodyLines, toolStateColor, toolStateLabel, wrappedToolBodyLineCount, } from './tool-present.js';
|
|
77
78
|
function discoverProviderModels(llm, request, signal) {
|
|
78
|
-
return llm.discoverModels(settingsNamespace('llm-pi-ai'),
|
|
79
|
+
return llm.discoverModels(settingsNamespace('llm-pi-ai'), request, signal);
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
82
|
* Ask a gateway which routes each of its models answers (`supported_endpoints`
|
|
@@ -111,11 +112,7 @@ async function fetchModelEndpoints(baseURL, apiKey, signal) {
|
|
|
111
112
|
return undefined;
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
|
-
function installUserQuestionAnswerer(ctx,
|
|
115
|
-
const provider = questions;
|
|
116
|
-
if (typeof provider.registerProvider === 'function') {
|
|
117
|
-
return provider.registerProvider({ ask });
|
|
118
|
-
}
|
|
115
|
+
function installUserQuestionAnswerer(ctx, ask) {
|
|
119
116
|
return ctx.on('user-questions/request', async (request, next) => {
|
|
120
117
|
try {
|
|
121
118
|
return await ask(request);
|
|
@@ -128,6 +125,16 @@ function installUserQuestionAnswerer(ctx, questions, ask) {
|
|
|
128
125
|
});
|
|
129
126
|
}
|
|
130
127
|
const ROUTE_MEMORY_NS = ROUTE_MEMORY_NAMESPACE;
|
|
128
|
+
/**
|
|
129
|
+
* The `agent-default-model` settings entry `/model` falls back to writing.
|
|
130
|
+
*
|
|
131
|
+
* 0.1.5 exports this id as `AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE`; 0.1.7
|
|
132
|
+
* removed the export and let the profile own entry ids, which keeps the name the
|
|
133
|
+
* legacy `settings.yaml` section is imported under. Inlined so one build runs
|
|
134
|
+
* against either host — the primary path, `agentDefaultModel.saveSelection`,
|
|
135
|
+
* is the same call on both.
|
|
136
|
+
*/
|
|
137
|
+
const AGENT_DEFAULT_MODEL_NS = settingsNamespace('agent-default-model');
|
|
131
138
|
/**
|
|
132
139
|
* The window llm-pi-ai falls back to when neither a model entry nor the
|
|
133
140
|
* installed catalog sizes a model. The wizard pre-fills it only so a route
|
|
@@ -308,7 +315,6 @@ const SUBAGENT_DEFAULT_EFFORT_LABEL = () => t('footer.effortDefault');
|
|
|
308
315
|
*/
|
|
309
316
|
const QUOTA_RETRY_MS = 15_000;
|
|
310
317
|
const RESERVED_BOTTOM_LINES = 3; // input line + stats line + status line
|
|
311
|
-
const IS_WINDOWS = process.platform === 'win32';
|
|
312
318
|
function dshHomeDir() {
|
|
313
319
|
return resolveDshHome();
|
|
314
320
|
}
|
|
@@ -331,23 +337,9 @@ function hostDshVersion() {
|
|
|
331
337
|
return 'unknown';
|
|
332
338
|
}
|
|
333
339
|
function displayDshPath(file) {
|
|
334
|
-
|
|
335
|
-
if (IS_WINDOWS) {
|
|
336
|
-
const profile = process.env.USERPROFILE;
|
|
337
|
-
if (profile !== undefined && home.toLowerCase().startsWith(profile.toLowerCase())) {
|
|
338
|
-
const rest = home.slice(profile.length);
|
|
339
|
-
return `%USERPROFILE%${rest}\\${file}`.replaceAll('/', '\\');
|
|
340
|
-
}
|
|
341
|
-
return `${home}\\${file}`.replaceAll('/', '\\');
|
|
342
|
-
}
|
|
343
|
-
const userHome = homedir();
|
|
344
|
-
if (home === userHome)
|
|
345
|
-
return `~/.dsh/${file}`;
|
|
346
|
-
if (home.startsWith(`${userHome}/`))
|
|
347
|
-
return `~/${home.slice(userHome.length + 1)}/${file}`;
|
|
348
|
-
return join(home, file);
|
|
340
|
+
return displayHomePath(dshHomeDir(), file);
|
|
349
341
|
}
|
|
350
|
-
const DSH_ENV_FILE = join(dshHomeDir(),
|
|
342
|
+
const DSH_ENV_FILE = join(dshHomeDir(), envFileName());
|
|
351
343
|
const DEEPSEEK_LOGO_VARIANTS = [
|
|
352
344
|
{
|
|
353
345
|
width: 52,
|
|
@@ -654,6 +646,14 @@ export class SshTui {
|
|
|
654
646
|
/** Last route reported to the launcher; a repaint must not report it again. */
|
|
655
647
|
lastSessionRoute;
|
|
656
648
|
useAlternateScreen;
|
|
649
|
+
/**
|
|
650
|
+
* What the terminal in front of us can do. Read from the environment at
|
|
651
|
+
* construction (see `terminal-caps.ts`): the mouse, bracketed paste and the
|
|
652
|
+
* alternate screen are only claimed where the terminal has them.
|
|
653
|
+
*/
|
|
654
|
+
terminalCaps;
|
|
655
|
+
/** The OSC 52 caveat is worth saying once, not after every copy. */
|
|
656
|
+
osc52HintShown = false;
|
|
657
657
|
agentGone = false;
|
|
658
658
|
onboarding;
|
|
659
659
|
commandSuggestions = [];
|
|
@@ -842,8 +842,8 @@ export class SshTui {
|
|
|
842
842
|
this.disconnectPolicy = config.disconnectPolicy ?? this.readDisconnectPolicy();
|
|
843
843
|
this.presetId = config.presetId ?? 'standard';
|
|
844
844
|
this.presetName = presetLabel(this.presetId, config.presetName, config.presetTrust);
|
|
845
|
-
this.
|
|
846
|
-
|
|
845
|
+
this.terminalCaps = config.terminalCaps ?? terminalCapabilities();
|
|
846
|
+
this.useAlternateScreen = !this.lineMode && this.terminalCaps.alternateScreen;
|
|
847
847
|
this.paintLink = detectSshSession() ? 'ssh' : 'local';
|
|
848
848
|
this.paintIntervalMs = resolvePaintIntervalMs(config.paintIntervalMs, process.env, {
|
|
849
849
|
ssh: this.paintLink === 'ssh',
|
|
@@ -861,7 +861,13 @@ export class SshTui {
|
|
|
861
861
|
}
|
|
862
862
|
this.refreshRosterHealth();
|
|
863
863
|
if (this.rosterMissing) {
|
|
864
|
-
|
|
864
|
+
// The missing thing differs by host line: a roster the profile can mount,
|
|
865
|
+
// or the agent-plane rows a 0.1.7 terminal profile owns. The advice has to
|
|
866
|
+
// match what `/mode fix` will actually write.
|
|
867
|
+
this.pushRow({
|
|
868
|
+
kind: 'system',
|
|
869
|
+
text: t(this.settingsGeneration === 'forms' ? 'mode.bootFormsMissing' : 'mode.bootMissing'),
|
|
870
|
+
});
|
|
865
871
|
}
|
|
866
872
|
if (config.cwdNotice !== undefined && config.cwdNotice !== '') {
|
|
867
873
|
this.pushRow({ kind: /进入|Entered/u.test(config.cwdNotice) ? 'system' : 'error', text: config.cwdNotice });
|
|
@@ -869,6 +875,14 @@ export class SshTui {
|
|
|
869
875
|
if (config.restoredRoute !== undefined) {
|
|
870
876
|
this.pushRow({ kind: 'system', text: sessionRouteNotice(config.restoredRoute) });
|
|
871
877
|
}
|
|
878
|
+
// Windows only, and only on the fallback path: with no PowerShell there is
|
|
879
|
+
// no hidden console, so the Host is a direct child and closing the window
|
|
880
|
+
// ends the session's compute. Say it at boot rather than let the user find
|
|
881
|
+
// out by closing the window mid-turn. A bootstrapped Windows Host and every
|
|
882
|
+
// POSIX Host keep running, so neither says anything.
|
|
883
|
+
if (isTuiHostProcess() && !hostHasOwnConsole()) {
|
|
884
|
+
this.pushRow({ kind: 'system', text: t('boot.directHost') });
|
|
885
|
+
}
|
|
872
886
|
for (const notice of config.launchNotices ?? []) {
|
|
873
887
|
this.pushRow({ kind: notice.kind, text: notice.text });
|
|
874
888
|
}
|
|
@@ -898,7 +912,7 @@ export class SshTui {
|
|
|
898
912
|
process.stdin.setRawMode(true);
|
|
899
913
|
process.stdin.resume();
|
|
900
914
|
process.stdout.on('resize', this.onDirectResize);
|
|
901
|
-
if (
|
|
915
|
+
if (usesSigwinch()) {
|
|
902
916
|
process.on('SIGWINCH', this.onDirectResize);
|
|
903
917
|
}
|
|
904
918
|
process.stdin.prependListener('end', this.handleHangupStream);
|
|
@@ -912,7 +926,7 @@ export class SshTui {
|
|
|
912
926
|
this.bootBackgroundTasks();
|
|
913
927
|
return;
|
|
914
928
|
}
|
|
915
|
-
this.write(
|
|
929
|
+
this.write(this.enterScreenSequence());
|
|
916
930
|
this.render();
|
|
917
931
|
this.updateTerminalTitle();
|
|
918
932
|
void this.calibratePaintInterval().finally(() => {
|
|
@@ -923,11 +937,22 @@ export class SshTui {
|
|
|
923
937
|
});
|
|
924
938
|
this.bootBackgroundTasks();
|
|
925
939
|
}
|
|
940
|
+
/**
|
|
941
|
+
* Take over the screen: alternate screen where it is safe, then the input
|
|
942
|
+
* modes the terminal actually supports, then hide the cursor.
|
|
943
|
+
*
|
|
944
|
+
* Joining the parts here (rather than at each call site) is what keeps
|
|
945
|
+
* `start`, a reattach and a teardown from drifting apart — the reattach path
|
|
946
|
+
* used to duplicate this string verbatim.
|
|
947
|
+
*/
|
|
948
|
+
enterScreenSequence() {
|
|
949
|
+
return `${this.useAlternateScreen ? '\x1b[?1049h' : ''}${mouseEnableSequence(this.terminalCaps)}${bracketedPasteSequence(this.terminalCaps, true)}\x1b[?25l`;
|
|
950
|
+
}
|
|
926
951
|
bindAgentEvents() {
|
|
927
952
|
this.disposers.push(this.ctx.on('session/event', this.handleSessionEvent), listenHostEvent(this.ctx, 'agent/assistant-stream', this.handleAssistantStream), this.ctx.on('agent/status', this.handleStatus), this.ctx.on('agent/error', this.handleError), this.ctx.on('agent/disposed', this.handleDisposed), this.ctx.on('agent/inbox/claimed', this.handleInboxClaimed), this.ctx.on('agent/inbox/discarded', this.handleInboxDiscarded), this.ctx.on('agent/request', this.handleAgentRequest), this.ctx.on('subagent/start', this.handleSubagentStart), this.ctx.on('subagent/end', this.handleSubagentEnd), this.ctx.on('approval/request', this.handleApproval));
|
|
928
953
|
const questions = this.ctx.get('userQuestions');
|
|
929
954
|
if (questions !== undefined) {
|
|
930
|
-
this.userQuestionDisposer = installUserQuestionAnswerer(this.ctx,
|
|
955
|
+
this.userQuestionDisposer = installUserQuestionAnswerer(this.ctx, this.handleUserQuestions);
|
|
931
956
|
}
|
|
932
957
|
}
|
|
933
958
|
bootBackgroundTasks() {
|
|
@@ -1008,7 +1033,7 @@ export class SshTui {
|
|
|
1008
1033
|
}
|
|
1009
1034
|
}
|
|
1010
1035
|
readSkippedUpdate() {
|
|
1011
|
-
const raw = this.ctx
|
|
1036
|
+
const raw = readSettingsSection(this.ctx, UI_LOCALE_NAMESPACE);
|
|
1012
1037
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
1013
1038
|
return undefined;
|
|
1014
1039
|
const skip = raw.skipUpdate;
|
|
@@ -1021,14 +1046,14 @@ export class SshTui {
|
|
|
1021
1046
|
const settings = this.ctx.get('settings');
|
|
1022
1047
|
if (settings === undefined)
|
|
1023
1048
|
return;
|
|
1024
|
-
const raw =
|
|
1049
|
+
const raw = readSettingsSection(this.ctx, UI_LOCALE_NAMESPACE);
|
|
1025
1050
|
const previous = raw !== null && typeof raw === 'object' && !Array.isArray(raw)
|
|
1026
1051
|
? raw
|
|
1027
1052
|
: {};
|
|
1028
1053
|
await settings.replace(UI_LOCALE_NAMESPACE, { ...previous, ...patch });
|
|
1029
1054
|
}
|
|
1030
1055
|
readWorkspaceView() {
|
|
1031
|
-
const raw = this.ctx
|
|
1056
|
+
const raw = readSettingsSection(this.ctx, UI_LOCALE_NAMESPACE);
|
|
1032
1057
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
1033
1058
|
return 'detailed';
|
|
1034
1059
|
return parseWorkspaceView(String(raw.view ?? '')) ?? 'detailed';
|
|
@@ -1037,7 +1062,7 @@ export class SshTui {
|
|
|
1037
1062
|
const env = parseDisconnectPolicy(process.env.DSH_TUI_DISCONNECT ?? '');
|
|
1038
1063
|
if (env !== undefined)
|
|
1039
1064
|
return env;
|
|
1040
|
-
const raw = this.ctx
|
|
1065
|
+
const raw = readSettingsSection(this.ctx, UI_LOCALE_NAMESPACE);
|
|
1041
1066
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
1042
1067
|
return 'pause';
|
|
1043
1068
|
return parseDisconnectPolicy(String(raw.disconnect ?? '')) ?? 'pause';
|
|
@@ -1046,7 +1071,7 @@ export class SshTui {
|
|
|
1046
1071
|
const env = parseAutoApprovalMode(process.env.DSH_TUI_AUTO_APPROVAL ?? '');
|
|
1047
1072
|
if (env !== undefined)
|
|
1048
1073
|
return env;
|
|
1049
|
-
const raw = this.ctx
|
|
1074
|
+
const raw = readSettingsSection(this.ctx, UI_LOCALE_NAMESPACE);
|
|
1050
1075
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
1051
1076
|
return 'off';
|
|
1052
1077
|
return parseAutoApprovalMode(String(raw.autoApproval ?? '')) ?? 'off';
|
|
@@ -1086,7 +1111,7 @@ export class SshTui {
|
|
|
1086
1111
|
const raw = Number.parseInt(process.env.DSH_TUI_IDLE_EXIT_MS ?? '', 10);
|
|
1087
1112
|
if (Number.isFinite(raw) && raw >= 0)
|
|
1088
1113
|
return raw;
|
|
1089
|
-
const saved = this.ctx
|
|
1114
|
+
const saved = readSettingsSection(this.ctx, UI_LOCALE_NAMESPACE);
|
|
1090
1115
|
if (saved !== null && typeof saved === 'object' && !Array.isArray(saved)) {
|
|
1091
1116
|
const value = saved.idleExit;
|
|
1092
1117
|
if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
|
|
@@ -1510,7 +1535,11 @@ export class SshTui {
|
|
|
1510
1535
|
try {
|
|
1511
1536
|
process.stdout.write('\x1b]0;\x07');
|
|
1512
1537
|
process.stdout.write('\x1b[0m\x1b[2J\x1b[3J\x1b[H');
|
|
1513
|
-
|
|
1538
|
+
// Leave the alternate screen unconditionally, the way every mouse mode is
|
|
1539
|
+
// disabled: `?1049l` on a terminal that never entered is ignored, while
|
|
1540
|
+
// *skipping* it after a reattach whose relay classified the terminal
|
|
1541
|
+
// differently leaves the user stuck in a screen they cannot scroll.
|
|
1542
|
+
process.stdout.write(`${mouseDisableSequence()}${bracketedPasteSequence(this.terminalCaps, false)}\x1b[?25h\x1b[?1049l`);
|
|
1514
1543
|
}
|
|
1515
1544
|
catch (error) {
|
|
1516
1545
|
if (!isHangupErrno(error)) {
|
|
@@ -1611,10 +1640,7 @@ export class SshTui {
|
|
|
1611
1640
|
}
|
|
1612
1641
|
/** Routing facts for the doctor's self-consistency check. */
|
|
1613
1642
|
doctorRouting() {
|
|
1614
|
-
const
|
|
1615
|
-
if (settings === undefined)
|
|
1616
|
-
return undefined;
|
|
1617
|
-
const section = settings.get(settingsNamespace('llm-pi-ai'));
|
|
1643
|
+
const section = readSettingsSection(this.ctx, settingsNamespace('llm-pi-ai'));
|
|
1618
1644
|
const providers = section?.providers ?? {};
|
|
1619
1645
|
const provider = this.currentProvider();
|
|
1620
1646
|
const models = Array.isArray(providers[provider]?.models) ? providers[provider]?.models ?? [] : [];
|
|
@@ -1640,6 +1666,7 @@ export class SshTui {
|
|
|
1640
1666
|
roster: this.ctx.get('agentPresets') !== undefined,
|
|
1641
1667
|
codeRuntime: this.hasHostService('codeRuntime'),
|
|
1642
1668
|
},
|
|
1669
|
+
generation: hostSettingsGeneration(this.ctx),
|
|
1643
1670
|
anchors: [process.argv[1], this.ctx.baseUrl],
|
|
1644
1671
|
...(routing === undefined ? {} : { routing }),
|
|
1645
1672
|
});
|
|
@@ -1660,11 +1687,11 @@ export class SshTui {
|
|
|
1660
1687
|
/** `/fix <row>`: repair one named roster row of the profile patch. */
|
|
1661
1688
|
async runFixCommand(arg) {
|
|
1662
1689
|
const name = arg.trim().toLowerCase();
|
|
1663
|
-
const row =
|
|
1690
|
+
const row = ALL_ROSTER_ROWS.find(candidate => candidate.id === name || candidate.name.toLowerCase() === name);
|
|
1664
1691
|
if (row === undefined) {
|
|
1665
1692
|
this.pushRow({
|
|
1666
1693
|
kind: 'error',
|
|
1667
|
-
text: t('doctor.fix.unknownRow', { row: arg.trim(), rows:
|
|
1694
|
+
text: t('doctor.fix.unknownRow', { row: arg.trim(), rows: ALL_ROSTER_ROWS.map(candidate => candidate.id).join(', ') }),
|
|
1668
1695
|
});
|
|
1669
1696
|
this.markDirty();
|
|
1670
1697
|
return;
|
|
@@ -1683,7 +1710,7 @@ export class SshTui {
|
|
|
1683
1710
|
const duplicates = facts.patch.readable ? planDuplicateRepair(text) : undefined;
|
|
1684
1711
|
if (duplicates !== undefined)
|
|
1685
1712
|
text = duplicates.text;
|
|
1686
|
-
const roster = planRosterRepair(text, rows);
|
|
1713
|
+
const roster = planRosterRepair(text, rows, facts.generation ?? 'legacy');
|
|
1687
1714
|
if (roster !== undefined)
|
|
1688
1715
|
text = roster.text;
|
|
1689
1716
|
const added = roster?.added ?? [];
|
|
@@ -2066,7 +2093,7 @@ export class SshTui {
|
|
|
2066
2093
|
void this.onReattach?.();
|
|
2067
2094
|
return;
|
|
2068
2095
|
}
|
|
2069
|
-
this.write(
|
|
2096
|
+
this.write(this.enterScreenSequence());
|
|
2070
2097
|
this.forceFullPaint = true;
|
|
2071
2098
|
this.dirty = true;
|
|
2072
2099
|
// The user is back: say so, with how many times this Host has been
|
|
@@ -2375,7 +2402,7 @@ export class SshTui {
|
|
|
2375
2402
|
const note = planDockNote(plan);
|
|
2376
2403
|
lines.push(this.styleLine('plan-dock', padToWidth(` ${note}`, width)));
|
|
2377
2404
|
if (plan.planMarkdown !== undefined && plan.planMarkdown !== '') {
|
|
2378
|
-
const markdown = renderMarkdownLines(plan.planMarkdown, inner, this.color);
|
|
2405
|
+
const markdown = renderMarkdownLines(plan.planMarkdown, inner, this.color, this.terminalCaps.osc8);
|
|
2379
2406
|
const budget = Math.max(4, Math.min(12, markdown.length));
|
|
2380
2407
|
for (const line of markdown.slice(0, budget)) {
|
|
2381
2408
|
lines.push(`${clipAnsiToWidth(` ${line}`, width)}\x1b[0m`);
|
|
@@ -2944,7 +2971,7 @@ export class SshTui {
|
|
|
2944
2971
|
};
|
|
2945
2972
|
const pushRow = (kind, text, ref) => {
|
|
2946
2973
|
if (kind === 'assistant') {
|
|
2947
|
-
for (const line of renderMarkdownLines(text, width, this.color)) {
|
|
2974
|
+
for (const line of renderMarkdownLines(text, width, this.color, this.terminalCaps.osc8)) {
|
|
2948
2975
|
addDisplay(line, ref);
|
|
2949
2976
|
}
|
|
2950
2977
|
return;
|
|
@@ -3087,7 +3114,7 @@ export class SshTui {
|
|
|
3087
3114
|
if (row.expanded) {
|
|
3088
3115
|
addDisplay(this.styleLine('plan-dock', ` ${planDockNote({ ...row, active: false, pending: false })}`), row);
|
|
3089
3116
|
if (row.planMarkdown !== undefined && row.planMarkdown !== '') {
|
|
3090
|
-
for (const line of renderMarkdownLines(row.planMarkdown, Math.max(1, width - 2), this.color).slice(0, 8)) {
|
|
3117
|
+
for (const line of renderMarkdownLines(row.planMarkdown, Math.max(1, width - 2), this.color, this.terminalCaps.osc8).slice(0, 8)) {
|
|
3091
3118
|
addDisplay(` ${line}`, row);
|
|
3092
3119
|
}
|
|
3093
3120
|
}
|
|
@@ -3125,7 +3152,7 @@ export class SshTui {
|
|
|
3125
3152
|
}
|
|
3126
3153
|
if (row.detail !== undefined && row.detail !== '') {
|
|
3127
3154
|
if (row.intent === 'plan-review') {
|
|
3128
|
-
for (const line of renderMarkdownLines(row.detail, Math.max(1, width - 2), this.color)) {
|
|
3155
|
+
for (const line of renderMarkdownLines(row.detail, Math.max(1, width - 2), this.color, this.terminalCaps.osc8)) {
|
|
3129
3156
|
addDisplay(` ${line}`, row);
|
|
3130
3157
|
}
|
|
3131
3158
|
}
|
|
@@ -3408,7 +3435,7 @@ export class SshTui {
|
|
|
3408
3435
|
addDialog(t('dialog.planReview', { index: d.index + 1, total: d.total }) + (d.question.header === undefined ? '' : ` · ${d.question.header}`));
|
|
3409
3436
|
addDialog(d.question.question);
|
|
3410
3437
|
if (d.question.detail !== undefined && d.question.detail !== '') {
|
|
3411
|
-
for (const line of renderMarkdownLines(d.question.detail, Math.max(1, width - 2), this.color).slice(0, 16)) {
|
|
3438
|
+
for (const line of renderMarkdownLines(d.question.detail, Math.max(1, width - 2), this.color, this.terminalCaps.osc8).slice(0, 16)) {
|
|
3412
3439
|
dialogLines.push(this.styleLine('assistant', line));
|
|
3413
3440
|
}
|
|
3414
3441
|
}
|
|
@@ -3670,7 +3697,7 @@ export class SshTui {
|
|
|
3670
3697
|
// the session counters go before the operational signals. Quota and context
|
|
3671
3698
|
// pressure are deliberately not here — they are on the identity line.
|
|
3672
3699
|
const strip = [];
|
|
3673
|
-
const health = footerHealthChip(this.rosterMissing, this.color);
|
|
3700
|
+
const health = footerHealthChip(this.rosterMissing, this.color, this.settingsGeneration === 'forms' ? 'agent-plane' : 'roster');
|
|
3674
3701
|
// Muted like the identity line, accent excepted: the pre-strip footer
|
|
3675
3702
|
// painted the counters dim and only the ⚠'s own glyph yellow.
|
|
3676
3703
|
if (health !== undefined)
|
|
@@ -3828,6 +3855,10 @@ export class SshTui {
|
|
|
3828
3855
|
updateTerminalTitle() {
|
|
3829
3856
|
if (this.exiting || this.lineMode)
|
|
3830
3857
|
return;
|
|
3858
|
+
// A console that has no title to set (the Linux virtual console) would take
|
|
3859
|
+
// the escape as noise; the matrix says so, so honour it.
|
|
3860
|
+
if (!this.terminalCaps.title)
|
|
3861
|
+
return;
|
|
3831
3862
|
const now = Date.now();
|
|
3832
3863
|
// Completion wins over a still-running agent status: the turn/end event
|
|
3833
3864
|
// lands before agent/status flips to idle, and the title must not stay
|
|
@@ -3936,13 +3967,7 @@ export class SshTui {
|
|
|
3936
3967
|
if (service === undefined) {
|
|
3937
3968
|
this.pushRow({
|
|
3938
3969
|
kind: 'error',
|
|
3939
|
-
text:
|
|
3940
|
-
t('preset.missingService'),
|
|
3941
|
-
t('mode.missingServiceHint', {
|
|
3942
|
-
profile: profileFromArgv(),
|
|
3943
|
-
patch: rosterPatchPath(resolveDshHome(), profileFromArgv()),
|
|
3944
|
-
}),
|
|
3945
|
-
].join('\n'),
|
|
3970
|
+
text: this.missingPresetService(t('preset.missingService')),
|
|
3946
3971
|
});
|
|
3947
3972
|
this.markDirty();
|
|
3948
3973
|
return;
|
|
@@ -4122,9 +4147,11 @@ export class SshTui {
|
|
|
4122
4147
|
}
|
|
4123
4148
|
const lines = [t('preset.showTitle', { id: preset.id, name: preset.name ?? preset.id })];
|
|
4124
4149
|
lines.push(t('preset.showMeta', {
|
|
4125
|
-
trust: preset.trust === 'user'
|
|
4150
|
+
trust: preset.trust === 'user'
|
|
4151
|
+
? t('preset.trustUser')
|
|
4152
|
+
: preset.trust === undefined ? t('preset.trustManaged') : t('preset.trustSystem'),
|
|
4126
4153
|
order: preset.order === undefined ? '—' : String(preset.order),
|
|
4127
|
-
directory: presetDirectory(preset),
|
|
4154
|
+
directory: presetDirectory(preset) ?? t('preset.directoryManaged'),
|
|
4128
4155
|
}));
|
|
4129
4156
|
if (preset.description !== undefined)
|
|
4130
4157
|
lines.push(t('preset.showDescription', { description: preset.description }));
|
|
@@ -4189,7 +4216,7 @@ export class SshTui {
|
|
|
4189
4216
|
from: plan.from,
|
|
4190
4217
|
id: plan.id,
|
|
4191
4218
|
name: plan.name === undefined ? '' : ` · ${plan.name}`,
|
|
4192
|
-
directory: presetDirectory(presets.find(candidate => candidate.id === plan.id) ?? presets[0]),
|
|
4219
|
+
directory: presetDirectory(presets.find(candidate => candidate.id === plan.id) ?? presets[0]) ?? '',
|
|
4193
4220
|
});
|
|
4194
4221
|
});
|
|
4195
4222
|
}
|
|
@@ -4212,7 +4239,8 @@ export class SshTui {
|
|
|
4212
4239
|
}
|
|
4213
4240
|
// Merge with what the preset publishes now, so editing one field never
|
|
4214
4241
|
// drops the other and `order` survives.
|
|
4215
|
-
const
|
|
4242
|
+
const directory = presetDirectory(preset);
|
|
4243
|
+
const current = directory === undefined ? {} : await readPresetMetadata(directory);
|
|
4216
4244
|
const plan = planMetadata({ preset, current, patch });
|
|
4217
4245
|
if (isRefusal(plan)) {
|
|
4218
4246
|
this.pushPresetRefusal(plan);
|
|
@@ -4255,7 +4283,7 @@ export class SshTui {
|
|
|
4255
4283
|
return;
|
|
4256
4284
|
}
|
|
4257
4285
|
const answer = await new Promise(resolve => {
|
|
4258
|
-
this.openConfirm(t('preset.deleteConfirm', { id: plan.id }), t('preset.deleteHint', { directory: presetDirectory(preset) }), resolve);
|
|
4286
|
+
this.openConfirm(t('preset.deleteConfirm', { id: plan.id }), t('preset.deleteHint', { directory: presetDirectory(preset) ?? t('preset.directoryManaged') }), resolve);
|
|
4259
4287
|
});
|
|
4260
4288
|
if (answer !== 'y') {
|
|
4261
4289
|
this.pushRow({ kind: 'system', text: t('preset.deleteCancelled') });
|
|
@@ -4384,11 +4412,11 @@ export class SshTui {
|
|
|
4384
4412
|
return `\x1b[7m${styled.replaceAll('\x1b[0m', '\x1b[0m\x1b[7m')}\x1b[27m`;
|
|
4385
4413
|
}
|
|
4386
4414
|
/**
|
|
4387
|
-
* Fold one live
|
|
4388
|
-
*
|
|
4389
|
-
*
|
|
4415
|
+
* Fold one live stream chunk into the in-progress assistant row. Hosts from
|
|
4416
|
+
* 0.1.5 on emit the chunk on `agent/assistant-stream` and never write it to
|
|
4417
|
+
* the log.
|
|
4390
4418
|
*/
|
|
4391
|
-
applyStreamChunk(streamed
|
|
4419
|
+
applyStreamChunk(streamed) {
|
|
4392
4420
|
const { chunk } = streamed;
|
|
4393
4421
|
if (isTokenDeltaChunk(chunk)) {
|
|
4394
4422
|
this.statsTracker.noteFirstToken(streamed.turn, streamed.step, streamed.time);
|
|
@@ -4396,8 +4424,6 @@ export class SshTui {
|
|
|
4396
4424
|
if (chunk.type === 'usage' && chunk.usage !== undefined && streamed.stepKnown) {
|
|
4397
4425
|
this.statsTracker.recordUsage(streamed.turn, streamed.step, chunk.usage);
|
|
4398
4426
|
}
|
|
4399
|
-
if (statsOnly)
|
|
4400
|
-
return;
|
|
4401
4427
|
if (chunk.type === 'text-delta') {
|
|
4402
4428
|
this.streaming ??= { text: '', reasoning: '' };
|
|
4403
4429
|
this.streaming.text += chunk.text ?? '';
|
|
@@ -4414,9 +4440,9 @@ export class SshTui {
|
|
|
4414
4440
|
}
|
|
4415
4441
|
}
|
|
4416
4442
|
/**
|
|
4417
|
-
* 0.1.5 live tokens arrive as process-local
|
|
4418
|
-
* frames (start / chunk / end). Chunk frames carry
|
|
4419
|
-
* `StreamChunk` the
|
|
4443
|
+
* 0.1.5 and 0.1.7 live tokens arrive as process-local
|
|
4444
|
+
* `agent/assistant-stream` frames (start / chunk / end). Chunk frames carry
|
|
4445
|
+
* the same `StreamChunk` the settled `assistant/message` embeds in `stream`.
|
|
4420
4446
|
*/
|
|
4421
4447
|
handleAssistantStream = (...args) => {
|
|
4422
4448
|
const payload = args[0];
|
|
@@ -4505,23 +4531,8 @@ export class SshTui {
|
|
|
4505
4531
|
return;
|
|
4506
4532
|
}
|
|
4507
4533
|
this.lastActivity = Date.now();
|
|
4508
|
-
if (this.replaying && isAssistantStreamEvent(event)) {
|
|
4509
|
-
// Replay skips the in-progress row, but durable chunks still carry the
|
|
4510
|
-
// timing the footer needs: a resumed session must keep TTFT and tok/s.
|
|
4511
|
-
const streamed = streamChunkOf(event);
|
|
4512
|
-
if (streamed !== undefined)
|
|
4513
|
-
this.applyStreamChunk(streamed, true);
|
|
4514
|
-
return;
|
|
4515
|
-
}
|
|
4516
4534
|
if (!this.replaying)
|
|
4517
4535
|
this.refreshContextPressure();
|
|
4518
|
-
const eventType = sessionEventType(event);
|
|
4519
|
-
if (eventType === 'assistant/chunk') {
|
|
4520
|
-
const streamed = streamChunkOf(event);
|
|
4521
|
-
if (streamed !== undefined)
|
|
4522
|
-
this.applyStreamChunk(streamed);
|
|
4523
|
-
return;
|
|
4524
|
-
}
|
|
4525
4536
|
switch (event.type) {
|
|
4526
4537
|
case 'user/message': {
|
|
4527
4538
|
const text = event.data.content
|
|
@@ -4579,8 +4590,7 @@ export class SshTui {
|
|
|
4579
4590
|
.join('');
|
|
4580
4591
|
// The settlement's own packed stream is authoritative: a retried step
|
|
4581
4592
|
// keeps the failed attempt's first token in the live latch, and spanning
|
|
4582
|
-
// both attempts reported a rate ~20x off.
|
|
4583
|
-
// relies on the replayed `assistant/chunk` events (or the live latch).
|
|
4593
|
+
// both attempts reported a rate ~20x off.
|
|
4584
4594
|
this.statsTracker.settleMessage({
|
|
4585
4595
|
turn: event.data.turn,
|
|
4586
4596
|
step: event.data.step,
|
|
@@ -4694,7 +4704,7 @@ export class SshTui {
|
|
|
4694
4704
|
this.markDirty();
|
|
4695
4705
|
break;
|
|
4696
4706
|
}
|
|
4697
|
-
const failed = event.data.error !== undefined || event.data.message
|
|
4707
|
+
const failed = event.data.error !== undefined || toolResultFailed(event.data.message);
|
|
4698
4708
|
spawnCard.status = failed ? 'error' : 'ok';
|
|
4699
4709
|
spawnCard.endedAt = typeof event.time === 'number' ? event.time : Date.now();
|
|
4700
4710
|
if (output !== '') {
|
|
@@ -4725,7 +4735,7 @@ export class SshTui {
|
|
|
4725
4735
|
row.output = output;
|
|
4726
4736
|
}
|
|
4727
4737
|
const failed = event.data.error !== undefined
|
|
4728
|
-
|| event.data.message
|
|
4738
|
+
|| toolResultFailed(event.data.message)
|
|
4729
4739
|
|| (isShell && ((row.exitCode !== undefined && row.exitCode !== 0) || row.signal !== undefined));
|
|
4730
4740
|
row.status = failed ? 'error' : 'ok';
|
|
4731
4741
|
if (READ_TOOL_NAMES.has(row.name)) {
|
|
@@ -5418,8 +5428,6 @@ export class SshTui {
|
|
|
5418
5428
|
const row = this.findSubagentRow(String(sessionId));
|
|
5419
5429
|
if (row === undefined)
|
|
5420
5430
|
return;
|
|
5421
|
-
if (sessionEventType(event) === 'assistant/chunk')
|
|
5422
|
-
return;
|
|
5423
5431
|
switch (event.type) {
|
|
5424
5432
|
case 'user/message': {
|
|
5425
5433
|
const text = collectText(event.data.content);
|
|
@@ -5450,7 +5458,7 @@ export class SshTui {
|
|
|
5450
5458
|
break;
|
|
5451
5459
|
}
|
|
5452
5460
|
case 'tool/result': {
|
|
5453
|
-
const ok = event.data.error === undefined && event.data.message
|
|
5461
|
+
const ok = event.data.error === undefined && !toolResultFailed(event.data.message);
|
|
5454
5462
|
const callId = String(event.data.message.source.callId);
|
|
5455
5463
|
const sourceName = event.data.message.source.name;
|
|
5456
5464
|
const recorded = this.toolCallNames.get(callId)
|
|
@@ -6209,7 +6217,7 @@ export class SshTui {
|
|
|
6209
6217
|
piAiProviderProfile(provider) {
|
|
6210
6218
|
if (provider === 'deepseek-official')
|
|
6211
6219
|
return undefined;
|
|
6212
|
-
const section = this.ctx
|
|
6220
|
+
const section = readSettingsSection(this.ctx, settingsNamespace('llm-pi-ai'));
|
|
6213
6221
|
return section?.providers?.[provider];
|
|
6214
6222
|
}
|
|
6215
6223
|
/**
|
|
@@ -6428,7 +6436,7 @@ export class SshTui {
|
|
|
6428
6436
|
* picker costs one listing instead of one per sibling.
|
|
6429
6437
|
*/
|
|
6430
6438
|
async discoverGatewayListing(provider, signal) {
|
|
6431
|
-
const llmPiAi = this.ctx
|
|
6439
|
+
const llmPiAi = readSettingsSection(this.ctx, settingsNamespace('llm-pi-ai'));
|
|
6432
6440
|
const profile = this.piAiProviderProfile(provider);
|
|
6433
6441
|
const source = openCodeSourceFor(provider, llmPiAi);
|
|
6434
6442
|
const baseURL = typeof profile?.baseURL === 'string' && profile.baseURL.trim() !== ''
|
|
@@ -6821,7 +6829,7 @@ export class SshTui {
|
|
|
6821
6829
|
}
|
|
6822
6830
|
/** Persist a provider/model/effort choice and keep the subagent on the same family. */
|
|
6823
6831
|
rememberedRoute(provider) {
|
|
6824
|
-
const section = this.ctx
|
|
6832
|
+
const section = readSettingsSection(this.ctx, ROUTE_MEMORY_NS);
|
|
6825
6833
|
const memory = section !== null && typeof section === 'object' && !Array.isArray(section)
|
|
6826
6834
|
? parseRouteMemory(section.providers)
|
|
6827
6835
|
: {};
|
|
@@ -6870,7 +6878,7 @@ export class SshTui {
|
|
|
6870
6878
|
const settings = this.ctx.get('settings');
|
|
6871
6879
|
if (settings === undefined)
|
|
6872
6880
|
return;
|
|
6873
|
-
const section =
|
|
6881
|
+
const section = readSettingsSection(this.ctx, ROUTE_MEMORY_NS);
|
|
6874
6882
|
const memory = section !== null && typeof section === 'object' && !Array.isArray(section)
|
|
6875
6883
|
? parseRouteMemory(section.providers)
|
|
6876
6884
|
: {};
|
|
@@ -6986,7 +6994,7 @@ export class SshTui {
|
|
|
6986
6994
|
}
|
|
6987
6995
|
if (settings !== undefined) {
|
|
6988
6996
|
try {
|
|
6989
|
-
await settings.replace(
|
|
6997
|
+
await settings.replace(AGENT_DEFAULT_MODEL_NS, {
|
|
6990
6998
|
provider: next.provider,
|
|
6991
6999
|
model: next.model,
|
|
6992
7000
|
...(next.reasoningEffort === undefined ? {} : { reasoningEffort: String(next.reasoningEffort) }),
|
|
@@ -7586,7 +7594,7 @@ export class SshTui {
|
|
|
7586
7594
|
}
|
|
7587
7595
|
/** /mode: pick an agent preset (standard / minimal / ptc / cordis / routing-suite / ...). */
|
|
7588
7596
|
async runModeCommand(arg = '') {
|
|
7589
|
-
const agentPresets = this.
|
|
7597
|
+
const agentPresets = this.presetService();
|
|
7590
7598
|
const direct = arg.trim().toLowerCase();
|
|
7591
7599
|
if (direct === 'fix' || direct === 'repair') {
|
|
7592
7600
|
await this.repairRoster();
|
|
@@ -7601,13 +7609,7 @@ export class SshTui {
|
|
|
7601
7609
|
const profile = profileFromArgv();
|
|
7602
7610
|
this.pushRow({
|
|
7603
7611
|
kind: 'error',
|
|
7604
|
-
text:
|
|
7605
|
-
t('mode.missingService'),
|
|
7606
|
-
t('mode.missingServiceHint', {
|
|
7607
|
-
profile,
|
|
7608
|
-
patch: rosterPatchPath(resolveDshHome(), profile),
|
|
7609
|
-
}),
|
|
7610
|
-
].join('\n'),
|
|
7612
|
+
text: this.missingPresetService(t('mode.missingService')),
|
|
7611
7613
|
});
|
|
7612
7614
|
this.markDirty();
|
|
7613
7615
|
return;
|
|
@@ -7667,6 +7669,13 @@ export class SshTui {
|
|
|
7667
7669
|
const selectedName = option.label;
|
|
7668
7670
|
const hasWork = sessionEvents(this.agent.session).some(event => event.type === 'turn/start');
|
|
7669
7671
|
if (!hasWork) {
|
|
7672
|
+
// Feature-detected: a host whose registry predates `recompose` still
|
|
7673
|
+
// lists presets, and switching then needs a restarted session.
|
|
7674
|
+
if (typeof agentPresets.recompose !== 'function') {
|
|
7675
|
+
this.pushRow({ kind: 'error', text: t('mode.noRecompose', { id: selected.id }) });
|
|
7676
|
+
this.markDirty();
|
|
7677
|
+
return;
|
|
7678
|
+
}
|
|
7670
7679
|
await agentPresets.recompose(this.agent.ctx, selected.id);
|
|
7671
7680
|
this.presetId = selected.id;
|
|
7672
7681
|
this.presetName = selectedName;
|
|
@@ -7690,13 +7699,40 @@ export class SshTui {
|
|
|
7690
7699
|
*/
|
|
7691
7700
|
/** Re-read the roster fact this process was composed with (cheap, no IO). */
|
|
7692
7701
|
refreshRosterHealth() {
|
|
7693
|
-
|
|
7702
|
+
// 0.1.7 has no roster to be missing: what a terminal profile owns there is
|
|
7703
|
+
// the agent-plane rows, so the chip reports those instead.
|
|
7704
|
+
this.rosterMissing = this.settingsGeneration === 'forms'
|
|
7705
|
+
? !formsRowsDeclared(resolveDshHome(), profileFromArgv())
|
|
7706
|
+
: this.ctx.get('agentPresets') === undefined;
|
|
7707
|
+
}
|
|
7708
|
+
/** Which settings protocol this host speaks; decides what "missing" means. */
|
|
7709
|
+
get settingsGeneration() {
|
|
7710
|
+
return hostSettingsGeneration(this.ctx);
|
|
7711
|
+
}
|
|
7712
|
+
/**
|
|
7713
|
+
* The two lines `/mode` and `/preset` print when the preset service is absent.
|
|
7714
|
+
*
|
|
7715
|
+
* The reason differs by host line: 0.1.5 lost a roster the profile can mount,
|
|
7716
|
+
* while 0.1.7 composes the agent process-wide and has no terminal presets at
|
|
7717
|
+
* all — the repair there is the agent-plane rows, not a roster.
|
|
7718
|
+
*/
|
|
7719
|
+
missingPresetService(first) {
|
|
7720
|
+
const profile = profileFromArgv();
|
|
7721
|
+
const patch = rosterPatchPath(resolveDshHome(), profile);
|
|
7722
|
+
return this.settingsGeneration === 'forms'
|
|
7723
|
+
? [first, t('mode.formsHost'), t('mode.formsHint', { profile, patch })].join('\n')
|
|
7724
|
+
: [first, t('mode.missingServiceHint', { profile, patch })].join('\n');
|
|
7694
7725
|
}
|
|
7695
7726
|
async repairRoster() {
|
|
7696
7727
|
const profile = profileFromArgv();
|
|
7697
7728
|
const patch = rosterPatchPath(resolveDshHome(), profile);
|
|
7698
7729
|
try {
|
|
7699
|
-
|
|
7730
|
+
// Which rows are "the rows" is a host-line fact: 0.1.7 composes the agent
|
|
7731
|
+
// process-wide and owns the agent-plane rows instead of the roster. Both
|
|
7732
|
+
// the list and the header come from the same generation `/doctor` uses,
|
|
7733
|
+
// or this writes rows the running host cannot resolve.
|
|
7734
|
+
const generation = this.settingsGeneration;
|
|
7735
|
+
const result = await ensureRosterRows(resolveDshHome(), profile, rosterRows(generation), generation);
|
|
7700
7736
|
this.refreshRosterHealth();
|
|
7701
7737
|
this.pushRow({
|
|
7702
7738
|
kind: 'system',
|
|
@@ -7817,7 +7853,7 @@ export class SshTui {
|
|
|
7817
7853
|
return;
|
|
7818
7854
|
}
|
|
7819
7855
|
const provider = this.currentProviderId();
|
|
7820
|
-
const llmPiAi = this.ctx
|
|
7856
|
+
const llmPiAi = readSettingsSection(this.ctx, settingsNamespace('llm-pi-ai'));
|
|
7821
7857
|
const source = openCodeSourceFor(provider, llmPiAi);
|
|
7822
7858
|
if (source?.flavor === 'zen') {
|
|
7823
7859
|
this.pushRow({ kind: 'system', text: this.zenUsageText(source) });
|
|
@@ -7879,7 +7915,7 @@ export class SshTui {
|
|
|
7879
7915
|
* neither gets nothing.
|
|
7880
7916
|
*/
|
|
7881
7917
|
hasQuotaSurface(provider) {
|
|
7882
|
-
const llmPiAi = this.ctx
|
|
7918
|
+
const llmPiAi = readSettingsSection(this.ctx, settingsNamespace('llm-pi-ai'));
|
|
7883
7919
|
return providerHasQuotaSurface(provider, llmPiAi);
|
|
7884
7920
|
}
|
|
7885
7921
|
async refreshQuota(options) {
|
|
@@ -7938,7 +7974,7 @@ export class SshTui {
|
|
|
7938
7974
|
const apiKey = await this.resolveCredential('DEEPSEEK_API_KEY');
|
|
7939
7975
|
if (apiKey === undefined)
|
|
7940
7976
|
throw new Error(t('usage.noDeepseekKey'));
|
|
7941
|
-
const section = this.ctx
|
|
7977
|
+
const section = readSettingsSection(this.ctx, settingsNamespace('llm-deepseek'));
|
|
7942
7978
|
const baseURL = typeof section?.baseURL === 'string' && section.baseURL.trim() !== ''
|
|
7943
7979
|
? section.baseURL.trim()
|
|
7944
7980
|
: (process.env.DEEPSEEK_BASE_URL?.trim() || DEEPSEEK_PUBLIC_BASE_URL);
|
|
@@ -8007,7 +8043,7 @@ export class SshTui {
|
|
|
8007
8043
|
return parseSuperGrokBilling(payload);
|
|
8008
8044
|
}
|
|
8009
8045
|
}
|
|
8010
|
-
const llmPiAi = this.ctx
|
|
8046
|
+
const llmPiAi = readSettingsSection(this.ctx, settingsNamespace('llm-pi-ai'));
|
|
8011
8047
|
const commandCode = commandCodeSourceFor(provider, llmPiAi);
|
|
8012
8048
|
if (commandCode !== null)
|
|
8013
8049
|
return this.fetchCommandCodeQuota(commandCode);
|
|
@@ -9374,6 +9410,10 @@ export class SshTui {
|
|
|
9374
9410
|
const shadowed = shadowing !== undefined && shadowing !== '';
|
|
9375
9411
|
if (credentials !== undefined && !shadowed) {
|
|
9376
9412
|
await credentials.set(credentialRef(envRef), key);
|
|
9413
|
+
// The store writes it; we are the reason it exists. On Windows that write
|
|
9414
|
+
// ignored any mode the store passed, so the file that holds every API key
|
|
9415
|
+
// gets its ACL tightened here.
|
|
9416
|
+
await restrictPathToUser(join(dshHomeDir(), '.credentials.yaml'), { mode: 0o600 });
|
|
9377
9417
|
this.pushRow({ kind: 'system', text: t('onboard.credSaved', { env: envRef, path: displayDshPath('.credentials.yaml') }) });
|
|
9378
9418
|
return;
|
|
9379
9419
|
}
|
|
@@ -9384,13 +9424,13 @@ export class SshTui {
|
|
|
9384
9424
|
? IS_WINDOWS
|
|
9385
9425
|
? t('onboard.envShadowWin', { env: envRef })
|
|
9386
9426
|
: t('onboard.envShadowUnix', { env: envRef })
|
|
9387
|
-
: t('onboard.credMissing', { path: displayDshPath(
|
|
9427
|
+
: t('onboard.credMissing', { path: displayDshPath(envFileName()) }),
|
|
9388
9428
|
});
|
|
9389
9429
|
}
|
|
9390
9430
|
/** Write launch-environment overrides so they beat system-injected variables. */
|
|
9391
9431
|
async writeLaunchEnv(entries) {
|
|
9392
9432
|
const home = dshHomeDir();
|
|
9393
|
-
const file = join(home,
|
|
9433
|
+
const file = join(home, envFileName());
|
|
9394
9434
|
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
9395
9435
|
if (IS_WINDOWS) {
|
|
9396
9436
|
let previous = '';
|
|
@@ -9414,6 +9454,10 @@ export class SshTui {
|
|
|
9414
9454
|
const additions = Object.entries(entries).map(([name, value]) => `set "${name}=${value.replaceAll('"', '')}"`);
|
|
9415
9455
|
const lines = ['@echo off', 'rem Generated by dsh-ssh-tui onboarding.', ...preserved, ...additions];
|
|
9416
9456
|
await writeFile(file, `${lines.join('\r\n')}\r\n`, { mode: 0o600 });
|
|
9457
|
+
// This file holds API keys. `mode` covers POSIX; on Windows the same
|
|
9458
|
+
// intent has to be an ACL or the keys sit under whatever the parent
|
|
9459
|
+
// directory allowed.
|
|
9460
|
+
await restrictPathToUser(file, { mode: 0o600 });
|
|
9417
9461
|
// Persist for future processes; best-effort, env.cmd remains as a manual fallback.
|
|
9418
9462
|
await Promise.all(Object.entries(entries).map(([name, value]) => this.setWindowsEnv(name, value))).catch(() => { });
|
|
9419
9463
|
return;
|
|
@@ -9437,6 +9481,7 @@ export class SshTui {
|
|
|
9437
9481
|
});
|
|
9438
9482
|
const additions = Object.entries(entries).map(([name, value]) => `export ${name}=${quote(value)}`);
|
|
9439
9483
|
await writeFile(file, `${['# Generated by dsh-ssh-tui onboarding.', ...preserved, ...additions].join('\n')}\n`, { mode: 0o600 });
|
|
9484
|
+
await restrictPathToUser(file, { mode: 0o600 });
|
|
9440
9485
|
await this.ensurePosixEnvHook();
|
|
9441
9486
|
}
|
|
9442
9487
|
/** Persist one variable into the Windows user environment (best-effort). */
|
|
@@ -9699,6 +9744,16 @@ export class SshTui {
|
|
|
9699
9744
|
this.leaveHistoryBrowse();
|
|
9700
9745
|
this.write(osc52Clipboard(text));
|
|
9701
9746
|
this.pushRow({ kind: 'system', text: notice });
|
|
9747
|
+
// The write is a request the terminal may ignore: VTE (GNOME/XFCE/…) has
|
|
9748
|
+
// never implemented OSC 52, conhost and screen cannot, Konsole only from
|
|
9749
|
+
// 24.12, and xterm/tmux need configuration. The write still goes out — it is
|
|
9750
|
+
// how the terminals that *do* honour it work — but where the table does not
|
|
9751
|
+
// promise it the user is told once per session rather than at every copy, or
|
|
9752
|
+
// not at all until they paste into an empty clipboard.
|
|
9753
|
+
if (!this.terminalCaps.osc52 && !this.osc52HintShown) {
|
|
9754
|
+
this.osc52HintShown = true;
|
|
9755
|
+
this.pushRow({ kind: 'system', text: t('copy.osc52Hint', { terminal: this.terminalCaps.label }) });
|
|
9756
|
+
}
|
|
9702
9757
|
this.markDirty();
|
|
9703
9758
|
}
|
|
9704
9759
|
copyFocusedCard(arg = '') {
|
|
@@ -10388,7 +10443,16 @@ export function mountTui(ctx, config) {
|
|
|
10388
10443
|
else
|
|
10389
10444
|
process.exit(1);
|
|
10390
10445
|
};
|
|
10391
|
-
|
|
10446
|
+
// The explicit `return undefined` is deliberate, not noise: 0.1.6-alpha.2 turns
|
|
10447
|
+
// `agent/created` into a serial event whose handler must return
|
|
10448
|
+
// `undefined | Promise<undefined>`, and a bare `void` body is *not* assignable
|
|
10449
|
+
// to that (verified by compiling this file against the published 0.1.6-alpha.2
|
|
10450
|
+
// types: TS2322/TS2345). Returning `undefined` satisfies the whole 0.1.5 line
|
|
10451
|
+
// too, so the handler is ready for the next line without a compat branch.
|
|
10452
|
+
const disposeCreated = ctx.on('agent/created', ({ agent }) => {
|
|
10453
|
+
start(agent);
|
|
10454
|
+
return undefined;
|
|
10455
|
+
});
|
|
10392
10456
|
const disposeFailure = ctx.on('agent-loop/config-start-failed', ({ sessionId: failedSessionId, error }) => fail(failedSessionId, error));
|
|
10393
10457
|
const stopWaiting = () => {
|
|
10394
10458
|
disposeCreated();
|