dsh-ssh-tui 0.5.6 → 0.5.8
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 +24 -0
- package/README.md +37 -0
- package/lib/display-sock.js +237 -60
- package/lib/display-sock.js.map +1 -1
- package/lib/dsh-compat.js +138 -5
- package/lib/dsh-compat.js.map +1 -1
- package/lib/footer.js +27 -2
- package/lib/footer.js.map +1 -1
- package/lib/index.js +8 -4
- package/lib/index.js.map +1 -1
- package/lib/paint.js +21 -2
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +45 -18
- package/lib/picker.js.map +1 -1
- package/lib/session-index.js +2 -2
- package/lib/session-index.js.map +1 -1
- package/lib/session-lock.js +177 -24
- package/lib/session-lock.js.map +1 -1
- package/lib/tui.js +83 -24
- package/lib/tui.js.map +1 -1
- package/lib/types/display-sock.d.ts +59 -5
- package/lib/types/dsh-compat.d.ts +60 -2
- package/lib/types/footer.d.ts +11 -1
- package/lib/types/paint.d.ts +13 -0
- package/lib/types/picker.d.ts +6 -1
- package/lib/types/session-lock.d.ts +42 -3
- package/lib/types/tui.d.ts +15 -5
- package/package.json +50 -48
package/lib/tui.js
CHANGED
|
@@ -25,12 +25,12 @@ import { StringDecoder } from 'node:string_decoder';
|
|
|
25
25
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
26
26
|
import { createUserMessage, errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
27
27
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
28
|
-
import { commandAcceptsAttachments,
|
|
28
|
+
import { commandAcceptsAttachments, forEachSessionEventAsync, isAssistantStreamEvent, isTokenDeltaChunk, listenHostEvent, REPLAY_YIELD_EVERY, sessionEventType, sessionEvents, settingsNamespace, streamChunkOf, streamFirstTokenTime, streamFrameAttemptId, streamFrameOwner, } from './dsh-compat.js';
|
|
29
29
|
import { classifyApprovalDetailed, commandForApprovalRequest, isApprovalStatusArg, parseAutoApprovalMode } from './auto-approval.js';
|
|
30
30
|
import { buildReviewUserMessage, parseReviewOutput, reviewSystemPrompt } from './approval-reviewer.js';
|
|
31
31
|
import { loadProviderCatalog, mergeProviderEntries } from './provider-catalog.js';
|
|
32
32
|
import { formatFooterCwd, formatSessionTime, listResumableSessions } from './session-list.js';
|
|
33
|
-
import { detachFromSshSession, DisplayHost, sessionSockPath } from './display-sock.js';
|
|
33
|
+
import { detachFromSshSession, DisplayHost, resolveDshHome, sessionSockPath } from './display-sock.js';
|
|
34
34
|
import { applySavedLocale, getLocale, localeDisplayName, localeFromTag, setLocale, t, UI_LOCALE_NAMESPACE, } from './i18n/index.js';
|
|
35
35
|
import { defaultReasoningEffort } from './reasoning.js';
|
|
36
36
|
import { checkForPluginUpdate, installPluginLatest } from './update-check.js';
|
|
@@ -47,10 +47,10 @@ import { crossedQuotaThresholds, DEEPSEEK_PUBLIC_BASE_URL, formatAccountBalance,
|
|
|
47
47
|
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';
|
|
48
48
|
import { buildToolHeader, canMergeToolCall, compactEditPath, compactToolBursts, countDiffAddDel, countDiffLines, countOutputLines, diffMetaDiffs, diffStatToken, formatModelList, HIDDEN_TOOL_NAMES, parseExitStatus, planReviewOf, presentToolCall, READ_TOOL_NAMES, SHELL_TOOL_NAMES, toolBodyFitsWorkspace, toolBodyLines, toolTitle, TOOL_FLIP_MS, wrappedToolBodyLineCount, } from './tool-present.js';
|
|
49
49
|
export { clipAnsiToWidth, cursorVisualPosition, displayWidth, foldInputView, hrefAtColumn, osc52Clipboard, osc8Enabled, paintedLinkHits, fmtElapsedCompact, padAnsiToWidth, padToWidth, renderMarkdownLines, repeatToWidth, shimmerText, truncateToWidth, visibleWidth, waitCardCopy, waitSummaryFromReasoning, wrapWaitDetails, } from './term-text.js';
|
|
50
|
-
export { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, ignoreFurtherHangupSignals, isEscapePrefix, isHangupErrno, linkQualityOf, linkSignalPips, paintIntervalForRtt, paintLinkLabel, parseCursorPositionReply, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, writeBootSplash, } from './paint.js';
|
|
51
|
-
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, } from './footer.js';
|
|
50
|
+
export { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, ignoreFurtherHangupSignals, isEscapePrefix, findCursorPositionReply, isHangupErrno, linkQualityOf, linkSignalPips, paintIntervalForRtt, paintLinkLabel, parseCursorPositionReply, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, writeBootSplash, } from './paint.js';
|
|
51
|
+
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';
|
|
52
52
|
export { crossedQuotaThresholds, formatAccountBalance, formatFooterBalance, formatOpenCodeGoUsage, formatQuotaSnapshot, formatQuotaStatusLine, joinUrl, openCodeSourceFor, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, quotaAlertText, quotaRefreshEverySteps, quotaRefreshEveryTurns, remainingPercentFromUsed, tightestQuotaWindow, } from './quota.js';
|
|
53
|
-
export { commandAcceptsAttachments, forEachSessionEvent, isAssistantStreamEvent, listPersistenceHeaders, inspectPersistenceSession, sessionEventType, sessionEvents, settingsNamespace, streamChunkOf, } from './dsh-compat.js';
|
|
53
|
+
export { commandAcceptsAttachments, forEachSessionEvent, isAssistantStreamEvent, isTokenDeltaChunk, listPersistenceHeaders, inspectPersistenceSession, sessionEventType, sessionEvents, settingsNamespace, streamChunkOf, streamFirstTokenTime, streamFrameAttemptId, streamFrameOwner, } from './dsh-compat.js';
|
|
54
54
|
export { applyTurnEndToPlan, askSummary, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planDockNote, planIsLive, planTitleFromMarkdown, planTurnLeftOpen, promptInjectionSources, promptInjectionTitle, subagentHeaderText, todoProgressLabel, todoSummary, } from './plan.js';
|
|
55
55
|
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';
|
|
56
56
|
function discoverProviderModels(llm, request, signal) {
|
|
@@ -146,7 +146,7 @@ const RESERVED_BOTTOM_LINES = 3; // input line + stats line + status line
|
|
|
146
146
|
const MAX_TRANSCRIPT_ROWS = 5000;
|
|
147
147
|
const IS_WINDOWS = process.platform === 'win32';
|
|
148
148
|
function dshHomeDir() {
|
|
149
|
-
return
|
|
149
|
+
return resolveDshHome();
|
|
150
150
|
}
|
|
151
151
|
function displayDshPath(file) {
|
|
152
152
|
const home = dshHomeDir();
|
|
@@ -446,6 +446,10 @@ export class SshTui {
|
|
|
446
446
|
usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
|
447
447
|
};
|
|
448
448
|
openStepStats;
|
|
449
|
+
/** Attempt whose live `start` frame opened the current token stream. */
|
|
450
|
+
liveStreamOwner;
|
|
451
|
+
/** Live events parked while the (yielding) history replay holds the floor. */
|
|
452
|
+
replayQueue;
|
|
449
453
|
pendingToolTimes = new Map();
|
|
450
454
|
usageByStep = new Map();
|
|
451
455
|
lastStatsTurn = null;
|
|
@@ -871,16 +875,29 @@ export class SshTui {
|
|
|
871
875
|
this.paintIntervalMs = resolvePaintIntervalMs(undefined, {}, { ssh: true, rttMs: rtt });
|
|
872
876
|
this.markDirty();
|
|
873
877
|
}
|
|
874
|
-
/**
|
|
875
|
-
|
|
878
|
+
/**
|
|
879
|
+
* Replay the durable session log so a resumed session renders its history.
|
|
880
|
+
* Chunked on purpose: a synchronous walk of a long log froze the TUI on the
|
|
881
|
+
* pre-replay frame, so the relay's RTT frame could not be applied and the
|
|
882
|
+
* footer sat on `SSH ○○○○` for the whole load.
|
|
883
|
+
*/
|
|
884
|
+
async replayHistory() {
|
|
885
|
+
const parked = [];
|
|
886
|
+
this.replayQueue = parked;
|
|
876
887
|
this.replaying = true;
|
|
877
888
|
try {
|
|
878
|
-
|
|
879
|
-
this.
|
|
880
|
-
});
|
|
889
|
+
await forEachSessionEventAsync(this.agent.session, (event) => {
|
|
890
|
+
this.applySessionEvent(this.agent.session, event);
|
|
891
|
+
}, REPLAY_YIELD_EVERY, () => this.disposed);
|
|
881
892
|
}
|
|
882
893
|
finally {
|
|
883
894
|
this.replaying = false;
|
|
895
|
+
this.replayQueue = undefined;
|
|
896
|
+
}
|
|
897
|
+
for (const item of parked) {
|
|
898
|
+
if (this.disposed)
|
|
899
|
+
return;
|
|
900
|
+
this.applySessionEvent(item.session, item.event);
|
|
884
901
|
}
|
|
885
902
|
this.streaming = undefined;
|
|
886
903
|
this.streamingReasoning = undefined;
|
|
@@ -2589,7 +2606,8 @@ export class SshTui {
|
|
|
2589
2606
|
provider,
|
|
2590
2607
|
parentModel,
|
|
2591
2608
|
subModel: sub.model,
|
|
2592
|
-
|
|
2609
|
+
...(sub.provider === undefined ? {} : { subProvider: sub.provider }),
|
|
2610
|
+
...(sub.reasoningEffort === undefined ? {} : { subEffort: String(sub.reasoningEffort) }),
|
|
2593
2611
|
...(quotaWindow === undefined || this.quotaSnapshot === undefined || this.quotaSnapshot.provider !== provider
|
|
2594
2612
|
? {}
|
|
2595
2613
|
: { quotaCode: this.quotaSnapshot.plan, quotaPercent: quotaWindow.remainingPercent }),
|
|
@@ -2879,20 +2897,20 @@ export class SshTui {
|
|
|
2879
2897
|
* row. 0.1.2 hosts append `assistant/chunk`; 0.1.5 emits the same chunk
|
|
2880
2898
|
* on `agent/assistant-stream` and never writes it to the log.
|
|
2881
2899
|
*/
|
|
2882
|
-
applyStreamChunk(streamed) {
|
|
2900
|
+
applyStreamChunk(streamed, statsOnly = false) {
|
|
2883
2901
|
const { chunk } = streamed;
|
|
2884
2902
|
const open = this.openStepStats;
|
|
2885
2903
|
if (open !== null && open !== undefined
|
|
2886
2904
|
&& open.turn === streamed.turn && open.step === streamed.step) {
|
|
2887
|
-
if (open.firstTokenTime === null
|
|
2888
|
-
&& chunk.type === 'text-delta'
|
|
2889
|
-
&& chunk.text !== '') {
|
|
2905
|
+
if (open.firstTokenTime === null && isTokenDeltaChunk(chunk)) {
|
|
2890
2906
|
this.openStepStats = { ...open, firstTokenTime: streamed.time };
|
|
2891
2907
|
}
|
|
2892
2908
|
}
|
|
2893
|
-
if (chunk.type === 'usage' && chunk.usage !== undefined) {
|
|
2909
|
+
if (chunk.type === 'usage' && chunk.usage !== undefined && streamed.stepKnown) {
|
|
2894
2910
|
this.recordUsage(streamed.turn, streamed.step, chunk.usage);
|
|
2895
2911
|
}
|
|
2912
|
+
if (statsOnly)
|
|
2913
|
+
return;
|
|
2896
2914
|
if (chunk.type === 'text-delta') {
|
|
2897
2915
|
this.streaming ??= { text: '', reasoning: '' };
|
|
2898
2916
|
this.streaming.text += chunk.text ?? '';
|
|
@@ -2924,7 +2942,23 @@ export class SshTui {
|
|
|
2924
2942
|
return;
|
|
2925
2943
|
this.lastActivity = Date.now();
|
|
2926
2944
|
this.refreshContextPressure();
|
|
2927
|
-
const
|
|
2945
|
+
const owner = streamFrameOwner(payload.frame);
|
|
2946
|
+
if (owner !== undefined) {
|
|
2947
|
+
// Live chunk frames carry no turn/step: remember the attempt's step here
|
|
2948
|
+
// or every chunk is attributed to step 0 and the stats never match.
|
|
2949
|
+
this.liveStreamOwner = owner;
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
if (payload.frame?.type === 'end') {
|
|
2953
|
+
this.liveStreamOwner = undefined;
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
const attemptId = streamFrameAttemptId(payload.frame);
|
|
2957
|
+
const current = this.liveStreamOwner;
|
|
2958
|
+
const fallback = current !== undefined && (attemptId === undefined || attemptId === current.attemptId)
|
|
2959
|
+
? current
|
|
2960
|
+
: this.openStepStats;
|
|
2961
|
+
const streamed = streamChunkOf(payload.frame, fallback);
|
|
2928
2962
|
if (streamed !== undefined)
|
|
2929
2963
|
this.applyStreamChunk(streamed);
|
|
2930
2964
|
};
|
|
@@ -2953,14 +2987,30 @@ export class SshTui {
|
|
|
2953
2987
|
};
|
|
2954
2988
|
};
|
|
2955
2989
|
handleSessionEvent = (session, event) => {
|
|
2990
|
+
if (this.replayQueue !== undefined) {
|
|
2991
|
+
// The replay yields between chunks, so live events can arrive mid-load.
|
|
2992
|
+
// Folding them now would interleave newer events under older history;
|
|
2993
|
+
// park them and drain in arrival order once the walk is done.
|
|
2994
|
+
this.replayQueue.push({ session, event });
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
this.applySessionEvent(session, event);
|
|
2998
|
+
};
|
|
2999
|
+
applySessionEvent(session, event) {
|
|
2956
3000
|
if (session.id !== this.agent.id) {
|
|
2957
3001
|
if (this.subagentSessions.has(session.id))
|
|
2958
3002
|
this.handleSubagentSessionEvent(session.id, event);
|
|
2959
3003
|
return;
|
|
2960
3004
|
}
|
|
2961
3005
|
this.lastActivity = Date.now();
|
|
2962
|
-
if (this.replaying && isAssistantStreamEvent(event))
|
|
3006
|
+
if (this.replaying && isAssistantStreamEvent(event)) {
|
|
3007
|
+
// Replay skips the in-progress row, but durable chunks still carry the
|
|
3008
|
+
// timing the footer needs: a resumed session must keep TTFT and tok/s.
|
|
3009
|
+
const streamed = streamChunkOf(event);
|
|
3010
|
+
if (streamed !== undefined)
|
|
3011
|
+
this.applyStreamChunk(streamed, true);
|
|
2963
3012
|
return;
|
|
3013
|
+
}
|
|
2964
3014
|
if (!this.replaying)
|
|
2965
3015
|
this.refreshContextPressure();
|
|
2966
3016
|
const eventType = sessionEventType(event);
|
|
@@ -3021,12 +3071,19 @@ export class SshTui {
|
|
|
3021
3071
|
const open = this.openStepStats;
|
|
3022
3072
|
if (open !== undefined && open.turn === event.data.turn && open.step === event.data.step) {
|
|
3023
3073
|
this.stats.llmMs += Math.max(0, event.time - open.startTime);
|
|
3024
|
-
|
|
3025
|
-
|
|
3074
|
+
// The settlement's own packed stream is authoritative: a retried
|
|
3075
|
+
// step keeps the failed attempt's first token in the live latch, and
|
|
3076
|
+
// spanning both attempts reported a rate ~20x off. 0.1.2 has no
|
|
3077
|
+
// `stream` and still relies on the replayed `assistant/chunk` events
|
|
3078
|
+
// (or the live latch) instead.
|
|
3079
|
+
const firstTokenTime = streamFirstTokenTime(event.data.stream)
|
|
3080
|
+
?? open.firstTokenTime;
|
|
3081
|
+
if (firstTokenTime !== null && firstTokenTime !== undefined && Number.isFinite(firstTokenTime)) {
|
|
3082
|
+
this.stats.ttftMs += Math.max(0, firstTokenTime - open.startTime);
|
|
3026
3083
|
this.stats.ttftSteps += 1;
|
|
3027
3084
|
const outputTokens = event.data.usage?.outputTokens;
|
|
3028
3085
|
if (typeof outputTokens === 'number' && Number.isFinite(outputTokens) && outputTokens >= 0) {
|
|
3029
|
-
this.stats.decodeMs += Math.max(0, event.time -
|
|
3086
|
+
this.stats.decodeMs += Math.max(0, event.time - firstTokenTime);
|
|
3030
3087
|
this.stats.decodeTokens += outputTokens;
|
|
3031
3088
|
}
|
|
3032
3089
|
}
|
|
@@ -3256,7 +3313,7 @@ export class SshTui {
|
|
|
3256
3313
|
this.handleExtensionEvent(event);
|
|
3257
3314
|
break;
|
|
3258
3315
|
}
|
|
3259
|
-
}
|
|
3316
|
+
}
|
|
3260
3317
|
handleStatus = ({ agent, status }) => {
|
|
3261
3318
|
if (agent !== this.agent)
|
|
3262
3319
|
return;
|
|
@@ -7150,7 +7207,9 @@ export function mountTui(ctx, config) {
|
|
|
7150
7207
|
stopWaiting();
|
|
7151
7208
|
controller = new SshTui(ctx, agent, config);
|
|
7152
7209
|
controller.start();
|
|
7153
|
-
|
|
7210
|
+
// Not awaited: the first frames paint while the (chunked) replay fills the
|
|
7211
|
+
// transcript, and relay RTT/resize frames keep flowing during the load.
|
|
7212
|
+
void controller.replayHistory();
|
|
7154
7213
|
};
|
|
7155
7214
|
const fail = (failedSessionId, error) => {
|
|
7156
7215
|
if (settled || failedSessionId !== sessionId)
|