dsh-ssh-tui 0.5.4 → 0.5.6
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 +10 -5
- package/README.md +6 -5
- package/lib/copy-text.js +62 -0
- package/lib/copy-text.js.map +1 -0
- package/lib/display-sock.js +9 -4
- package/lib/display-sock.js.map +1 -1
- package/lib/dsh-compat.js +142 -1
- package/lib/dsh-compat.js.map +1 -1
- package/lib/i18n/en.js +14 -1
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/zh.js +14 -1
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +7 -5
- package/lib/index.js.map +1 -1
- package/lib/paint.js +39 -4
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +51 -15
- package/lib/picker.js.map +1 -1
- package/lib/session-index.js +80 -0
- package/lib/session-index.js.map +1 -0
- package/lib/session-list.js +197 -72
- package/lib/session-list.js.map +1 -1
- package/lib/term-text.js +219 -53
- package/lib/term-text.js.map +1 -1
- package/lib/tui.js +201 -65
- package/lib/tui.js.map +1 -1
- package/lib/types/copy-text.d.ts +9 -0
- package/lib/types/display-sock.d.ts +3 -1
- package/lib/types/dsh-compat.d.ts +78 -6
- package/lib/types/paint.d.ts +2 -0
- package/lib/types/picker.d.ts +2 -0
- package/lib/types/session-index.d.ts +32 -0
- package/lib/types/session-list.d.ts +22 -2
- package/lib/types/term-text.d.ts +27 -3
- package/lib/types/tui.d.ts +41 -4
- package/package.json +20 -18
package/lib/tui.js
CHANGED
|
@@ -25,7 +25,7 @@ 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 { sessionEvents, settingsNamespace } from './dsh-compat.js';
|
|
28
|
+
import { commandAcceptsAttachments, forEachSessionEvent, isAssistantStreamEvent, listenHostEvent, sessionEventType, sessionEvents, settingsNamespace, streamChunkOf, } 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';
|
|
@@ -38,17 +38,19 @@ import { ROUTE_MEMORY_NAMESPACE, parseRouteMemory, rememberedRouteFor, upsertRem
|
|
|
38
38
|
import { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-default-model';
|
|
39
39
|
import { DEFAULT_SUBAGENT_MODEL, SUBAGENT_SETTINGS_NAMESPACE, defaultSubagentModelForProvider, subagentModelMatchesProvider, subagentSettingsValue, } from './subagent-model.js';
|
|
40
40
|
import { resolveFreshSuperGrokToken } from './supergrok-token.js';
|
|
41
|
+
import { copyTextFromTranscript } from './copy-text.js';
|
|
41
42
|
import { UserQuestionError, } from '@deepseek-ai/dsh-user-questions';
|
|
42
|
-
import { clipAnsiToWidth, cursorVisualPosition, displayWidth, foldInputView, fmtElapsedCompact, lastCodePoints, padToWidth, sliceCodePoints, paintSegmentedLine, renderMarkdownLines, repeatToWidth, sanitizeTerminalText, shimmerText, truncate, truncateToWidth, waitCardCopy, wrap, wrapSegmented, wrapWaitDetails, } from './term-text.js';
|
|
43
|
+
import { clipAnsiToWidth, hrefAtColumn, osc52Clipboard, paintedLinkHits, cursorVisualPosition, displayWidth, foldInputView, fmtElapsedCompact, lastCodePoints, padToWidth, sliceCodePoints, paintSegmentedLine, renderMarkdownLines, repeatToWidth, sanitizeTerminalText, shimmerText, truncate, truncateToWidth, waitCardCopy, wrap, wrapSegmented, wrapWaitDetails, } from './term-text.js';
|
|
43
44
|
import { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, HANGUP_CANCEL_TIMEOUT_MS, ignoreFurtherHangupSignals, isEscapePrefix, isHangupErrno, parseCursorPositionReply, PICKER_WINDOW, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, } from './paint.js';
|
|
44
45
|
import { contextPressureAlertText, contextPressureRingColor, contextPressureView, formatContextPressureRing, describeProviderRoute, fitFooterStatsLine, fitFooterStatusLine, footerActivity, footerIdentityParts, footerStatsGroups, formatContextPressureChip, formatStatusReport, formatTokens, parseContextPressure, promptPressureTokens, providerUsesLocalOAuth, shouldIdleAutoCompact, } from './footer.js';
|
|
45
46
|
import { crossedQuotaThresholds, DEEPSEEK_PUBLIC_BASE_URL, formatAccountBalance, formatFooterBalance, formatQuotaSnapshot, joinUrl, OPENAI_COMPAT_BALANCE_PATHS, OPENCODE_GO_USAGE_URL, OPENCODE_ZEN_BASE_URL, openCodeApiErrorMessage, openCodeSourceFor, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, quotaAlertText, quotaRefreshEverySteps, reasoningEffortsForDefault, SUPERGROK_BILLING_URL, tightestQuotaWindow, } from './quota.js';
|
|
46
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';
|
|
47
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';
|
|
48
|
-
export { clipAnsiToWidth, displayWidth, foldInputView, fmtElapsedCompact, padAnsiToWidth, padToWidth, renderMarkdownLines, repeatToWidth, shimmerText, truncateToWidth, visibleWidth, waitCardCopy, waitSummaryFromReasoning, wrapWaitDetails, } from './term-text.js';
|
|
49
|
-
export { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, ignoreFurtherHangupSignals, isEscapePrefix, isHangupErrno, linkQualityOf, linkSignalPips, paintIntervalForRtt, paintLinkLabel, parseCursorPositionReply, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, } from './paint.js';
|
|
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';
|
|
50
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';
|
|
51
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';
|
|
52
54
|
export { applyTurnEndToPlan, askSummary, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planDockNote, planIsLive, planTitleFromMarkdown, planTurnLeftOpen, promptInjectionSources, promptInjectionTitle, subagentHeaderText, todoProgressLabel, todoSummary, } from './plan.js';
|
|
53
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';
|
|
54
56
|
function discoverProviderModels(llm, request, signal) {
|
|
@@ -291,6 +293,7 @@ const LOCAL_COMMANDS = [
|
|
|
291
293
|
{ name: 'resume', key: 'cmd.resume' },
|
|
292
294
|
{ name: 'setup', key: 'cmd.setup' },
|
|
293
295
|
{ name: 'find', key: 'cmd.find' },
|
|
296
|
+
{ name: 'copy', key: 'cmd.copy' },
|
|
294
297
|
{ name: 'language', key: 'cmd.language' },
|
|
295
298
|
{ name: 'lang', key: 'cmd.language', aliasOf: 'language' },
|
|
296
299
|
{ name: 'dialog-test', key: 'cmd.dialog-test' },
|
|
@@ -448,6 +451,16 @@ export class SshTui {
|
|
|
448
451
|
lastStatsTurn = null;
|
|
449
452
|
scrollOffset = 0;
|
|
450
453
|
clickableRows = new Map();
|
|
454
|
+
paintedLinkHitsByRow = new Map();
|
|
455
|
+
copyYank = '';
|
|
456
|
+
/** Last OSC-52 payload (tests; empty when nothing has been copied). */
|
|
457
|
+
get lastCopiedText() {
|
|
458
|
+
return this.copyYank;
|
|
459
|
+
}
|
|
460
|
+
/** Screen-row → OSC 8 hits from the last paint (tests). */
|
|
461
|
+
get linkHitsByRow() {
|
|
462
|
+
return this.paintedLinkHitsByRow;
|
|
463
|
+
}
|
|
451
464
|
streamingReasoning;
|
|
452
465
|
escapeBuffer = '';
|
|
453
466
|
escapeTimer;
|
|
@@ -455,9 +468,13 @@ export class SshTui {
|
|
|
455
468
|
waitStartedAt;
|
|
456
469
|
completionSignaled = false;
|
|
457
470
|
replaying = false;
|
|
471
|
+
/** Display-line budget for the first paint after resume; 0 = full transcript. */
|
|
472
|
+
paintTailBudget = 0;
|
|
458
473
|
completedAt = 0;
|
|
459
474
|
lastTitleUpdateAt = 0;
|
|
460
475
|
lastPaintRows = [];
|
|
476
|
+
lastPaintCursorColumn = 1;
|
|
477
|
+
lastPaintCursorRow = 1;
|
|
461
478
|
lastChromeKey = '';
|
|
462
479
|
lastPaintWidth = 0;
|
|
463
480
|
lastPaintHeight = 0;
|
|
@@ -566,7 +583,7 @@ export class SshTui {
|
|
|
566
583
|
this.bootBackgroundTasks();
|
|
567
584
|
}
|
|
568
585
|
bindAgentEvents() {
|
|
569
|
-
this.disposers.push(this.ctx.on('session/event', this.handleSessionEvent), 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));
|
|
586
|
+
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));
|
|
570
587
|
const questions = this.ctx.get('userQuestions');
|
|
571
588
|
if (questions !== undefined) {
|
|
572
589
|
this.userQuestionDisposer = installUserQuestionAnswerer(this.ctx, questions, this.handleUserQuestions);
|
|
@@ -858,9 +875,9 @@ export class SshTui {
|
|
|
858
875
|
replayHistory() {
|
|
859
876
|
this.replaying = true;
|
|
860
877
|
try {
|
|
861
|
-
|
|
878
|
+
forEachSessionEvent(this.agent.session, (event) => {
|
|
862
879
|
this.handleSessionEvent(this.agent.session, event);
|
|
863
|
-
}
|
|
880
|
+
});
|
|
864
881
|
}
|
|
865
882
|
finally {
|
|
866
883
|
this.replaying = false;
|
|
@@ -870,6 +887,7 @@ export class SshTui {
|
|
|
870
887
|
this.thinkingStartedAt = undefined;
|
|
871
888
|
this.status = this.agent.status === 'running' ? 'running' : 'idle';
|
|
872
889
|
this.refreshContextPressure({ compact: false });
|
|
890
|
+
this.paintTailBudget = Math.max(24, this.screenRows() * 3);
|
|
873
891
|
this.dirty = true;
|
|
874
892
|
}
|
|
875
893
|
/** Show the first-launch provider/API-key onboarding when nothing is configured. */
|
|
@@ -1110,6 +1128,19 @@ export class SshTui {
|
|
|
1110
1128
|
return true;
|
|
1111
1129
|
return false;
|
|
1112
1130
|
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Display socket closed. Replacing a leftover Display with a new HELLO,
|
|
1133
|
+
* or closing a socket after we already detached, is not an SSH hangup.
|
|
1134
|
+
*/
|
|
1135
|
+
handleDisplayDetach(info) {
|
|
1136
|
+
if (this.disposed || this.hangingUp)
|
|
1137
|
+
return;
|
|
1138
|
+
if (info?.replaced === true)
|
|
1139
|
+
return;
|
|
1140
|
+
if (this.displayDetached)
|
|
1141
|
+
return;
|
|
1142
|
+
void this.handleHangup();
|
|
1143
|
+
}
|
|
1113
1144
|
/**
|
|
1114
1145
|
* SSH / TTY hangup: drop the local display, flush, and either keep the Host
|
|
1115
1146
|
* (busy: thinking / reply / tools / subagents) or exit (idle).
|
|
@@ -1209,6 +1240,14 @@ export class SshTui {
|
|
|
1209
1240
|
process.stdout.rows = previousRows;
|
|
1210
1241
|
}
|
|
1211
1242
|
}
|
|
1243
|
+
/** Last CSI cursor column written by {@link paint} (1-based). Tests only. */
|
|
1244
|
+
lastPaintedCursorColumn() {
|
|
1245
|
+
return this.lastPaintCursorColumn;
|
|
1246
|
+
}
|
|
1247
|
+
/** Last CSI cursor row written by {@link paint} (1-based). Tests only. */
|
|
1248
|
+
lastPaintedCursorRow() {
|
|
1249
|
+
return this.lastPaintCursorRow;
|
|
1250
|
+
}
|
|
1212
1251
|
screenColumns() {
|
|
1213
1252
|
if (this.headlessDisplay || this.displayDetached) {
|
|
1214
1253
|
return this.relayColumns ?? process.stdout.columns ?? 80;
|
|
@@ -1268,10 +1307,8 @@ export class SshTui {
|
|
|
1268
1307
|
onRtt: (rttMs) => {
|
|
1269
1308
|
this.applyProbedRtt(rttMs);
|
|
1270
1309
|
},
|
|
1271
|
-
onDetach: () => {
|
|
1272
|
-
|
|
1273
|
-
return;
|
|
1274
|
-
void this.handleHangup();
|
|
1310
|
+
onDetach: (info) => {
|
|
1311
|
+
this.handleDisplayDetach(info);
|
|
1275
1312
|
},
|
|
1276
1313
|
onAttach: () => {
|
|
1277
1314
|
if (this.disposed)
|
|
@@ -1684,6 +1721,8 @@ export class SshTui {
|
|
|
1684
1721
|
cursorRow: height,
|
|
1685
1722
|
cursorColumn: 1,
|
|
1686
1723
|
}));
|
|
1724
|
+
this.lastPaintCursorRow = height;
|
|
1725
|
+
this.lastPaintCursorColumn = 1;
|
|
1687
1726
|
this.lastPaintRows = paintRows.length > height ? paintRows.slice(0, height) : paintRows;
|
|
1688
1727
|
this.lastChromeKey = `inspect:${dialog.offset}:${width}x${height}`;
|
|
1689
1728
|
this.lastPaintWidth = width;
|
|
@@ -1828,6 +1867,10 @@ export class SshTui {
|
|
|
1828
1867
|
else {
|
|
1829
1868
|
this.focusedRow = null;
|
|
1830
1869
|
}
|
|
1870
|
+
if (this.paintTailBudget > 0) {
|
|
1871
|
+
this.paintTailBudget = 0;
|
|
1872
|
+
this.forceFullPaint = true;
|
|
1873
|
+
}
|
|
1831
1874
|
this.pendingReveal = row;
|
|
1832
1875
|
this.markDirty();
|
|
1833
1876
|
}
|
|
@@ -1942,8 +1985,19 @@ export class SshTui {
|
|
|
1942
1985
|
if (burst.after !== undefined)
|
|
1943
1986
|
compactBurstByReply.set(burst.after, burst);
|
|
1944
1987
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1988
|
+
const skipMiddle = this.paintTailBudget > 0 && this.scrollOffset === 0 && this.pendingReveal === undefined
|
|
1989
|
+
&& this.rows.length > this.paintTailBudget + 8;
|
|
1990
|
+
const historyStart = skipMiddle ? Math.max(0, this.rows.length - this.paintTailBudget) : 0;
|
|
1991
|
+
let paintedLeadingCompact = skipMiddle;
|
|
1992
|
+
for (let rowIndex = 0; rowIndex < this.rows.length; rowIndex += 1) {
|
|
1993
|
+
if (skipMiddle && rowIndex >= 4 && rowIndex < historyStart) {
|
|
1994
|
+
if (rowIndex === 4)
|
|
1995
|
+
addDisplay(this.styleLine('system', t('history.folded')));
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
const row = this.rows[rowIndex];
|
|
1999
|
+
if (row === undefined)
|
|
2000
|
+
continue;
|
|
1947
2001
|
if (compact && (row.kind === 'reasoning' || row.kind === 'prompt' || row.kind === 'tool'))
|
|
1948
2002
|
continue;
|
|
1949
2003
|
if (compact && !paintedLeadingCompact && (row.kind === 'assistant' || row.kind === 'user')) {
|
|
@@ -2415,6 +2469,11 @@ export class SshTui {
|
|
|
2415
2469
|
// across the full terminal grid, which parked the caret on a later
|
|
2416
2470
|
// chrome line after a long paste. Un-folded multi-line input still
|
|
2417
2471
|
// maps through wrap() so newlines stay on the right visual row.
|
|
2472
|
+
//
|
|
2473
|
+
// A caret after a glyph that filled the row must move to col 1 of the
|
|
2474
|
+
// next row (and that row must exist). Parking it on the last cell
|
|
2475
|
+
// punches through the glyph; parking at width+1 trips DEC auto-margin
|
|
2476
|
+
// onto the stats line.
|
|
2418
2477
|
let cursorRowOffset;
|
|
2419
2478
|
let column;
|
|
2420
2479
|
if (inputView.folded || masked) {
|
|
@@ -2423,18 +2482,11 @@ export class SshTui {
|
|
|
2423
2482
|
}
|
|
2424
2483
|
else {
|
|
2425
2484
|
const pos = cursorVisualPosition(inputView.text, this.cursor, inputTextWidth);
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
cursorRowOffset = Math.min(pos.row + 1, Math.max(0, inputDisplayLines.length - 1));
|
|
2432
|
-
column = (cursorRowOffset === 0 ? promptWidth : 0) + 1;
|
|
2433
|
-
}
|
|
2434
|
-
else {
|
|
2435
|
-
cursorRowOffset = Math.min(pos.row, Math.max(0, inputDisplayLines.length - 1));
|
|
2436
|
-
column = (pos.row === 0 ? promptWidth : 0) + pos.col + 1;
|
|
2437
|
-
}
|
|
2485
|
+
while (pos.row >= inputDisplayLines.length)
|
|
2486
|
+
inputDisplayLines.push('');
|
|
2487
|
+
cursorRowOffset = pos.row;
|
|
2488
|
+
const rowPrefix = pos.row === 0 ? promptWidth : 0;
|
|
2489
|
+
column = Math.min(width, rowPrefix + pos.col + 1);
|
|
2438
2490
|
}
|
|
2439
2491
|
const inputRows = Math.max(1, inputDisplayLines.length);
|
|
2440
2492
|
const yieldPlanDock = this.dialog !== undefined || suggestionLines.length > 0;
|
|
@@ -2468,10 +2520,15 @@ export class SshTui {
|
|
|
2468
2520
|
visibleRefs.unshift(undefined);
|
|
2469
2521
|
}
|
|
2470
2522
|
this.clickableRows.clear();
|
|
2523
|
+
this.paintedLinkHitsByRow.clear();
|
|
2471
2524
|
for (let index = 0; index < visibleRefs.length; index++) {
|
|
2472
2525
|
const ref = visibleRefs[index];
|
|
2526
|
+
const screenY = headerLines.length + index + 1;
|
|
2473
2527
|
if (ref !== undefined && 'expanded' in ref)
|
|
2474
|
-
this.clickableRows.set(
|
|
2528
|
+
this.clickableRows.set(screenY, ref);
|
|
2529
|
+
const hits = paintedLinkHits(visible[index] ?? '');
|
|
2530
|
+
if (hits.length > 0)
|
|
2531
|
+
this.paintedLinkHitsByRow.set(screenY, hits);
|
|
2475
2532
|
}
|
|
2476
2533
|
const dockPlan = this.findLivePlanRow();
|
|
2477
2534
|
if (dockPlan !== undefined && planDockLines.length > 0) {
|
|
@@ -2620,6 +2677,8 @@ export class SshTui {
|
|
|
2620
2677
|
cursorRow: row,
|
|
2621
2678
|
cursorColumn: column,
|
|
2622
2679
|
}));
|
|
2680
|
+
this.lastPaintCursorRow = row;
|
|
2681
|
+
this.lastPaintCursorColumn = Math.min(width, Math.max(1, column));
|
|
2623
2682
|
this.lastPaintRows = paintRows.length > height ? paintRows.slice(0, height) : paintRows;
|
|
2624
2683
|
this.lastChromeKey = chromeKey;
|
|
2625
2684
|
this.lastPaintWidth = width;
|
|
@@ -2655,7 +2714,7 @@ export class SshTui {
|
|
|
2655
2714
|
const desc = t(descKey, undefined, command.description);
|
|
2656
2715
|
return {
|
|
2657
2716
|
name: command.name,
|
|
2658
|
-
description: command.input
|
|
2717
|
+
description: commandAcceptsAttachments(command.input)
|
|
2659
2718
|
? t('cmd.withImagesSuffix', { desc })
|
|
2660
2719
|
: desc,
|
|
2661
2720
|
local: false,
|
|
@@ -2799,7 +2858,7 @@ export class SshTui {
|
|
|
2799
2858
|
if (!this.color)
|
|
2800
2859
|
return safe;
|
|
2801
2860
|
const code = kind === 'user' ? '36' :
|
|
2802
|
-
kind === 'assistant' ? '
|
|
2861
|
+
kind === 'assistant' ? '37' :
|
|
2803
2862
|
kind === 'reasoning' ? '2;3' :
|
|
2804
2863
|
kind === 'brand' ? '1;38;2;77;107;253' :
|
|
2805
2864
|
kind === 'tool' || kind === 'tool-result' ? '37' :
|
|
@@ -2815,6 +2874,60 @@ export class SshTui {
|
|
|
2815
2874
|
'90';
|
|
2816
2875
|
return `\x1b[${code}m${safe}\x1b[0m`;
|
|
2817
2876
|
}
|
|
2877
|
+
/**
|
|
2878
|
+
* Fold one live or durable stream chunk into the in-progress assistant
|
|
2879
|
+
* row. 0.1.2 hosts append `assistant/chunk`; 0.1.5 emits the same chunk
|
|
2880
|
+
* on `agent/assistant-stream` and never writes it to the log.
|
|
2881
|
+
*/
|
|
2882
|
+
applyStreamChunk(streamed) {
|
|
2883
|
+
const { chunk } = streamed;
|
|
2884
|
+
const open = this.openStepStats;
|
|
2885
|
+
if (open !== null && open !== undefined
|
|
2886
|
+
&& open.turn === streamed.turn && open.step === streamed.step) {
|
|
2887
|
+
if (open.firstTokenTime === null
|
|
2888
|
+
&& chunk.type === 'text-delta'
|
|
2889
|
+
&& chunk.text !== '') {
|
|
2890
|
+
this.openStepStats = { ...open, firstTokenTime: streamed.time };
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
if (chunk.type === 'usage' && chunk.usage !== undefined) {
|
|
2894
|
+
this.recordUsage(streamed.turn, streamed.step, chunk.usage);
|
|
2895
|
+
}
|
|
2896
|
+
if (chunk.type === 'text-delta') {
|
|
2897
|
+
this.streaming ??= { text: '', reasoning: '' };
|
|
2898
|
+
this.streaming.text += chunk.text ?? '';
|
|
2899
|
+
this.markDirty();
|
|
2900
|
+
}
|
|
2901
|
+
else if (chunk.type === 'reasoning-delta') {
|
|
2902
|
+
this.streaming ??= { text: '', reasoning: '' };
|
|
2903
|
+
if (this.streaming.reasoning === '' && (chunk.text ?? '') !== '') {
|
|
2904
|
+
this.thinkingStartedAt = Date.now();
|
|
2905
|
+
this.streamingReasoning = { kind: 'streaming-reasoning', expanded: false };
|
|
2906
|
+
}
|
|
2907
|
+
this.streaming.reasoning += chunk.text ?? '';
|
|
2908
|
+
this.markDirty();
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* 0.1.5 live tokens arrive as process-local `agent/assistant-stream`
|
|
2913
|
+
* frames (start / chunk / end). Chunk frames carry the same
|
|
2914
|
+
* `StreamChunk` the 0.1.2 log used to store as `assistant/chunk`.
|
|
2915
|
+
*/
|
|
2916
|
+
handleAssistantStream = (...args) => {
|
|
2917
|
+
const payload = args[0];
|
|
2918
|
+
if (payload === undefined)
|
|
2919
|
+
return;
|
|
2920
|
+
const agent = payload.agent;
|
|
2921
|
+
if (agent === undefined || agent !== this.agent)
|
|
2922
|
+
return;
|
|
2923
|
+
if (this.replaying)
|
|
2924
|
+
return;
|
|
2925
|
+
this.lastActivity = Date.now();
|
|
2926
|
+
this.refreshContextPressure();
|
|
2927
|
+
const streamed = streamChunkOf(payload.frame);
|
|
2928
|
+
if (streamed !== undefined)
|
|
2929
|
+
this.applyStreamChunk(streamed);
|
|
2930
|
+
};
|
|
2818
2931
|
// ── event handling ──────────────────────────────────────────────────────
|
|
2819
2932
|
/**
|
|
2820
2933
|
* Apply the TUI's subagent model selection to every child-agent request.
|
|
@@ -2846,7 +2959,17 @@ export class SshTui {
|
|
|
2846
2959
|
return;
|
|
2847
2960
|
}
|
|
2848
2961
|
this.lastActivity = Date.now();
|
|
2849
|
-
this.
|
|
2962
|
+
if (this.replaying && isAssistantStreamEvent(event))
|
|
2963
|
+
return;
|
|
2964
|
+
if (!this.replaying)
|
|
2965
|
+
this.refreshContextPressure();
|
|
2966
|
+
const eventType = sessionEventType(event);
|
|
2967
|
+
if (eventType === 'assistant/chunk') {
|
|
2968
|
+
const streamed = streamChunkOf(event);
|
|
2969
|
+
if (streamed !== undefined)
|
|
2970
|
+
this.applyStreamChunk(streamed);
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2850
2973
|
switch (event.type) {
|
|
2851
2974
|
case 'user/message': {
|
|
2852
2975
|
const text = event.data.content
|
|
@@ -2886,36 +3009,6 @@ export class SshTui {
|
|
|
2886
3009
|
}
|
|
2887
3010
|
break;
|
|
2888
3011
|
}
|
|
2889
|
-
case 'assistant/chunk': {
|
|
2890
|
-
const chunk = event.data.chunk;
|
|
2891
|
-
const open = this.openStepStats;
|
|
2892
|
-
if (open !== null && open !== undefined
|
|
2893
|
-
&& open.turn === event.data.turn && open.step === event.data.step) {
|
|
2894
|
-
if (open.firstTokenTime === null
|
|
2895
|
-
&& chunk.type === 'text-delta'
|
|
2896
|
-
&& chunk.text !== '') {
|
|
2897
|
-
this.openStepStats = { ...open, firstTokenTime: event.time };
|
|
2898
|
-
}
|
|
2899
|
-
}
|
|
2900
|
-
if (chunk.type === 'usage' && chunk.usage !== undefined) {
|
|
2901
|
-
this.recordUsage(event.data.turn, event.data.step, chunk.usage);
|
|
2902
|
-
}
|
|
2903
|
-
if (chunk.type === 'text-delta') {
|
|
2904
|
-
this.streaming ??= { text: '', reasoning: '' };
|
|
2905
|
-
this.streaming.text += chunk.text;
|
|
2906
|
-
this.markDirty();
|
|
2907
|
-
}
|
|
2908
|
-
else if (chunk.type === 'reasoning-delta') {
|
|
2909
|
-
this.streaming ??= { text: '', reasoning: '' };
|
|
2910
|
-
if (this.streaming.reasoning === '' && chunk.text !== '') {
|
|
2911
|
-
this.thinkingStartedAt = Date.now();
|
|
2912
|
-
this.streamingReasoning = { kind: 'streaming-reasoning', expanded: false };
|
|
2913
|
-
}
|
|
2914
|
-
this.streaming.reasoning += chunk.text;
|
|
2915
|
-
this.markDirty();
|
|
2916
|
-
}
|
|
2917
|
-
break;
|
|
2918
|
-
}
|
|
2919
3012
|
case 'assistant/message': {
|
|
2920
3013
|
const text = event.data.message.content
|
|
2921
3014
|
.filter(block => block.type === 'text')
|
|
@@ -3658,6 +3751,8 @@ export class SshTui {
|
|
|
3658
3751
|
const row = this.findSubagentRow(String(sessionId));
|
|
3659
3752
|
if (row === undefined)
|
|
3660
3753
|
return;
|
|
3754
|
+
if (sessionEventType(event) === 'assistant/chunk')
|
|
3755
|
+
return;
|
|
3661
3756
|
switch (event.type) {
|
|
3662
3757
|
case 'user/message': {
|
|
3663
3758
|
const text = collectText(event.data.content);
|
|
@@ -3665,8 +3760,6 @@ export class SshTui {
|
|
|
3665
3760
|
appendSubagentLog(row, { kind: 'user', text: `❯ ${truncate(text, 4)}` });
|
|
3666
3761
|
break;
|
|
3667
3762
|
}
|
|
3668
|
-
case 'assistant/chunk':
|
|
3669
|
-
break;
|
|
3670
3763
|
case 'assistant/message': {
|
|
3671
3764
|
const text = collectText(event.data.message.content);
|
|
3672
3765
|
if (text !== '')
|
|
@@ -5512,7 +5605,8 @@ export class SshTui {
|
|
|
5512
5605
|
return;
|
|
5513
5606
|
}
|
|
5514
5607
|
if (button === 0) {
|
|
5515
|
-
|
|
5608
|
+
const x = Number(sgrMouse[2]);
|
|
5609
|
+
this.handleMouseClick(y, x);
|
|
5516
5610
|
return;
|
|
5517
5611
|
}
|
|
5518
5612
|
}
|
|
@@ -5542,6 +5636,11 @@ export class SshTui {
|
|
|
5542
5636
|
this.deleteAtCursor();
|
|
5543
5637
|
return;
|
|
5544
5638
|
}
|
|
5639
|
+
// kitty / CSI-u Ctrl+Shift+C (codepoint 99, mods 6 = Ctrl+Shift)
|
|
5640
|
+
if (combined === '\x1b[99;6u') {
|
|
5641
|
+
this.copyFocusedCard();
|
|
5642
|
+
return;
|
|
5643
|
+
}
|
|
5545
5644
|
const ss3 = /^\x1bO[A-Z]/u.exec(combined);
|
|
5546
5645
|
if (ss3 !== null) {
|
|
5547
5646
|
const rest = combined.slice(ss3[0].length);
|
|
@@ -6413,10 +6512,16 @@ export class SshTui {
|
|
|
6413
6512
|
this.markDirty();
|
|
6414
6513
|
}
|
|
6415
6514
|
}
|
|
6416
|
-
/** Toggle the collapsible row under a click
|
|
6417
|
-
handleMouseClick(y) {
|
|
6515
|
+
/** Toggle the collapsible row under a click, or copy an OSC-8 link. */
|
|
6516
|
+
handleMouseClick(y, x = 1) {
|
|
6418
6517
|
if (this.dialog !== undefined)
|
|
6419
6518
|
return;
|
|
6519
|
+
const hits = this.paintedLinkHitsByRow.get(y);
|
|
6520
|
+
const href = hits === undefined ? undefined : hrefAtColumn(hits, Math.max(0, x - 1));
|
|
6521
|
+
if (href !== undefined && href.trim() !== '') {
|
|
6522
|
+
this.copyPlainText(href, t('copy.link', { url: href }));
|
|
6523
|
+
return;
|
|
6524
|
+
}
|
|
6420
6525
|
if (this.cwdChipRow !== undefined && y === this.cwdChipRow) {
|
|
6421
6526
|
this.announceWorkspaceCwd();
|
|
6422
6527
|
return;
|
|
@@ -6426,12 +6531,40 @@ export class SshTui {
|
|
|
6426
6531
|
return;
|
|
6427
6532
|
this.toggleCard(row);
|
|
6428
6533
|
}
|
|
6534
|
+
copyPlainText(text, notice) {
|
|
6535
|
+
this.copyYank = text;
|
|
6536
|
+
this.focusedRow = null;
|
|
6537
|
+
this.input = '';
|
|
6538
|
+
this.cursor = 0;
|
|
6539
|
+
this.inputFolded = false;
|
|
6540
|
+
this.leaveHistoryBrowse();
|
|
6541
|
+
this.write(osc52Clipboard(text));
|
|
6542
|
+
this.pushRow({ kind: 'system', text: notice });
|
|
6543
|
+
this.markDirty();
|
|
6544
|
+
}
|
|
6545
|
+
copyFocusedCard() {
|
|
6546
|
+
if (this.dialog !== undefined)
|
|
6547
|
+
return false;
|
|
6548
|
+
const picked = copyTextFromTranscript(this.rows, this.focusedRow);
|
|
6549
|
+
if (picked.text.trim() === '') {
|
|
6550
|
+
this.pushRow({ kind: 'system', text: t('copy.empty') });
|
|
6551
|
+
this.markDirty();
|
|
6552
|
+
return false;
|
|
6553
|
+
}
|
|
6554
|
+
const source = picked.source === 'focused' ? t('copy.sourceFocused') : t('copy.sourceAssistant');
|
|
6555
|
+
this.copyPlainText(picked.text, t('copy.ok', { chars: picked.text.length, source }));
|
|
6556
|
+
return true;
|
|
6557
|
+
}
|
|
6429
6558
|
scrollInspectOrTranscript(delta) {
|
|
6430
6559
|
if (this.dialog?.kind === 'inspect') {
|
|
6431
6560
|
this.dialog.offset = Math.max(0, this.dialog.offset + delta);
|
|
6432
6561
|
this.markDirty();
|
|
6433
6562
|
return;
|
|
6434
6563
|
}
|
|
6564
|
+
if (this.paintTailBudget > 0) {
|
|
6565
|
+
this.paintTailBudget = 0;
|
|
6566
|
+
this.forceFullPaint = true;
|
|
6567
|
+
}
|
|
6435
6568
|
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
|
|
6436
6569
|
this.markDirty();
|
|
6437
6570
|
}
|
|
@@ -6522,7 +6655,7 @@ export class SshTui {
|
|
|
6522
6655
|
.map(item => {
|
|
6523
6656
|
const descKey = `cmd.${item.name}`;
|
|
6524
6657
|
const desc = t(descKey, undefined, item.description);
|
|
6525
|
-
return `/${item.name.padEnd(12)} ${item.input
|
|
6658
|
+
return `/${item.name.padEnd(12)} ${commandAcceptsAttachments(item.input) ? t('cmd.withImagesSuffix', { desc }) : desc} (dsh)`;
|
|
6526
6659
|
});
|
|
6527
6660
|
this.pushRow({
|
|
6528
6661
|
kind: 'system',
|
|
@@ -6646,6 +6779,9 @@ export class SshTui {
|
|
|
6646
6779
|
this.markDirty();
|
|
6647
6780
|
});
|
|
6648
6781
|
break;
|
|
6782
|
+
case 'copy':
|
|
6783
|
+
this.copyFocusedCard();
|
|
6784
|
+
break;
|
|
6649
6785
|
case 'find':
|
|
6650
6786
|
this.runFindCommand(arg);
|
|
6651
6787
|
break;
|