@toddzheng024/dscode-bundle 0.7.12 → 0.7.14
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/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +6 -2
- package/package.json +7 -5
- package/plugins/code-review/index.mjs +5 -2
- package/plugins/compaction/threshold.mjs +12 -0
- package/plugins/credentials/index.mjs +13 -0
- package/plugins/grok/adapter.mjs +114 -0
- package/plugins/grok/auth.mjs +76 -0
- package/plugins/grok/billing.mjs +146 -0
- package/plugins/grok/index.mjs +118 -0
- package/plugins/grok/models.mjs +129 -0
- package/plugins/grok/status.mjs +28 -0
- package/plugins/grok/wire.mjs +268 -0
- package/plugins/i18n/messages.mjs +6 -6
- package/plugins/providers/catalog.mjs +2 -0
- package/plugins/session-metrics/view.mjs +34 -3
- package/vendor/compaction-basic/index.js +91 -1
- package/vendor/persistent/index.js +10 -9
- package/vendor/terminal/index.js +5 -0
- package/vendor/tui/lib/app.mjs +6711 -0
- package/vendor/tui/lib/approval.mjs +122 -0
- package/vendor/tui/lib/attachments.mjs +218 -0
- package/vendor/tui/lib/authorization-panel.mjs +242 -0
- package/vendor/tui/lib/authorization.mjs +111 -0
- package/vendor/tui/lib/commands.mjs +123 -0
- package/vendor/tui/lib/dscode/chat.mjs +84 -0
- package/vendor/tui/lib/dscode/flags.mjs +32 -0
- package/vendor/tui/lib/dscode/model-search.mjs +68 -0
- package/vendor/tui/lib/dscode/paste.mjs +87 -0
- package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
- package/vendor/tui/lib/dscode/welcome.mjs +100 -0
- package/vendor/tui/lib/editor-keys.mjs +346 -0
- package/vendor/tui/lib/editor.mjs +50 -0
- package/vendor/tui/lib/fork.mjs +24 -0
- package/vendor/tui/lib/git-workflow.mjs +278 -0
- package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
- package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
- package/vendor/tui/lib/index.mjs +2200 -0
- package/vendor/tui/lib/input-split.mjs +170 -0
- package/vendor/tui/lib/internals.mjs +90 -0
- package/vendor/tui/lib/invariant.mjs +22 -0
- package/vendor/tui/lib/kernel-panels.mjs +1265 -0
- package/vendor/tui/lib/keyboard.mjs +283 -0
- package/vendor/tui/lib/language-panel.mjs +41 -0
- package/vendor/tui/lib/locales/en.mjs +495 -0
- package/vendor/tui/lib/locales/zh.mjs +495 -0
- package/vendor/tui/lib/mentions.mjs +150 -0
- package/vendor/tui/lib/model-capabilities.mjs +245 -0
- package/vendor/tui/lib/models.mjs +215 -0
- package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
- package/vendor/tui/lib/permissions.mjs +45 -0
- package/vendor/tui/lib/plugin-inventory.mjs +27 -0
- package/vendor/tui/lib/presets.mjs +46 -0
- package/vendor/tui/lib/provider-settings.mjs +600 -0
- package/vendor/tui/lib/questions.mjs +116 -0
- package/vendor/tui/lib/rainbow.mjs +181 -0
- package/vendor/tui/lib/render/animations.mjs +665 -0
- package/vendor/tui/lib/render/editor.mjs +437 -0
- package/vendor/tui/lib/render/export.mjs +119 -0
- package/vendor/tui/lib/render/fuzzy.mjs +79 -0
- package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
- package/vendor/tui/lib/render/inspector.mjs +93 -0
- package/vendor/tui/lib/render/lines.mjs +538 -0
- package/vendor/tui/lib/render/markdown.mjs +592 -0
- package/vendor/tui/lib/render/projection.mjs +1864 -0
- package/vendor/tui/lib/render/status.mjs +591 -0
- package/vendor/tui/lib/render/text.mjs +157 -0
- package/vendor/tui/lib/render/tool-detail.mjs +187 -0
- package/vendor/tui/lib/render/tool-preview.mjs +79 -0
- package/vendor/tui/lib/render/usage.mjs +336 -0
- package/vendor/tui/lib/render/width.mjs +191 -0
- package/vendor/tui/lib/session-directory.mjs +329 -0
- package/vendor/tui/lib/session-query.mjs +184 -0
- package/vendor/tui/lib/session-switch.mjs +51 -0
- package/vendor/tui/lib/settings-file.mjs +75 -0
- package/vendor/tui/lib/skills.mjs +119 -0
- package/vendor/tui/lib/startup.mjs +111 -0
- package/vendor/tui/lib/store.mjs +129 -0
- package/vendor/tui/lib/subagents.mjs +200 -0
- package/vendor/tui/lib/terminal-title.mjs +186 -0
- package/vendor/tui/lib/theme-panel.mjs +60 -0
- package/vendor/tui/lib/theme.mjs +379 -0
- package/vendor/tui/lib/update-panel.mjs +239 -0
- package/vendor/tui/lib/update.mjs +101 -0
- package/vendor/tui/lib/version.mjs +105 -0
- package/vendor/tui/lib/whale-glyph.mjs +20 -0
- package/vendor/tui/LICENSE +0 -21
- package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
- package/vendor/tui/dscode-email/cli.mjs +0 -32
- package/vendor/tui/dscode-email/contacts.mjs +0 -36
- package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
- package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
- package/vendor/tui/dscode-email/gmail.mjs +0 -194
- package/vendor/tui/dscode-email/imap.mjs +0 -136
- package/vendor/tui/dscode-email/inbox.d.mts +0 -25
- package/vendor/tui/dscode-email/inbox.mjs +0 -76
- package/vendor/tui/dscode-email/smtp.mjs +0 -69
- package/vendor/tui/dscode-email/store.mjs +0 -32
- package/vendor/tui/dscode-providers/catalog.mjs +0 -136
- package/vendor/tui/dscode-providers/effort.mjs +0 -35
- package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
- package/vendor/tui/index.mjs +0 -44441
- package/vendor/tui/invariant.mjs +0 -21
- package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
- package/vendor/tui/session-query.mjs +0 -150
- package/vendor/tui/startup.mjs +0 -109
- package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
- package/vendor/tui/types/app.d.ts +0 -348
- package/vendor/tui/types/approval.d.ts +0 -59
- package/vendor/tui/types/attachments.d.ts +0 -52
- package/vendor/tui/types/authorization-panel.d.ts +0 -22
- package/vendor/tui/types/authorization.d.ts +0 -36
- package/vendor/tui/types/commands.d.ts +0 -52
- package/vendor/tui/types/editor-keys.d.ts +0 -105
- package/vendor/tui/types/editor.d.ts +0 -6
- package/vendor/tui/types/fork.d.ts +0 -8
- package/vendor/tui/types/git-workflow.d.ts +0 -121
- package/vendor/tui/types/index.d.ts +0 -223
- package/vendor/tui/types/input-split.d.ts +0 -54
- package/vendor/tui/types/internals.d.ts +0 -26
- package/vendor/tui/types/invariant.d.ts +0 -15
- package/vendor/tui/types/kernel-panels.d.ts +0 -245
- package/vendor/tui/types/keyboard.d.ts +0 -80
- package/vendor/tui/types/language-panel.d.ts +0 -12
- package/vendor/tui/types/locales/en.d.ts +0 -450
- package/vendor/tui/types/locales/zh.d.ts +0 -9
- package/vendor/tui/types/mentions.d.ts +0 -85
- package/vendor/tui/types/model-capabilities.d.ts +0 -82
- package/vendor/tui/types/models.d.ts +0 -133
- package/vendor/tui/types/permissions.d.ts +0 -27
- package/vendor/tui/types/plugin-inventory.d.ts +0 -11
- package/vendor/tui/types/presets.d.ts +0 -22
- package/vendor/tui/types/provider-settings.d.ts +0 -239
- package/vendor/tui/types/questions.d.ts +0 -54
- package/vendor/tui/types/rainbow.d.ts +0 -69
- package/vendor/tui/types/render/animations.d.ts +0 -307
- package/vendor/tui/types/render/editor.d.ts +0 -163
- package/vendor/tui/types/render/export.d.ts +0 -9
- package/vendor/tui/types/render/fuzzy.d.ts +0 -21
- package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
- package/vendor/tui/types/render/inspector.d.ts +0 -62
- package/vendor/tui/types/render/lines.d.ts +0 -86
- package/vendor/tui/types/render/markdown.d.ts +0 -29
- package/vendor/tui/types/render/projection.d.ts +0 -587
- package/vendor/tui/types/render/status.d.ts +0 -196
- package/vendor/tui/types/render/text.d.ts +0 -64
- package/vendor/tui/types/render/tool-detail.d.ts +0 -94
- package/vendor/tui/types/render/tool-preview.d.ts +0 -28
- package/vendor/tui/types/render/usage.d.ts +0 -113
- package/vendor/tui/types/render/width.d.ts +0 -29
- package/vendor/tui/types/session-directory.d.ts +0 -190
- package/vendor/tui/types/session-query.d.ts +0 -92
- package/vendor/tui/types/session-switch.d.ts +0 -17
- package/vendor/tui/types/settings-file.d.ts +0 -41
- package/vendor/tui/types/skills.d.ts +0 -47
- package/vendor/tui/types/startup.d.ts +0 -65
- package/vendor/tui/types/store.d.ts +0 -58
- package/vendor/tui/types/subagents.d.ts +0 -70
- package/vendor/tui/types/terminal-title.d.ts +0 -66
- package/vendor/tui/types/theme-panel.d.ts +0 -24
- package/vendor/tui/types/theme.d.ts +0 -434
- package/vendor/tui/types/update-panel.d.ts +0 -49
- package/vendor/tui/types/update.d.ts +0 -75
- package/vendor/tui/types/version.d.ts +0 -19
- package/vendor/tui/types/whale-glyph.d.ts +0 -6
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
- /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status-bar composition for the TUI footer. Codex/Claude-Code-style split
|
|
3
|
+
* line: identity facts and session figures flow from the left, while the
|
|
4
|
+
* permission badge (the Codex "autonomous selection" anchor, with its
|
|
5
|
+
* shift+tab cycle hint) pins to the right edge. Every segment carries a tone
|
|
6
|
+
* the footer maps to a theme color, and layoutStatusBar degrades the line
|
|
7
|
+
* item by item so it always fits one physical row — truncation with an
|
|
8
|
+
* ellipsis happens only after every lesser group has already dropped out.
|
|
9
|
+
*
|
|
10
|
+
* @module @deepseek-ai/dsh-tui/render/status
|
|
11
|
+
*/
|
|
12
|
+
import { visibleColumns } from './markdown.mjs';
|
|
13
|
+
import { billedInputTokens } from './usage.mjs';
|
|
14
|
+
import { singleLineText, truncateColumns, formatTokens } from './text.mjs';
|
|
15
|
+
// Re-exported for the callers that have always read the formatter here.
|
|
16
|
+
export { formatTokens };
|
|
17
|
+
import { t } from '../i18n.mjs';
|
|
18
|
+
/**
|
|
19
|
+
* Compact duration: 45.2s under a minute, 2m42s from there on.
|
|
20
|
+
* @param ms - duration in milliseconds.
|
|
21
|
+
* @returns display string.
|
|
22
|
+
*/
|
|
23
|
+
export function formatDuration(ms) {
|
|
24
|
+
const s = ms / 1_000;
|
|
25
|
+
if (s < 60)
|
|
26
|
+
return String(Math.round(s * 10) / 10) + 's';
|
|
27
|
+
const whole = Math.round(s);
|
|
28
|
+
return Math.floor(whole / 60) + 'm' + (whole % 60) + 's';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Compact decode rate: one decimal under a hundred, whole below a thousand,
|
|
32
|
+
* then thousands (15.3 / 124 / 1.2K).
|
|
33
|
+
* @param n - tokens per second.
|
|
34
|
+
* @returns display string.
|
|
35
|
+
*/
|
|
36
|
+
function formatRate(n) {
|
|
37
|
+
if (n < 100)
|
|
38
|
+
return String(Math.round(n * 10) / 10);
|
|
39
|
+
if (n < 1_000)
|
|
40
|
+
return String(Math.round(n));
|
|
41
|
+
return String(Math.round(n / 100) / 10) + 'K';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Cache-hit share of billed prompt-side input. The denominator is the same
|
|
45
|
+
* billed total the /usage panel shows (uncached input plus both cache
|
|
46
|
+
* buckets), so the two readouts can never disagree.
|
|
47
|
+
* @param usage - cumulative token totals.
|
|
48
|
+
* @returns percent rounded to one decimal place, or null when no input was billed.
|
|
49
|
+
*/
|
|
50
|
+
export function cacheHitPercent(usage) {
|
|
51
|
+
const billed = billedInputTokens(usage);
|
|
52
|
+
return billed === 0
|
|
53
|
+
? null
|
|
54
|
+
: Math.round(usage.cacheReadTokens / billed * 1_000) / 10;
|
|
55
|
+
}
|
|
56
|
+
/** Separator between leading clusters. */
|
|
57
|
+
export const STATUS_GROUP_SEPARATOR = ' | ';
|
|
58
|
+
/** Separator between trailing state spans. */
|
|
59
|
+
export const STATUS_ITEM_SEPARATOR = ' | ';
|
|
60
|
+
/** The Codex-style mode cycle hint appended to the permission badge. */
|
|
61
|
+
/** English compatibility value for callers that only measure the default layout. */
|
|
62
|
+
export const STATUS_CYCLE_HINT = ' (shift+tab to cycle)';
|
|
63
|
+
/** Localized mode-cycle hint used by the live layout. */
|
|
64
|
+
export function statusCycleHint() {
|
|
65
|
+
return ` ${t('status.cycleHint')}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Interior columns of the context bar. The layout starts every bar at this
|
|
69
|
+
* width so the drop ladder can pre-measure the group, then degrades the
|
|
70
|
+
* readout and shrinks the bar inside a tighter budget before dropping the
|
|
71
|
+
* group (see CONTEXT_MIN_WIDTH) rather than asking the layout for more room.
|
|
72
|
+
*/
|
|
73
|
+
export const CONTEXT_BAR_WIDTH = 24;
|
|
74
|
+
/** Occupancy at which the usage readout flips from brand blue to amber. */
|
|
75
|
+
const CONTEXT_WARN_PERCENT = 90;
|
|
76
|
+
/**
|
|
77
|
+
* Narrowest bar width the drop ladder keeps before dropping the whole
|
|
78
|
+
* context group: the bar shrinks to this floor first (the absolute readout
|
|
79
|
+
* survives), and only past it does the readout degrade and the group go.
|
|
80
|
+
*/
|
|
81
|
+
const CONTEXT_MIN_WIDTH = 5;
|
|
82
|
+
/**
|
|
83
|
+
* Render context occupancy as ONE stepless proportional bar: a solid
|
|
84
|
+
* DeepSeek-blue fill run tracking the occupancy and a dim dotted free
|
|
85
|
+
* track for the rest. Nothing else lives inside the bar — the usage
|
|
86
|
+
* readout rides outside it (see contextGroupSpans) — so the geometry
|
|
87
|
+
* always reads as the true remaining share. A given occupancy always
|
|
88
|
+
* renders the identical bar.
|
|
89
|
+
* @param usedTokens - reported used tokens.
|
|
90
|
+
* @param contextWindow - route capacity.
|
|
91
|
+
* @param width - total bar columns.
|
|
92
|
+
* @returns tone-split spans for the footer to paint.
|
|
93
|
+
*/
|
|
94
|
+
export function contextBar(usedTokens, contextWindow, width) {
|
|
95
|
+
if (width <= 0 || contextWindow <= 0)
|
|
96
|
+
return [];
|
|
97
|
+
const used = Math.max(0, usedTokens);
|
|
98
|
+
const fill = Math.min(width, Math.max(0, Math.round(used / contextWindow * width)));
|
|
99
|
+
const spans = [];
|
|
100
|
+
if (fill > 0)
|
|
101
|
+
spans.push({ text: '█'.repeat(fill), tone: 'ctxFill' });
|
|
102
|
+
const free = width - fill;
|
|
103
|
+
if (free > 0)
|
|
104
|
+
spans.push({ text: '░'.repeat(free), tone: 'label' });
|
|
105
|
+
return spans;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compose the context group: the proportional bar plus the usage readout
|
|
109
|
+
* OUTSIDE the bar, so the dotted track keeps its proportional meaning no
|
|
110
|
+
* matter how wide the readout is. `full` reads `12.3K/1.0M 25%`; `percent`
|
|
111
|
+
* drops the absolute pair; `none` is the bare bar. The readout turns amber
|
|
112
|
+
* once occupancy reaches the warning threshold.
|
|
113
|
+
*/
|
|
114
|
+
export function contextGroupSpans(usedTokens, contextWindow, barWidth, readout) {
|
|
115
|
+
const spans = [{ text: t('status.label.context') + ' ', tone: 'label' }];
|
|
116
|
+
spans.push(...contextBar(usedTokens, contextWindow, barWidth));
|
|
117
|
+
if (readout === 'none' || barWidth <= 0 || contextWindow <= 0)
|
|
118
|
+
return spans;
|
|
119
|
+
const used = Math.max(0, usedTokens);
|
|
120
|
+
const percent = Math.round(used / contextWindow * 100);
|
|
121
|
+
const text = readout === 'full'
|
|
122
|
+
? `${formatTokens(used)}/${formatTokens(contextWindow)} ${percent}%`
|
|
123
|
+
: `${percent}%`;
|
|
124
|
+
spans.push({ text: ' ', tone: 'label' }, { text, tone: percent >= CONTEXT_WARN_PERCENT ? 'warn' : 'value' });
|
|
125
|
+
return spans;
|
|
126
|
+
}
|
|
127
|
+
/** The full item catalog in canonical order (the /statusline default). */
|
|
128
|
+
export const STATUS_ITEMS = [
|
|
129
|
+
{ id: 'model', label: 'model', description: 'provider/model serving this session', side: 'left' },
|
|
130
|
+
{ id: 'cwd', label: 'cwd', description: 'working-directory basename', side: 'left' },
|
|
131
|
+
{ id: 'mode', label: 'mode', description: 'agent preset composing the session', side: 'left' },
|
|
132
|
+
{ id: 'branch', label: 'branch', description: 'git branch inside a repository', side: 'left' },
|
|
133
|
+
{ id: 'context', label: 'context', description: 'context-window occupancy meter', side: 'left' },
|
|
134
|
+
{ id: 'permission', label: 'permission', description: 'permission preset badge with cycle hint', side: 'right' },
|
|
135
|
+
{ id: 'plan', label: 'plan', description: 'plan-mode state mark', side: 'left' },
|
|
136
|
+
{ id: 'turns', label: 'turns', description: 'turn and step counters', side: 'left' },
|
|
137
|
+
{ id: 'durations', label: 'durations', description: 'llm/ttft/decode/tool wall time', side: 'left' },
|
|
138
|
+
{ id: 'cache', label: 'cache', description: 'cache-hit share of billed input', side: 'left' },
|
|
139
|
+
{ id: 'tokens', label: 'tokens', description: 'cumulative input/output tokens', side: 'left' },
|
|
140
|
+
{ id: 'title', label: 'title', description: 'session title or short id', side: 'left' },
|
|
141
|
+
{ id: 'goal', label: 'goal', description: 'live goal phase and round progress', side: 'left' },
|
|
142
|
+
{ id: 'sandbox', label: 'sandbox', description: 'divergent sandbox-mode override', side: 'left' },
|
|
143
|
+
];
|
|
144
|
+
/**
|
|
145
|
+
* Default order: the whole catalog (matches the pre-customization bar).
|
|
146
|
+
* The busy dot is not an item — it always leads the identity cluster.
|
|
147
|
+
*/
|
|
148
|
+
export const DEFAULT_STATUSLINE_ITEMS = ['model', 'permission', 'title', 'plan', 'goal', 'sandbox'];
|
|
149
|
+
/**
|
|
150
|
+
* Parse a persisted statusline item list. The stored value is the ordered
|
|
151
|
+
* set of ENABLED items (the Codex /statusline contract): unknown ids and
|
|
152
|
+
* duplicates drop out, and a non-array value (missing or corrupt file)
|
|
153
|
+
* falls back to the full default set. An explicitly empty array is valid —
|
|
154
|
+
* the bar degrades to its busy dot alone.
|
|
155
|
+
* @param value - the raw parsed JSON value (expected string[]).
|
|
156
|
+
* @returns the normalized ordered item list.
|
|
157
|
+
*/
|
|
158
|
+
export function parseStatuslineItems(value) {
|
|
159
|
+
if (!Array.isArray(value))
|
|
160
|
+
return [...DEFAULT_STATUSLINE_ITEMS];
|
|
161
|
+
const known = new Set(STATUS_ITEMS.map(item => item.id));
|
|
162
|
+
const kept = [];
|
|
163
|
+
for (const entry of value) {
|
|
164
|
+
if (typeof entry === 'string' && known.has(entry) && !kept.includes(entry)) {
|
|
165
|
+
kept.push(entry);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return kept;
|
|
169
|
+
}
|
|
170
|
+
/** Minimum blank gap kept between the leading and trailing sides. */
|
|
171
|
+
const LEFT_RIGHT_GAP = 2;
|
|
172
|
+
/** Column held back so Ink/yoga measurement drift can never force a wrap. */
|
|
173
|
+
const WIDTH_SAFETY = 1;
|
|
174
|
+
/**
|
|
175
|
+
* Extra left padding on the secondary row so its content aligns with the
|
|
176
|
+
* model name's left edge on the primary row (padding 2 + busy dot 2). The
|
|
177
|
+
* layout subtracts it from row 2's budget so the indent can never wrap it.
|
|
178
|
+
*/
|
|
179
|
+
export const STATUS_ROW2_INDENT = 2;
|
|
180
|
+
/** Column budget for the session title before it ellipsizes. */
|
|
181
|
+
const TITLE_BUDGET = 48;
|
|
182
|
+
/**
|
|
183
|
+
* Primary-row drop ranks: context drops before permission; the identity
|
|
184
|
+
* cluster never drops and ellipsizes only after the right badge is gone.
|
|
185
|
+
* Secondary-row groups reuse the remaining ranks independently.
|
|
186
|
+
*/
|
|
187
|
+
const RANK_TITLE = 10;
|
|
188
|
+
const RANK_TOKENS = 50;
|
|
189
|
+
const RANK_COUNTS = 90;
|
|
190
|
+
const RANK_CONTEXT = 90;
|
|
191
|
+
const RANK_SANDBOX = 92;
|
|
192
|
+
const RANK_GOAL = 95;
|
|
193
|
+
const RANK_BADGE = 100;
|
|
194
|
+
const RANK_IDENTITY = Number.POSITIVE_INFINITY;
|
|
195
|
+
/** Row 2 drop ranks: title and durations go first; state and counts survive longest. */
|
|
196
|
+
const RANK2_DURATIONS = 40;
|
|
197
|
+
const RANK2_CACHE = 50;
|
|
198
|
+
const RANK2_PLAN = 70;
|
|
199
|
+
/**
|
|
200
|
+
* Traffic-light tone for a permission preset: read-only stays success green,
|
|
201
|
+
* full access reads error red, and every workspace-scoped middle ground
|
|
202
|
+
* (including unknown presets) reads warning amber.
|
|
203
|
+
* @param permission - active permission preset label.
|
|
204
|
+
* @returns tone for the badge span.
|
|
205
|
+
*/
|
|
206
|
+
export function permissionTone(permission) {
|
|
207
|
+
const label = permission.toLowerCase();
|
|
208
|
+
if (label.includes('read'))
|
|
209
|
+
return 'success';
|
|
210
|
+
if (label.includes('danger') || label.includes('full'))
|
|
211
|
+
return 'error';
|
|
212
|
+
return 'warn';
|
|
213
|
+
}
|
|
214
|
+
/** Display-safe external text: one row, controls escaped. */
|
|
215
|
+
function safe(text) {
|
|
216
|
+
return singleLineText(text);
|
|
217
|
+
}
|
|
218
|
+
/** Dim junction separator span inside a cluster. */
|
|
219
|
+
function sep() {
|
|
220
|
+
return { text: ' | ', tone: 'label' };
|
|
221
|
+
}
|
|
222
|
+
/** Total visible columns of a span list (separators ride inside the spans). */
|
|
223
|
+
function spansWidth(spans) {
|
|
224
|
+
let width = 0;
|
|
225
|
+
for (const span of spans)
|
|
226
|
+
width += visibleColumns(span.text);
|
|
227
|
+
return width;
|
|
228
|
+
}
|
|
229
|
+
/** Join widths of parts with one fixed separator between neighbors. */
|
|
230
|
+
function joinWidth(parts, separator) {
|
|
231
|
+
if (parts.length === 0)
|
|
232
|
+
return 0;
|
|
233
|
+
let width = 0;
|
|
234
|
+
for (const part of parts)
|
|
235
|
+
width += part;
|
|
236
|
+
return width + separator * (parts.length - 1);
|
|
237
|
+
}
|
|
238
|
+
/** Build every candidate group/span with its drop rank and item id. */
|
|
239
|
+
/** dscode: the footer telemetry header — `provider: model @ effort`. */
|
|
240
|
+
export function dscodeFooterHeader(facts, stats) {
|
|
241
|
+
const raw = typeof facts.model === 'string' ? facts.model : '';
|
|
242
|
+
if (raw === '')
|
|
243
|
+
return '';
|
|
244
|
+
const cut = raw.indexOf('/');
|
|
245
|
+
const provider = cut > 0 ? raw.slice(0, cut) : '';
|
|
246
|
+
const model = cut > 0 ? raw.slice(cut + 1) : raw;
|
|
247
|
+
const effort = typeof facts.effort === 'string' && facts.effort !== '' ? facts.effort : (typeof stats.reasoningEffort === 'string' ? stats.reasoningEffort : '');
|
|
248
|
+
return (provider === '' ? model : provider + ': ' + model) + (effort === '' ? '' : ' @ ' + effort);
|
|
249
|
+
}
|
|
250
|
+
/** dscode: row 1's identity lead — the session title, else the model and its effort. */
|
|
251
|
+
function dscodeStatusLead(facts, model, effort) {
|
|
252
|
+
const source = facts.title !== undefined && facts.title !== '' ? facts.title : facts.sessionId;
|
|
253
|
+
const title = source === undefined || source === '' ? '' : truncateColumns(safe(source), TITLE_BUDGET);
|
|
254
|
+
if (title !== '')
|
|
255
|
+
return title;
|
|
256
|
+
return effort === '' ? model : model + ' | ' + effort;
|
|
257
|
+
}
|
|
258
|
+
function buildCandidates(facts, stats, busy, enabled, contextWidth) {
|
|
259
|
+
const identity = [
|
|
260
|
+
{ text: busy ? '● ' : '○ ', tone: busy ? 'live' : 'meta' },
|
|
261
|
+
];
|
|
262
|
+
// The dot glues straight to the first fact; further facts join through
|
|
263
|
+
// explicit dim separators, so an absent model never strands a leading ' · '.
|
|
264
|
+
const push = (span) => {
|
|
265
|
+
if (identity.length > 1)
|
|
266
|
+
identity.push(sep());
|
|
267
|
+
identity.push(span);
|
|
268
|
+
};
|
|
269
|
+
const model = safe(facts.model).split('/').at(-1) ?? '';
|
|
270
|
+
// dscode: row 1 leads with the session title (falling back to the model), and the
|
|
271
|
+
// full `provider: model @ effort` header rides the telemetry segment instead.
|
|
272
|
+
if ((model !== '' && enabled.has('model')) || enabled.has('title')) {
|
|
273
|
+
const effort = safe(facts.effort ?? stats.reasoningEffort);
|
|
274
|
+
push({ text: dscodeStatusLead(facts, model, effort), tone: 'model' });
|
|
275
|
+
}
|
|
276
|
+
const cwd = safe(facts.cwd);
|
|
277
|
+
if (cwd !== '' && enabled.has('cwd'))
|
|
278
|
+
push({ text: cwd, tone: 'path' });
|
|
279
|
+
const mode = safe(facts.mode ?? '');
|
|
280
|
+
if (mode !== '' && enabled.has('mode')) {
|
|
281
|
+
push({ text: t('status.label.mode') + ' ', tone: 'label' });
|
|
282
|
+
identity.push({ text: mode, tone: 'accent' });
|
|
283
|
+
}
|
|
284
|
+
const branch = safe(facts.branch);
|
|
285
|
+
if (branch !== '' && enabled.has('branch'))
|
|
286
|
+
push({ text: '⑂ ' + branch, tone: 'branch' });
|
|
287
|
+
const left = [
|
|
288
|
+
{ group: { spans: identity }, rank: RANK_IDENTITY, id: 'identity' },
|
|
289
|
+
];
|
|
290
|
+
const right = [];
|
|
291
|
+
const row2 = [];
|
|
292
|
+
if (stats.turns > 0 || stats.steps > 0) {
|
|
293
|
+
if (enabled.has('turns')) {
|
|
294
|
+
// Label/value pairs join through explicit dim separators.
|
|
295
|
+
const counts = [];
|
|
296
|
+
const pair = (label, value) => {
|
|
297
|
+
if (counts.length > 0)
|
|
298
|
+
counts.push(sep());
|
|
299
|
+
counts.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' });
|
|
300
|
+
};
|
|
301
|
+
pair(t('status.label.turns'), String(stats.turns));
|
|
302
|
+
pair(t('status.label.steps'), String(stats.steps));
|
|
303
|
+
row2.push({ group: { spans: counts }, rank: RANK_COUNTS, id: 'turns' });
|
|
304
|
+
}
|
|
305
|
+
if (enabled.has('durations')) {
|
|
306
|
+
// Model round-trip, first-token latency, decode rate, and tool wall
|
|
307
|
+
// time; the label keeps its one trailing space so each reads as one
|
|
308
|
+
// figure ('model 45.2s'). Named in full — no single-letter codes.
|
|
309
|
+
const durations = [];
|
|
310
|
+
const pair = (label, value) => {
|
|
311
|
+
if (durations.length > 0)
|
|
312
|
+
durations.push(sep());
|
|
313
|
+
durations.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' });
|
|
314
|
+
};
|
|
315
|
+
if (stats.llmMs > 0)
|
|
316
|
+
pair(t('status.label.modelTime'), formatDuration(stats.llmMs));
|
|
317
|
+
if (stats.ttftSteps > 0)
|
|
318
|
+
pair(t('status.label.latency'), formatDuration(stats.ttftMs / stats.ttftSteps));
|
|
319
|
+
if (stats.decodeMs > 0 && stats.decodeTokens > 0) {
|
|
320
|
+
if (durations.length > 0)
|
|
321
|
+
durations.push(sep());
|
|
322
|
+
durations.push({ text: formatRate(stats.decodeTokens / (stats.decodeMs / 1_000)), tone: 'value' }, { text: t('status.label.tokensPerSec'), tone: 'label' });
|
|
323
|
+
}
|
|
324
|
+
if (stats.toolMs > 0)
|
|
325
|
+
pair(t('status.label.tool'), formatDuration(stats.toolMs));
|
|
326
|
+
if (durations.length > 0) {
|
|
327
|
+
row2.push({ group: { spans: durations }, rank: RANK2_DURATIONS, id: 'durations' });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// The cache group carries both facts about cached prompt tokens: how many
|
|
332
|
+
// were read and what share of the billed prompt that was. The read count
|
|
333
|
+
// lives here rather than in the tokens group so the tokens group keeps
|
|
334
|
+
// meaning "what the provider billed outside the cache".
|
|
335
|
+
const cacheHit = cacheHitPercent(stats.usage);
|
|
336
|
+
if (cacheHit !== null && enabled.has('cache')) {
|
|
337
|
+
const spans = [{ text: t('status.label.cache') + ' ', tone: 'label' }];
|
|
338
|
+
if (stats.usage.cacheReadTokens > 0) {
|
|
339
|
+
spans.push({ text: formatTokens(stats.usage.cacheReadTokens), tone: 'value' }, sep());
|
|
340
|
+
}
|
|
341
|
+
spans.push({ text: cacheHit + '%', tone: 'value' });
|
|
342
|
+
row2.push({ group: { spans }, rank: RANK2_CACHE, id: 'cache' });
|
|
343
|
+
}
|
|
344
|
+
// Context occupancy as a purely proportional bar with the usage readout
|
|
345
|
+
// riding outside it: the used total is the most recent reported prompt
|
|
346
|
+
// size against the advertised route capacity.
|
|
347
|
+
if (stats.contextWindow > 0 && stats.lastPromptTokens > 0 && enabled.has('context')) {
|
|
348
|
+
left.push({
|
|
349
|
+
group: {
|
|
350
|
+
spans: contextGroupSpans(stats.lastPromptTokens, stats.contextWindow, contextWidth, 'full'),
|
|
351
|
+
},
|
|
352
|
+
rank: RANK_CONTEXT,
|
|
353
|
+
id: 'context',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
if ((stats.usage.uncachedInputTokens > 0 || stats.usage.outputTokens > 0) && enabled.has('tokens')) {
|
|
357
|
+
const tokens = [];
|
|
358
|
+
const pair = (label, value) => {
|
|
359
|
+
if (tokens.length > 0)
|
|
360
|
+
tokens.push(sep());
|
|
361
|
+
tokens.push({ text: label + ' ', tone: 'label' }, { text: value, tone: 'value' });
|
|
362
|
+
};
|
|
363
|
+
pair(t('status.label.in'), formatTokens(stats.usage.uncachedInputTokens));
|
|
364
|
+
pair(t('status.label.out'), formatTokens(stats.usage.outputTokens));
|
|
365
|
+
row2.push({ group: { spans: tokens }, rank: RANK_TOKENS, id: 'tokens' });
|
|
366
|
+
}
|
|
367
|
+
// dscode: the session title moved to row 1's identity lead (dscodeStatusLead),
|
|
368
|
+
// so row 2 no longer carries it.
|
|
369
|
+
// Secondary state rides row 2; permission alone remains right-pinned on row 1.
|
|
370
|
+
if (facts.goal !== undefined && enabled.has('goal')) {
|
|
371
|
+
row2.push({
|
|
372
|
+
group: {
|
|
373
|
+
spans: [{
|
|
374
|
+
text: facts.goal.phase === 'active'
|
|
375
|
+
? '◎ round ' + facts.goal.rounds + '/' + facts.goal.max
|
|
376
|
+
: '◎ ' + safe(facts.goal.phase),
|
|
377
|
+
tone: 'accent',
|
|
378
|
+
}],
|
|
379
|
+
},
|
|
380
|
+
rank: RANK_GOAL,
|
|
381
|
+
id: 'goal',
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
// The sandbox override stays implicit when it merely echoes the preset.
|
|
385
|
+
const sandbox = safe(facts.sandbox ?? '');
|
|
386
|
+
if (sandbox !== '' && sandbox.toLowerCase() !== facts.permission.toLowerCase() && enabled.has('sandbox')) {
|
|
387
|
+
row2.push({ group: { spans: [{ text: 'sandbox ' + sandbox, tone: 'warn' }] }, rank: RANK_SANDBOX, id: 'sandbox' });
|
|
388
|
+
}
|
|
389
|
+
const permission = safe(facts.permission);
|
|
390
|
+
let badge = -1;
|
|
391
|
+
// The plan STATION names itself in the permission badge: with the most
|
|
392
|
+
// restrictive preset active, plan mode reads as the green fourth cycle
|
|
393
|
+
// station 'plan' (that preset IS the station's permission layer). Plan on
|
|
394
|
+
// any other preset (a typed /plan mid-session) stays orthogonal: the badge
|
|
395
|
+
// keeps naming the preset and row 2 carries the green plan marker.
|
|
396
|
+
const planStation = facts.plan && permissionTone(permission) === 'success';
|
|
397
|
+
// dscode: the permission badge rides row 1 beside the title rather than
|
|
398
|
+
// right-pinning itself on the identity row.
|
|
399
|
+
if (permission !== '' && enabled.has('permission')) {
|
|
400
|
+
left.push({
|
|
401
|
+
group: {
|
|
402
|
+
spans: planStation
|
|
403
|
+
? [{ text: 'plan on', tone: 'plan' }]
|
|
404
|
+
: [{ text: permission, tone: permissionTone(permission) }],
|
|
405
|
+
},
|
|
406
|
+
rank: RANK_BADGE,
|
|
407
|
+
id: 'permission',
|
|
408
|
+
});
|
|
409
|
+
badge = left.length - 1;
|
|
410
|
+
}
|
|
411
|
+
if (facts.plan && enabled.has('plan')) {
|
|
412
|
+
row2.push({ group: { spans: [{ text: '⧉ plan', tone: 'accent' }] }, rank: RANK2_PLAN, id: 'plan' });
|
|
413
|
+
}
|
|
414
|
+
return { left, right, badge, row2 };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Compose the two-row footer layout under a column budget. Row 1 keeps model,
|
|
418
|
+
* cwd, mode, branch, context, then the right-pinned permission badge and cycle
|
|
419
|
+
* hint. It drops hint, context, and permission before ellipsizing identity.
|
|
420
|
+
* Row 2 fits all secondary figures and state within its own budget.
|
|
421
|
+
* @param facts - identity facts resolved by the runner.
|
|
422
|
+
* @param stats - session figures folded from the durable log.
|
|
423
|
+
* @param columns - usable columns for each row (before their left padding).
|
|
424
|
+
* @param options - 'busy' hides the cycle hint while a turn runs (Codex
|
|
425
|
+
* keeps mode hints idle-only); 'items' is the ordered enabled-item config
|
|
426
|
+
* from /statusline (defaults to the full catalog). Display order follows the
|
|
427
|
+
* config per side while the drop ladder keeps its fixed ranks.
|
|
428
|
+
* @returns the two rows to render; row1.left is never empty.
|
|
429
|
+
*/
|
|
430
|
+
export function layoutStatusBar(facts, stats, columns, options = {}) {
|
|
431
|
+
const busy = options.busy === true;
|
|
432
|
+
const items = options.items ?? DEFAULT_STATUSLINE_ITEMS;
|
|
433
|
+
const enabled = new Set(items);
|
|
434
|
+
const budget = Math.max(1, Math.floor(columns) - WIDTH_SAFETY);
|
|
435
|
+
// The context meter shares the same measured width as the composer. Its
|
|
436
|
+
// initial width is a ceiling only; the drop ladder below shrinks it around
|
|
437
|
+
// the other status groups before dropping any group.
|
|
438
|
+
const maxContextWidth = Math.max(CONTEXT_MIN_WIDTH, Math.min(budget, Math.floor(options.contextWidth ?? CONTEXT_BAR_WIDTH)));
|
|
439
|
+
const { left, right, badge, row2 } = buildCandidates(facts, stats, busy, enabled, maxContextWidth);
|
|
440
|
+
const groupSeparator = visibleColumns(STATUS_GROUP_SEPARATOR);
|
|
441
|
+
const itemSeparator = visibleColumns(STATUS_ITEM_SEPARATOR);
|
|
442
|
+
// Display order follows the config per side (Codex /statusline reorder).
|
|
443
|
+
// The identity cluster stays anchored first — it owns the busy dot.
|
|
444
|
+
const position = new Map(items.map((id, index) => [id, index]));
|
|
445
|
+
const byPosition = (a, b) => (position.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (position.get(b.id) ?? Number.MAX_SAFE_INTEGER);
|
|
446
|
+
const orderedLeft = [left[0], ...left.slice(1).sort(byPosition)];
|
|
447
|
+
const orderedRight = right.slice().sort(byPosition);
|
|
448
|
+
const orderedRow2 = row2.slice().sort(byPosition);
|
|
449
|
+
let hint = badge >= 0 && !busy;
|
|
450
|
+
const leftKept = [...orderedLeft];
|
|
451
|
+
const rightKept = [...orderedRight];
|
|
452
|
+
// Context degradation state: the readout drops its absolute pair first,
|
|
453
|
+
// then the bar shrinks inside its own budget, and only then is the whole
|
|
454
|
+
// group removed — the proportional meter outlives the auxiliary numbers.
|
|
455
|
+
// Rebuilding replaces the group's spans in place so width() re-measures it.
|
|
456
|
+
let contextReadout = 'full';
|
|
457
|
+
let contextWidth = maxContextWidth;
|
|
458
|
+
const rebuildContext = () => {
|
|
459
|
+
const index = leftKept.findIndex(entry => entry.id === 'context');
|
|
460
|
+
if (index < 0)
|
|
461
|
+
return;
|
|
462
|
+
leftKept[index] = {
|
|
463
|
+
group: {
|
|
464
|
+
spans: contextGroupSpans(stats.lastPromptTokens, stats.contextWindow, contextWidth, contextReadout),
|
|
465
|
+
},
|
|
466
|
+
rank: RANK_CONTEXT,
|
|
467
|
+
id: 'context',
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
const width = () => {
|
|
471
|
+
const leftWidth = joinWidth(leftKept.map(entry => spansWidth(entry.group.spans)), groupSeparator);
|
|
472
|
+
const rightWidth = joinWidth(rightKept.map(entry => visibleColumns(entry.span.text)), itemSeparator)
|
|
473
|
+
+ (hint ? visibleColumns(statusCycleHint()) : 0);
|
|
474
|
+
return rightWidth > 0 ? leftWidth + LEFT_RIGHT_GAP + rightWidth : leftWidth;
|
|
475
|
+
};
|
|
476
|
+
while (width() > budget) {
|
|
477
|
+
// Context is the lowest-priority visual group: the bar shrinks inside
|
|
478
|
+
// its own budget first (the absolute readout survives), then the
|
|
479
|
+
// readout degrades to the bare percent, and only then does the whole
|
|
480
|
+
// group go — before the permission badge or its Shift+Tab affordance
|
|
481
|
+
// is touched.
|
|
482
|
+
if (leftKept.some(entry => entry.id === 'context')) {
|
|
483
|
+
if (contextWidth > CONTEXT_MIN_WIDTH) {
|
|
484
|
+
const overflow = width() - budget;
|
|
485
|
+
contextWidth = Math.max(CONTEXT_MIN_WIDTH, contextWidth - overflow);
|
|
486
|
+
rebuildContext();
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (contextReadout === 'full') {
|
|
490
|
+
contextReadout = 'percent';
|
|
491
|
+
rebuildContext();
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
leftKept.splice(leftKept.findIndex(entry => entry.id === 'context'), 1);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (hint && rightKept.length > 0 && leftKept.length > 0) {
|
|
498
|
+
const identity = leftKept[0];
|
|
499
|
+
const identityText = identity.group.spans.map(span => span.text).join('');
|
|
500
|
+
const rightWidth = joinWidth(rightKept.map(entry => visibleColumns(entry.span.text)), itemSeparator);
|
|
501
|
+
const identityBudget = budget - rightWidth - LEFT_RIGHT_GAP - visibleColumns(statusCycleHint());
|
|
502
|
+
if (identityBudget > 0 && visibleColumns(identityText) > identityBudget) {
|
|
503
|
+
leftKept[0] = {
|
|
504
|
+
...identity,
|
|
505
|
+
group: { spans: [{ text: truncateColumns(identityText, identityBudget), tone: 'model' }] },
|
|
506
|
+
};
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (hint) {
|
|
511
|
+
hint = false;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
let dropLeft = -1;
|
|
515
|
+
let dropRight = -1;
|
|
516
|
+
let dropRank = Number.POSITIVE_INFINITY;
|
|
517
|
+
for (let index = 0; index < leftKept.length; index += 1) {
|
|
518
|
+
const rank = leftKept[index].rank;
|
|
519
|
+
if (rank < dropRank) {
|
|
520
|
+
dropRank = rank;
|
|
521
|
+
dropLeft = index;
|
|
522
|
+
dropRight = -1;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
for (let index = 0; index < rightKept.length; index += 1) {
|
|
526
|
+
const rank = rightKept[index].rank;
|
|
527
|
+
if (rank < dropRank) {
|
|
528
|
+
dropRank = rank;
|
|
529
|
+
dropRight = index;
|
|
530
|
+
dropLeft = -1;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (dropLeft < 0 && dropRight < 0)
|
|
534
|
+
break;
|
|
535
|
+
if (dropLeft >= 0) {
|
|
536
|
+
leftKept.splice(dropLeft, 1);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
rightKept.splice(dropRight, 1);
|
|
540
|
+
if (dropRight === rightKept.length)
|
|
541
|
+
hint = false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Only the identity cluster can remain overflowing: collapse to it and
|
|
545
|
+
// ellipsize inside the budget as the last resort. Flat spans make the
|
|
546
|
+
// joined text identical to what the row would have displayed.
|
|
547
|
+
if (width() > budget) {
|
|
548
|
+
rightKept.length = 0;
|
|
549
|
+
hint = false;
|
|
550
|
+
while (leftKept.length > 1)
|
|
551
|
+
leftKept.pop();
|
|
552
|
+
const identity = leftKept[0].group;
|
|
553
|
+
const joined = identity.spans.map(span => span.text).join('');
|
|
554
|
+
leftKept[0] = {
|
|
555
|
+
group: { spans: [{ text: truncateColumns(joined, budget), tone: 'model' }] },
|
|
556
|
+
rank: RANK_IDENTITY,
|
|
557
|
+
id: 'identity',
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
// Row 2 fits its own budget minus the model-name indent; the lowest-rank
|
|
561
|
+
// group drops first until the row fits or nothing is left. An empty row2 is
|
|
562
|
+
// a valid state — the footer degrades back to a single status row.
|
|
563
|
+
const row2Kept = [...orderedRow2];
|
|
564
|
+
// dscode: the telemetry segment owns row 2's right edge, so its width comes out
|
|
565
|
+
// of the left groups' budget first.
|
|
566
|
+
const row2Budget = Math.max(0, budget - 1 - (facts.telemetry ? visibleColumns(facts.telemetry) + 3 : 0));
|
|
567
|
+
const row2Width = () => joinWidth(row2Kept.map(entry => spansWidth(entry.group.spans)), groupSeparator);
|
|
568
|
+
while (row2Width() > row2Budget && row2Kept.length > 0) {
|
|
569
|
+
let dropIndex = 0;
|
|
570
|
+
let dropRank = Number.POSITIVE_INFINITY;
|
|
571
|
+
for (let index = 0; index < row2Kept.length; index += 1) {
|
|
572
|
+
if (row2Kept[index].rank < dropRank) {
|
|
573
|
+
dropRank = row2Kept[index].rank;
|
|
574
|
+
dropIndex = index;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
row2Kept.splice(dropIndex, 1);
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
row1: {
|
|
581
|
+
left: leftKept.map(entry => entry.group),
|
|
582
|
+
right: rightKept.map(entry => entry.span),
|
|
583
|
+
hint,
|
|
584
|
+
},
|
|
585
|
+
row2: {
|
|
586
|
+
left: row2Kept.map(entry => entry.group),
|
|
587
|
+
right: facts.telemetry ? [{ text: facts.telemetry, tone: 'meta' }] : [],
|
|
588
|
+
hint: false,
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
}
|