dsh-ssh-tui 0.5.4 → 0.5.5
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/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 +28 -2
- 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 +178 -52
- package/lib/term-text.js.map +1 -1
- package/lib/tui.js +186 -61
- package/lib/tui.js.map +1 -1
- package/lib/types/copy-text.d.ts +9 -0
- 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 +22 -3
- package/lib/types/tui.d.ts +34 -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. */
|
|
@@ -1209,6 +1227,14 @@ export class SshTui {
|
|
|
1209
1227
|
process.stdout.rows = previousRows;
|
|
1210
1228
|
}
|
|
1211
1229
|
}
|
|
1230
|
+
/** Last CSI cursor column written by {@link paint} (1-based). Tests only. */
|
|
1231
|
+
lastPaintedCursorColumn() {
|
|
1232
|
+
return this.lastPaintCursorColumn;
|
|
1233
|
+
}
|
|
1234
|
+
/** Last CSI cursor row written by {@link paint} (1-based). Tests only. */
|
|
1235
|
+
lastPaintedCursorRow() {
|
|
1236
|
+
return this.lastPaintCursorRow;
|
|
1237
|
+
}
|
|
1212
1238
|
screenColumns() {
|
|
1213
1239
|
if (this.headlessDisplay || this.displayDetached) {
|
|
1214
1240
|
return this.relayColumns ?? process.stdout.columns ?? 80;
|
|
@@ -1684,6 +1710,8 @@ export class SshTui {
|
|
|
1684
1710
|
cursorRow: height,
|
|
1685
1711
|
cursorColumn: 1,
|
|
1686
1712
|
}));
|
|
1713
|
+
this.lastPaintCursorRow = height;
|
|
1714
|
+
this.lastPaintCursorColumn = 1;
|
|
1687
1715
|
this.lastPaintRows = paintRows.length > height ? paintRows.slice(0, height) : paintRows;
|
|
1688
1716
|
this.lastChromeKey = `inspect:${dialog.offset}:${width}x${height}`;
|
|
1689
1717
|
this.lastPaintWidth = width;
|
|
@@ -1828,6 +1856,10 @@ export class SshTui {
|
|
|
1828
1856
|
else {
|
|
1829
1857
|
this.focusedRow = null;
|
|
1830
1858
|
}
|
|
1859
|
+
if (this.paintTailBudget > 0) {
|
|
1860
|
+
this.paintTailBudget = 0;
|
|
1861
|
+
this.forceFullPaint = true;
|
|
1862
|
+
}
|
|
1831
1863
|
this.pendingReveal = row;
|
|
1832
1864
|
this.markDirty();
|
|
1833
1865
|
}
|
|
@@ -1942,8 +1974,19 @@ export class SshTui {
|
|
|
1942
1974
|
if (burst.after !== undefined)
|
|
1943
1975
|
compactBurstByReply.set(burst.after, burst);
|
|
1944
1976
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1977
|
+
const skipMiddle = this.paintTailBudget > 0 && this.scrollOffset === 0 && this.pendingReveal === undefined
|
|
1978
|
+
&& this.rows.length > this.paintTailBudget + 8;
|
|
1979
|
+
const historyStart = skipMiddle ? Math.max(0, this.rows.length - this.paintTailBudget) : 0;
|
|
1980
|
+
let paintedLeadingCompact = skipMiddle;
|
|
1981
|
+
for (let rowIndex = 0; rowIndex < this.rows.length; rowIndex += 1) {
|
|
1982
|
+
if (skipMiddle && rowIndex >= 4 && rowIndex < historyStart) {
|
|
1983
|
+
if (rowIndex === 4)
|
|
1984
|
+
addDisplay(this.styleLine('system', t('history.folded')));
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
const row = this.rows[rowIndex];
|
|
1988
|
+
if (row === undefined)
|
|
1989
|
+
continue;
|
|
1947
1990
|
if (compact && (row.kind === 'reasoning' || row.kind === 'prompt' || row.kind === 'tool'))
|
|
1948
1991
|
continue;
|
|
1949
1992
|
if (compact && !paintedLeadingCompact && (row.kind === 'assistant' || row.kind === 'user')) {
|
|
@@ -2415,6 +2458,11 @@ export class SshTui {
|
|
|
2415
2458
|
// across the full terminal grid, which parked the caret on a later
|
|
2416
2459
|
// chrome line after a long paste. Un-folded multi-line input still
|
|
2417
2460
|
// maps through wrap() so newlines stay on the right visual row.
|
|
2461
|
+
//
|
|
2462
|
+
// A caret after a glyph that filled the row must move to col 1 of the
|
|
2463
|
+
// next row (and that row must exist). Parking it on the last cell
|
|
2464
|
+
// punches through the glyph; parking at width+1 trips DEC auto-margin
|
|
2465
|
+
// onto the stats line.
|
|
2418
2466
|
let cursorRowOffset;
|
|
2419
2467
|
let column;
|
|
2420
2468
|
if (inputView.folded || masked) {
|
|
@@ -2423,18 +2471,11 @@ export class SshTui {
|
|
|
2423
2471
|
}
|
|
2424
2472
|
else {
|
|
2425
2473
|
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
|
-
}
|
|
2474
|
+
while (pos.row >= inputDisplayLines.length)
|
|
2475
|
+
inputDisplayLines.push('');
|
|
2476
|
+
cursorRowOffset = pos.row;
|
|
2477
|
+
const rowPrefix = pos.row === 0 ? promptWidth : 0;
|
|
2478
|
+
column = Math.min(width, rowPrefix + pos.col + 1);
|
|
2438
2479
|
}
|
|
2439
2480
|
const inputRows = Math.max(1, inputDisplayLines.length);
|
|
2440
2481
|
const yieldPlanDock = this.dialog !== undefined || suggestionLines.length > 0;
|
|
@@ -2468,10 +2509,15 @@ export class SshTui {
|
|
|
2468
2509
|
visibleRefs.unshift(undefined);
|
|
2469
2510
|
}
|
|
2470
2511
|
this.clickableRows.clear();
|
|
2512
|
+
this.paintedLinkHitsByRow.clear();
|
|
2471
2513
|
for (let index = 0; index < visibleRefs.length; index++) {
|
|
2472
2514
|
const ref = visibleRefs[index];
|
|
2515
|
+
const screenY = headerLines.length + index + 1;
|
|
2473
2516
|
if (ref !== undefined && 'expanded' in ref)
|
|
2474
|
-
this.clickableRows.set(
|
|
2517
|
+
this.clickableRows.set(screenY, ref);
|
|
2518
|
+
const hits = paintedLinkHits(visible[index] ?? '');
|
|
2519
|
+
if (hits.length > 0)
|
|
2520
|
+
this.paintedLinkHitsByRow.set(screenY, hits);
|
|
2475
2521
|
}
|
|
2476
2522
|
const dockPlan = this.findLivePlanRow();
|
|
2477
2523
|
if (dockPlan !== undefined && planDockLines.length > 0) {
|
|
@@ -2620,6 +2666,8 @@ export class SshTui {
|
|
|
2620
2666
|
cursorRow: row,
|
|
2621
2667
|
cursorColumn: column,
|
|
2622
2668
|
}));
|
|
2669
|
+
this.lastPaintCursorRow = row;
|
|
2670
|
+
this.lastPaintCursorColumn = Math.min(width, Math.max(1, column));
|
|
2623
2671
|
this.lastPaintRows = paintRows.length > height ? paintRows.slice(0, height) : paintRows;
|
|
2624
2672
|
this.lastChromeKey = chromeKey;
|
|
2625
2673
|
this.lastPaintWidth = width;
|
|
@@ -2655,7 +2703,7 @@ export class SshTui {
|
|
|
2655
2703
|
const desc = t(descKey, undefined, command.description);
|
|
2656
2704
|
return {
|
|
2657
2705
|
name: command.name,
|
|
2658
|
-
description: command.input
|
|
2706
|
+
description: commandAcceptsAttachments(command.input)
|
|
2659
2707
|
? t('cmd.withImagesSuffix', { desc })
|
|
2660
2708
|
: desc,
|
|
2661
2709
|
local: false,
|
|
@@ -2799,7 +2847,7 @@ export class SshTui {
|
|
|
2799
2847
|
if (!this.color)
|
|
2800
2848
|
return safe;
|
|
2801
2849
|
const code = kind === 'user' ? '36' :
|
|
2802
|
-
kind === 'assistant' ? '
|
|
2850
|
+
kind === 'assistant' ? '37' :
|
|
2803
2851
|
kind === 'reasoning' ? '2;3' :
|
|
2804
2852
|
kind === 'brand' ? '1;38;2;77;107;253' :
|
|
2805
2853
|
kind === 'tool' || kind === 'tool-result' ? '37' :
|
|
@@ -2815,6 +2863,60 @@ export class SshTui {
|
|
|
2815
2863
|
'90';
|
|
2816
2864
|
return `\x1b[${code}m${safe}\x1b[0m`;
|
|
2817
2865
|
}
|
|
2866
|
+
/**
|
|
2867
|
+
* Fold one live or durable stream chunk into the in-progress assistant
|
|
2868
|
+
* row. 0.1.2 hosts append `assistant/chunk`; 0.1.5 emits the same chunk
|
|
2869
|
+
* on `agent/assistant-stream` and never writes it to the log.
|
|
2870
|
+
*/
|
|
2871
|
+
applyStreamChunk(streamed) {
|
|
2872
|
+
const { chunk } = streamed;
|
|
2873
|
+
const open = this.openStepStats;
|
|
2874
|
+
if (open !== null && open !== undefined
|
|
2875
|
+
&& open.turn === streamed.turn && open.step === streamed.step) {
|
|
2876
|
+
if (open.firstTokenTime === null
|
|
2877
|
+
&& chunk.type === 'text-delta'
|
|
2878
|
+
&& chunk.text !== '') {
|
|
2879
|
+
this.openStepStats = { ...open, firstTokenTime: streamed.time };
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
if (chunk.type === 'usage' && chunk.usage !== undefined) {
|
|
2883
|
+
this.recordUsage(streamed.turn, streamed.step, chunk.usage);
|
|
2884
|
+
}
|
|
2885
|
+
if (chunk.type === 'text-delta') {
|
|
2886
|
+
this.streaming ??= { text: '', reasoning: '' };
|
|
2887
|
+
this.streaming.text += chunk.text ?? '';
|
|
2888
|
+
this.markDirty();
|
|
2889
|
+
}
|
|
2890
|
+
else if (chunk.type === 'reasoning-delta') {
|
|
2891
|
+
this.streaming ??= { text: '', reasoning: '' };
|
|
2892
|
+
if (this.streaming.reasoning === '' && (chunk.text ?? '') !== '') {
|
|
2893
|
+
this.thinkingStartedAt = Date.now();
|
|
2894
|
+
this.streamingReasoning = { kind: 'streaming-reasoning', expanded: false };
|
|
2895
|
+
}
|
|
2896
|
+
this.streaming.reasoning += chunk.text ?? '';
|
|
2897
|
+
this.markDirty();
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* 0.1.5 live tokens arrive as process-local `agent/assistant-stream`
|
|
2902
|
+
* frames (start / chunk / end). Chunk frames carry the same
|
|
2903
|
+
* `StreamChunk` the 0.1.2 log used to store as `assistant/chunk`.
|
|
2904
|
+
*/
|
|
2905
|
+
handleAssistantStream = (...args) => {
|
|
2906
|
+
const payload = args[0];
|
|
2907
|
+
if (payload === undefined)
|
|
2908
|
+
return;
|
|
2909
|
+
const agent = payload.agent;
|
|
2910
|
+
if (agent === undefined || agent !== this.agent)
|
|
2911
|
+
return;
|
|
2912
|
+
if (this.replaying)
|
|
2913
|
+
return;
|
|
2914
|
+
this.lastActivity = Date.now();
|
|
2915
|
+
this.refreshContextPressure();
|
|
2916
|
+
const streamed = streamChunkOf(payload.frame);
|
|
2917
|
+
if (streamed !== undefined)
|
|
2918
|
+
this.applyStreamChunk(streamed);
|
|
2919
|
+
};
|
|
2818
2920
|
// ── event handling ──────────────────────────────────────────────────────
|
|
2819
2921
|
/**
|
|
2820
2922
|
* Apply the TUI's subagent model selection to every child-agent request.
|
|
@@ -2846,7 +2948,17 @@ export class SshTui {
|
|
|
2846
2948
|
return;
|
|
2847
2949
|
}
|
|
2848
2950
|
this.lastActivity = Date.now();
|
|
2849
|
-
this.
|
|
2951
|
+
if (this.replaying && isAssistantStreamEvent(event))
|
|
2952
|
+
return;
|
|
2953
|
+
if (!this.replaying)
|
|
2954
|
+
this.refreshContextPressure();
|
|
2955
|
+
const eventType = sessionEventType(event);
|
|
2956
|
+
if (eventType === 'assistant/chunk') {
|
|
2957
|
+
const streamed = streamChunkOf(event);
|
|
2958
|
+
if (streamed !== undefined)
|
|
2959
|
+
this.applyStreamChunk(streamed);
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2850
2962
|
switch (event.type) {
|
|
2851
2963
|
case 'user/message': {
|
|
2852
2964
|
const text = event.data.content
|
|
@@ -2886,36 +2998,6 @@ export class SshTui {
|
|
|
2886
2998
|
}
|
|
2887
2999
|
break;
|
|
2888
3000
|
}
|
|
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
3001
|
case 'assistant/message': {
|
|
2920
3002
|
const text = event.data.message.content
|
|
2921
3003
|
.filter(block => block.type === 'text')
|
|
@@ -3658,6 +3740,8 @@ export class SshTui {
|
|
|
3658
3740
|
const row = this.findSubagentRow(String(sessionId));
|
|
3659
3741
|
if (row === undefined)
|
|
3660
3742
|
return;
|
|
3743
|
+
if (sessionEventType(event) === 'assistant/chunk')
|
|
3744
|
+
return;
|
|
3661
3745
|
switch (event.type) {
|
|
3662
3746
|
case 'user/message': {
|
|
3663
3747
|
const text = collectText(event.data.content);
|
|
@@ -3665,8 +3749,6 @@ export class SshTui {
|
|
|
3665
3749
|
appendSubagentLog(row, { kind: 'user', text: `❯ ${truncate(text, 4)}` });
|
|
3666
3750
|
break;
|
|
3667
3751
|
}
|
|
3668
|
-
case 'assistant/chunk':
|
|
3669
|
-
break;
|
|
3670
3752
|
case 'assistant/message': {
|
|
3671
3753
|
const text = collectText(event.data.message.content);
|
|
3672
3754
|
if (text !== '')
|
|
@@ -5512,7 +5594,8 @@ export class SshTui {
|
|
|
5512
5594
|
return;
|
|
5513
5595
|
}
|
|
5514
5596
|
if (button === 0) {
|
|
5515
|
-
|
|
5597
|
+
const x = Number(sgrMouse[2]);
|
|
5598
|
+
this.handleMouseClick(y, x);
|
|
5516
5599
|
return;
|
|
5517
5600
|
}
|
|
5518
5601
|
}
|
|
@@ -5542,6 +5625,11 @@ export class SshTui {
|
|
|
5542
5625
|
this.deleteAtCursor();
|
|
5543
5626
|
return;
|
|
5544
5627
|
}
|
|
5628
|
+
// kitty / CSI-u Ctrl+Shift+C (codepoint 99, mods 6 = Ctrl+Shift)
|
|
5629
|
+
if (combined === '\x1b[99;6u') {
|
|
5630
|
+
this.copyFocusedCard();
|
|
5631
|
+
return;
|
|
5632
|
+
}
|
|
5545
5633
|
const ss3 = /^\x1bO[A-Z]/u.exec(combined);
|
|
5546
5634
|
if (ss3 !== null) {
|
|
5547
5635
|
const rest = combined.slice(ss3[0].length);
|
|
@@ -6413,10 +6501,16 @@ export class SshTui {
|
|
|
6413
6501
|
this.markDirty();
|
|
6414
6502
|
}
|
|
6415
6503
|
}
|
|
6416
|
-
/** Toggle the collapsible row under a click
|
|
6417
|
-
handleMouseClick(y) {
|
|
6504
|
+
/** Toggle the collapsible row under a click, or copy an OSC-8 link. */
|
|
6505
|
+
handleMouseClick(y, x = 1) {
|
|
6418
6506
|
if (this.dialog !== undefined)
|
|
6419
6507
|
return;
|
|
6508
|
+
const hits = this.paintedLinkHitsByRow.get(y);
|
|
6509
|
+
const href = hits === undefined ? undefined : hrefAtColumn(hits, Math.max(0, x - 1));
|
|
6510
|
+
if (href !== undefined && href.trim() !== '') {
|
|
6511
|
+
this.copyPlainText(href, t('copy.link', { url: href }));
|
|
6512
|
+
return;
|
|
6513
|
+
}
|
|
6420
6514
|
if (this.cwdChipRow !== undefined && y === this.cwdChipRow) {
|
|
6421
6515
|
this.announceWorkspaceCwd();
|
|
6422
6516
|
return;
|
|
@@ -6426,12 +6520,40 @@ export class SshTui {
|
|
|
6426
6520
|
return;
|
|
6427
6521
|
this.toggleCard(row);
|
|
6428
6522
|
}
|
|
6523
|
+
copyPlainText(text, notice) {
|
|
6524
|
+
this.copyYank = text;
|
|
6525
|
+
this.focusedRow = null;
|
|
6526
|
+
this.input = '';
|
|
6527
|
+
this.cursor = 0;
|
|
6528
|
+
this.inputFolded = false;
|
|
6529
|
+
this.leaveHistoryBrowse();
|
|
6530
|
+
this.write(osc52Clipboard(text));
|
|
6531
|
+
this.pushRow({ kind: 'system', text: notice });
|
|
6532
|
+
this.markDirty();
|
|
6533
|
+
}
|
|
6534
|
+
copyFocusedCard() {
|
|
6535
|
+
if (this.dialog !== undefined)
|
|
6536
|
+
return false;
|
|
6537
|
+
const picked = copyTextFromTranscript(this.rows, this.focusedRow);
|
|
6538
|
+
if (picked.text.trim() === '') {
|
|
6539
|
+
this.pushRow({ kind: 'system', text: t('copy.empty') });
|
|
6540
|
+
this.markDirty();
|
|
6541
|
+
return false;
|
|
6542
|
+
}
|
|
6543
|
+
const source = picked.source === 'focused' ? t('copy.sourceFocused') : t('copy.sourceAssistant');
|
|
6544
|
+
this.copyPlainText(picked.text, t('copy.ok', { chars: picked.text.length, source }));
|
|
6545
|
+
return true;
|
|
6546
|
+
}
|
|
6429
6547
|
scrollInspectOrTranscript(delta) {
|
|
6430
6548
|
if (this.dialog?.kind === 'inspect') {
|
|
6431
6549
|
this.dialog.offset = Math.max(0, this.dialog.offset + delta);
|
|
6432
6550
|
this.markDirty();
|
|
6433
6551
|
return;
|
|
6434
6552
|
}
|
|
6553
|
+
if (this.paintTailBudget > 0) {
|
|
6554
|
+
this.paintTailBudget = 0;
|
|
6555
|
+
this.forceFullPaint = true;
|
|
6556
|
+
}
|
|
6435
6557
|
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
|
|
6436
6558
|
this.markDirty();
|
|
6437
6559
|
}
|
|
@@ -6522,7 +6644,7 @@ export class SshTui {
|
|
|
6522
6644
|
.map(item => {
|
|
6523
6645
|
const descKey = `cmd.${item.name}`;
|
|
6524
6646
|
const desc = t(descKey, undefined, item.description);
|
|
6525
|
-
return `/${item.name.padEnd(12)} ${item.input
|
|
6647
|
+
return `/${item.name.padEnd(12)} ${commandAcceptsAttachments(item.input) ? t('cmd.withImagesSuffix', { desc }) : desc} (dsh)`;
|
|
6526
6648
|
});
|
|
6527
6649
|
this.pushRow({
|
|
6528
6650
|
kind: 'system',
|
|
@@ -6646,6 +6768,9 @@ export class SshTui {
|
|
|
6646
6768
|
this.markDirty();
|
|
6647
6769
|
});
|
|
6648
6770
|
break;
|
|
6771
|
+
case 'copy':
|
|
6772
|
+
this.copyFocusedCard();
|
|
6773
|
+
break;
|
|
6649
6774
|
case 'find':
|
|
6650
6775
|
this.runFindCommand(arg);
|
|
6651
6776
|
break;
|