dsh-ssh-tui 0.7.0-rc.2 → 0.7.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.en.md +133 -64
- package/README.md +75 -75
- package/docs/remote-ops.md +21 -0
- package/lib/color-depth.js +34 -0
- package/lib/color-depth.js.map +1 -1
- package/lib/footer.js +123 -16
- package/lib/footer.js.map +1 -1
- package/lib/quota.js +23 -2
- package/lib/quota.js.map +1 -1
- package/lib/tui.js +94 -11
- package/lib/tui.js.map +1 -1
- package/lib/types/footer.d.ts +67 -7
- package/lib/types/quota.d.ts +17 -0
- package/lib/types/transcript-types.d.ts +8 -0
- package/lib/types/tui.d.ts +19 -2
- package/package.json +1 -1
package/lib/tui.js
CHANGED
|
@@ -60,14 +60,14 @@ import { clipAnsiToWidth, hrefAtColumn, osc52Clipboard, paintedLinkHits, cursorV
|
|
|
60
60
|
import { clampSelection, offsetAfterColumn, offsetAtColumn, selectionSpans, selectionText, } from './selection.js';
|
|
61
61
|
import { captureHangupSignals, advancePaintedRows, composePaintFrame, composePaintOutput, frameByteBudget, detectSshSession, formatLinkQualityChip, HANGUP_CANCEL_TIMEOUT_MS, ignoreFurtherHangupSignals, isEscapePrefix, isHangupErrno, linkQualityOf, parseCursorPositionReply, PICKER_WINDOW, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, toolBodyLineLimit, waitUntilIdleOrTimeout, } from './paint.js';
|
|
62
62
|
import { TerminalInputGuard } from './terminal-input.js';
|
|
63
|
-
import { contextPressureAlertText, contextPressureRingColor, contextPressureView, formatContextPressureRing, describeProviderRoute, fitFooterChips, fitFooterStatusLine, footerActivity, footerHealthChip, footerIdentityParts, footerStatsGroups, formatContextPressureChip, formatStatusReport, formatTokens, parseContextPressure, promptPressureTokens, providerUsesLocalOAuth, shouldIdleAutoCompact, } from './footer.js';
|
|
64
|
-
import { COMMAND_CODE_CREDITS_URL, COMMAND_CODE_SUBSCRIPTIONS_URL, COMMAND_CODE_USAGE_URL, commandCodePeriodStart, commandCodeSourceFor, crossedQuotaThresholds, DEEPSEEK_PUBLIC_BASE_URL, formatAccountBalance, formatFooterBalance, formatQuotaSnapshot, joinUrl, OPENAI_COMPAT_BALANCE_PATHS, OPENCODE_GO_USAGE_URL, OPENCODE_ZEN_BASE_URL, openCodeApiErrorMessage, openCodeSourceFor, parseCommandCodeQuota, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, quotaAlertText, quotaRefreshEverySteps, reasoningEffortsForDefault, SUPERGROK_BILLING_URL, tightestQuotaWindow, } from './quota.js';
|
|
65
|
-
import { appendSubagentLog, applyTurnEndToPlan, cardCategoryLabel, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planMarkdownFromArgs, planDockNote, planIsLive, planTitleFromMarkdown, promptInjectionSources, promptInjectionTitle, subagentHeaderText, todoItemKind, todoProgressLabel, TODO_STATUS_MARK, } from './plan.js';
|
|
63
|
+
import { contextPressureAlertText, contextPressureRingColor, contextPressureView, formatContextPressureRing, describeProviderRoute, fitFooterChips, fitFooterStatusLine, footerActivity, footerHealthChip, footerIdentityParts, footerStatsGroups, formatContextPressureChip, providerHasQuotaSurface, formatStatusReport, shortQuotaPlanName, formatTokens, parseContextPressure, promptPressureTokens, providerUsesLocalOAuth, shouldIdleAutoCompact, } from './footer.js';
|
|
64
|
+
import { COMMAND_CODE_CREDITS_URL, COMMAND_CODE_SUBSCRIPTIONS_URL, COMMAND_CODE_USAGE_URL, commandCodePeriodStart, commandCodeSourceFor, crossedQuotaThresholds, DEEPSEEK_PUBLIC_BASE_URL, formatAccountBalance, formatFooterBalance, formatQuotaSnapshot, joinUrl, OPENAI_COMPAT_BALANCE_PATHS, OPENCODE_GO_USAGE_URL, OPENCODE_ZEN_BASE_URL, openCodeApiErrorMessage, openCodeSourceFor, parseCommandCodeQuota, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, preferredQuotaWindow, quotaAlertText, quotaRefreshEverySteps, reasoningEffortsForDefault, SUPERGROK_BILLING_URL, tightestQuotaWindow, } from './quota.js';
|
|
65
|
+
import { appendSubagentLog, applyTurnEndToPlan, cardCategoryLabel, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planMarkdownFromArgs, planDockNote, planIsLive, planTitleFromMarkdown, planTurnLeftOpen, promptInjectionSources, promptInjectionTitle, subagentHeaderText, todoItemKind, todoProgressLabel, TODO_STATUS_MARK, } from './plan.js';
|
|
66
66
|
import { buildToolHeader, compactEditPath, compactFailureLines, compactFileStats, compactToolBursts, countDiffAddDel, countDiffLines, countOutputLines, diffMetaDiffs, diffStatToken, formatModelList, HIDDEN_TOOL_NAMES, parseExitStatus, planReviewOf, presentToolCall, READ_TOOL_NAMES, SHELL_TOOL_NAMES, toolBodyFitsWorkspace, toolBodyLines, toolTitle, wrappedToolBodyLineCount, } from './tool-present.js';
|
|
67
67
|
export { clipAnsiToWidth, cursorVisualPosition, displayWidth, foldInputView, hrefAtColumn, osc52Clipboard, osc8Enabled, paintedLinkHits, fmtElapsedCompact, padAnsiToWidth, padToWidth, renderMarkdownLines, repeatToWidth, shimmerText, truncateToWidth, visibleWidth, waitCardCopy, waitSummaryFromReasoning, wrapWaitDetails, } from './term-text.js';
|
|
68
68
|
export { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, ignoreFurtherHangupSignals, isEscapePrefix, findCursorPositionReply, isHangupErrno, linkQualityOf, linkSignalPips, paintIntervalForRtt, paintLinkLabel, parseCursorPositionReply, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, writeBootSplash, } from './paint.js';
|
|
69
|
-
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, formatContextPressureChip, formatContextPressureRing, formatContextPressureStatusLine, formatDuration, formatFooterQuota, formatQuotaBar, formatStatusReport, formatTokens, formatTokensPerSecond, parseContextPressure, promptPressureTokens, providerShortCode, providerUsesLocalOAuth, shouldIdleAutoCompact, subagentRouteLabel, } from './footer.js';
|
|
70
|
-
export { commandCodePeriodStart, commandCodeSourceFor, crossedQuotaThresholds, formatAccountBalance, formatFooterBalance, formatOpenCodeGoUsage, formatQuotaSnapshot, formatQuotaStatusLine, joinUrl, openCodeSourceFor, parseCommandCodeQuota, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, quotaAlertText, quotaRefreshEverySteps, quotaRefreshEveryTurns, remainingPercentFromUsed, tightestQuotaWindow, } from './quota.js';
|
|
69
|
+
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, formatContextPressureChip, formatContextPressureRing, formatContextPressureStatusLine, formatDuration, formatFooterQuota, formatQuotaBar, formatQuotaUnknown, formatStatusReport, providerHasQuotaSurface, quotaWindowTag, shortModelName, shortQuotaPlanName, formatTokens, formatTokensPerSecond, parseContextPressure, promptPressureTokens, providerShortCode, providerUsesLocalOAuth, shouldIdleAutoCompact, subagentRouteLabel, } from './footer.js';
|
|
70
|
+
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';
|
|
71
71
|
export { commandAcceptsAttachments, forEachSessionEvent, isAssistantStreamEvent, isTokenDeltaChunk, listPersistenceHeaders, inspectPersistenceSession, sessionEventType, sessionEvents, settingsNamespace, streamChunkOf, streamFirstTokenTime, streamFrameAttemptId, streamFrameOwner, } from './dsh-compat.js';
|
|
72
72
|
export { applyTurnEndToPlan, askSummary, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planDockNote, planIsLive, planTitleFromMarkdown, planTurnLeftOpen, promptInjectionSources, promptInjectionTitle, subagentHeaderText, todoProgressLabel, todoSummary, } from './plan.js';
|
|
73
73
|
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';
|
|
@@ -229,6 +229,12 @@ const DEFAULT_IDLE_EXIT_MS = 60 * 1000;
|
|
|
229
229
|
const CTRL_C_EXIT_WINDOW_MS = 2000;
|
|
230
230
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
231
231
|
const SUBAGENT_DEFAULT_EFFORT_LABEL = () => t('footer.effortDefault');
|
|
232
|
+
/**
|
|
233
|
+
* How often to ask again while the footer still has no quota reading. A quota
|
|
234
|
+
* API that was down at boot is usually up seconds later, and the widget shows
|
|
235
|
+
* `?%` until then; once a reading lands, the normal step/idle cadence takes over.
|
|
236
|
+
*/
|
|
237
|
+
const QUOTA_RETRY_MS = 15_000;
|
|
232
238
|
const RESERVED_BOTTOM_LINES = 3; // input line + stats line + status line
|
|
233
239
|
const IS_WINDOWS = process.platform === 'win32';
|
|
234
240
|
function dshHomeDir() {
|
|
@@ -680,6 +686,13 @@ export class SshTui {
|
|
|
680
686
|
quotaAlerted = new Set();
|
|
681
687
|
quotaStepsSinceRefresh = 0;
|
|
682
688
|
quotaRefreshInFlight = false;
|
|
689
|
+
/**
|
|
690
|
+
* While the first reading is missing, ask again on this cadence instead of
|
|
691
|
+
* waiting for the next step: a quota API that was down at boot is usually up a
|
|
692
|
+
* few seconds later, and until then the footer shows `?%`.
|
|
693
|
+
*/
|
|
694
|
+
quotaRetryMs = QUOTA_RETRY_MS;
|
|
695
|
+
quotaRetryTimer;
|
|
683
696
|
contextPressure;
|
|
684
697
|
contextAlertLevel;
|
|
685
698
|
idleCompactInFlight = false;
|
|
@@ -821,7 +834,8 @@ export class SshTui {
|
|
|
821
834
|
this.markDirty();
|
|
822
835
|
});
|
|
823
836
|
void this.refreshQuota({ reason: 'start', announce: false }).catch(() => {
|
|
824
|
-
// Start-up quota is silent; /usage and threshold alerts still report.
|
|
837
|
+
// Start-up quota is silent; /usage and threshold alerts still report. The
|
|
838
|
+
// retry keeps asking, so the footer's `?%` turns into a reading by itself.
|
|
825
839
|
});
|
|
826
840
|
void this.notifyPluginUpdate().catch(() => {
|
|
827
841
|
// Update check is best-effort and never blocks the TUI.
|
|
@@ -1355,6 +1369,7 @@ export class SshTui {
|
|
|
1355
1369
|
this.exiting = true;
|
|
1356
1370
|
this.clearDetachedIdleTimer();
|
|
1357
1371
|
this.clearIdleExitTimer();
|
|
1372
|
+
this.clearQuotaRetry();
|
|
1358
1373
|
const dialog = this.dialog;
|
|
1359
1374
|
const queued = this.dialogQueue.splice(0);
|
|
1360
1375
|
this.dialog = undefined;
|
|
@@ -2062,7 +2077,12 @@ export class SshTui {
|
|
|
2062
2077
|
existing.archived = false;
|
|
2063
2078
|
if (patch.todos !== undefined || patch.active !== undefined || patch.pending !== undefined) {
|
|
2064
2079
|
existing.turnLeftOpen = false;
|
|
2065
|
-
this
|
|
2080
|
+
// A patch that leaves items open keeps this episode's reminder spent;
|
|
2081
|
+
// only a list that is fully closed lets a later one ask again.
|
|
2082
|
+
if (!planTurnLeftOpen(existing)) {
|
|
2083
|
+
existing.nudged = false;
|
|
2084
|
+
this.planNudgePending = false;
|
|
2085
|
+
}
|
|
2066
2086
|
}
|
|
2067
2087
|
if (!planIsLive(existing)) {
|
|
2068
2088
|
existing.archived = true;
|
|
@@ -2109,6 +2129,12 @@ export class SshTui {
|
|
|
2109
2129
|
const plan = this.findLivePlanRow();
|
|
2110
2130
|
if (plan === undefined || plan.turnLeftOpen !== true)
|
|
2111
2131
|
return;
|
|
2132
|
+
// One reminder per open list. The answer to a reminder is a `todo_write`,
|
|
2133
|
+
// and a patch that leaves an item open used to re-arm this — so every turn
|
|
2134
|
+
// end produced another reminder and another model turn, forever.
|
|
2135
|
+
if (plan.nudged === true)
|
|
2136
|
+
return;
|
|
2137
|
+
plan.nudged = true;
|
|
2112
2138
|
this.planNudgePending = true;
|
|
2113
2139
|
const text = planCloseNudgeText(plan);
|
|
2114
2140
|
const queued = t('plan.nudgeQueued');
|
|
@@ -2124,7 +2150,14 @@ export class SshTui {
|
|
|
2124
2150
|
this.agent.followup(message);
|
|
2125
2151
|
}
|
|
2126
2152
|
catch (error) {
|
|
2153
|
+
// The reminder never reached the model: drop the queued notice that
|
|
2154
|
+
// announced it (it would otherwise read as "already asked", here and in a
|
|
2155
|
+
// resumed transcript) and let a later turn end try again.
|
|
2127
2156
|
this.planNudgePending = false;
|
|
2157
|
+
plan.nudged = false;
|
|
2158
|
+
const queuedAt = this.rows.findLastIndex(row => row.kind === 'system' && row.text === queued);
|
|
2159
|
+
if (queuedAt >= 0)
|
|
2160
|
+
this.rows.splice(queuedAt, 1);
|
|
2128
2161
|
this.pushRow({ kind: 'error', text: t('plan.nudgeFailed', { error: errorChain(error) }) });
|
|
2129
2162
|
}
|
|
2130
2163
|
}
|
|
@@ -3217,7 +3250,9 @@ export class SshTui {
|
|
|
3217
3250
|
const liveGoal = this.rows.findLast((row) => row.kind === 'goal');
|
|
3218
3251
|
const current = this.selectionRef?.current;
|
|
3219
3252
|
const provider = this.currentProviderId();
|
|
3220
|
-
|
|
3253
|
+
// The footer shows the finest window the provider reports (5-hour first),
|
|
3254
|
+
// not the tightest percent: that is the number a working session hits first.
|
|
3255
|
+
const quotaWindow = this.quotaSnapshot === undefined ? undefined : preferredQuotaWindow(this.quotaSnapshot);
|
|
3221
3256
|
const balanceText = this.balanceSnapshot !== undefined && this.balanceSnapshot.provider === provider
|
|
3222
3257
|
? formatFooterBalance(this.balanceSnapshot)
|
|
3223
3258
|
: undefined;
|
|
@@ -3253,8 +3288,15 @@ export class SshTui {
|
|
|
3253
3288
|
// looking at a wide terminal read that as "额度条没了". The strip keeps
|
|
3254
3289
|
// the loss order for the groups it does own.
|
|
3255
3290
|
...(quotaWindow === undefined || this.quotaSnapshot === undefined || this.quotaSnapshot.provider !== provider
|
|
3256
|
-
|
|
3257
|
-
|
|
3291
|
+
// No reading for this provider: show the empty bar with a `?` rather
|
|
3292
|
+
// than a number nobody measured. A balance-only provider (DeepSeek)
|
|
3293
|
+
// shows nothing here; its balance line is the reading.
|
|
3294
|
+
? (this.hasQuotaSurface(provider) ? { quotaUnknown: true } : {})
|
|
3295
|
+
: {
|
|
3296
|
+
quotaCode: shortQuotaPlanName(this.quotaSnapshot),
|
|
3297
|
+
quotaPercent: quotaWindow.remainingPercent,
|
|
3298
|
+
quotaPeriod: quotaWindow.period,
|
|
3299
|
+
}),
|
|
3258
3300
|
...(this.contextPressure === undefined
|
|
3259
3301
|
? {}
|
|
3260
3302
|
: { contextChip: formatContextPressureChip(this.contextPressure, false) }),
|
|
@@ -4103,6 +4145,13 @@ export class SshTui {
|
|
|
4103
4145
|
const alreadyShown = last?.kind === 'system' && last.text === summary;
|
|
4104
4146
|
if (summary !== '' && !alreadyShown)
|
|
4105
4147
|
this.pushRow({ kind: 'system', text: summary });
|
|
4148
|
+
// Replaying a resumed session: the reminder in the log belongs to
|
|
4149
|
+
// the open list that is being rebuilt, so mark it as spent.
|
|
4150
|
+
if (summary === t('plan.nudgeQueued')) {
|
|
4151
|
+
const resumed = this.findLivePlanRow();
|
|
4152
|
+
if (resumed !== undefined)
|
|
4153
|
+
resumed.nudged = true;
|
|
4154
|
+
}
|
|
4106
4155
|
if (!this.replaying)
|
|
4107
4156
|
this.beginWait();
|
|
4108
4157
|
}
|
|
@@ -6646,16 +6695,45 @@ export class SshTui {
|
|
|
6646
6695
|
}
|
|
6647
6696
|
this.markDirty();
|
|
6648
6697
|
}
|
|
6698
|
+
/** Stop the first-reading retry; the normal cadence owns the refresh now. */
|
|
6699
|
+
clearQuotaRetry() {
|
|
6700
|
+
if (this.quotaRetryTimer !== undefined)
|
|
6701
|
+
clearTimeout(this.quotaRetryTimer);
|
|
6702
|
+
this.quotaRetryTimer = undefined;
|
|
6703
|
+
}
|
|
6704
|
+
/** Ask again soon, but only while the footer still has nothing to show. */
|
|
6705
|
+
armQuotaRetry(provider) {
|
|
6706
|
+
if (this.disposed || this.quotaRetryTimer !== undefined)
|
|
6707
|
+
return;
|
|
6708
|
+
if (!this.hasQuotaSurface(provider))
|
|
6709
|
+
return;
|
|
6710
|
+
if (this.quotaSnapshot !== undefined && this.quotaSnapshot.provider === provider)
|
|
6711
|
+
return;
|
|
6712
|
+
this.quotaRetryTimer = setTimeout(() => {
|
|
6713
|
+
this.quotaRetryTimer = undefined;
|
|
6714
|
+
void this.refreshQuota({ reason: 'start', announce: false }).catch(() => { });
|
|
6715
|
+
}, this.quotaRetryMs);
|
|
6716
|
+
}
|
|
6717
|
+
/**
|
|
6718
|
+
* Whether this provider has a quota surface at all. `false` means the footer
|
|
6719
|
+
* shows no quota widget — a balance line covers DeepSeek, and a provider with
|
|
6720
|
+
* neither gets nothing.
|
|
6721
|
+
*/
|
|
6722
|
+
hasQuotaSurface(provider) {
|
|
6723
|
+
const llmPiAi = this.ctx.get('settings')?.get(settingsNamespace('llm-pi-ai'));
|
|
6724
|
+
return providerHasQuotaSurface(provider, llmPiAi);
|
|
6725
|
+
}
|
|
6649
6726
|
async refreshQuota(options) {
|
|
6650
6727
|
if (this.quotaRefreshInFlight && options.reason !== 'command')
|
|
6651
6728
|
return this.quotaSnapshot;
|
|
6652
6729
|
this.quotaRefreshInFlight = true;
|
|
6730
|
+
const provider = this.currentProviderId();
|
|
6653
6731
|
try {
|
|
6654
|
-
const provider = this.currentProviderId();
|
|
6655
6732
|
const snapshot = await this.fetchQuotaSnapshot(provider);
|
|
6656
6733
|
if (snapshot !== undefined) {
|
|
6657
6734
|
this.balanceSnapshot = undefined;
|
|
6658
6735
|
this.applyQuotaSnapshot(snapshot, options.announce);
|
|
6736
|
+
this.clearQuotaRetry();
|
|
6659
6737
|
return snapshot;
|
|
6660
6738
|
}
|
|
6661
6739
|
try {
|
|
@@ -6689,6 +6767,11 @@ export class SshTui {
|
|
|
6689
6767
|
}
|
|
6690
6768
|
finally {
|
|
6691
6769
|
this.quotaRefreshInFlight = false;
|
|
6770
|
+
// One place covers every outcome: a successful reading clears the retry
|
|
6771
|
+
// (and this check then sees a snapshot for the provider), while a throw,
|
|
6772
|
+
// an empty reply or another provider's snapshot keeps it asking.
|
|
6773
|
+
if (this.quotaSnapshot === undefined || this.quotaSnapshot.provider !== provider)
|
|
6774
|
+
this.armQuotaRetry(provider);
|
|
6692
6775
|
}
|
|
6693
6776
|
}
|
|
6694
6777
|
async fetchAccountBalance(provider) {
|