@toddzheng024/dscode-bundle 0.7.12 → 0.7.13

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.
Files changed (152) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/cordis.patch.yml +1 -1
  3. package/package.json +5 -4
  4. package/plugins/session-metrics/view.mjs +10 -2
  5. package/vendor/tui/lib/app.mjs +6674 -0
  6. package/vendor/tui/lib/approval.mjs +122 -0
  7. package/vendor/tui/lib/attachments.mjs +218 -0
  8. package/vendor/tui/lib/authorization-panel.mjs +242 -0
  9. package/vendor/tui/lib/authorization.mjs +111 -0
  10. package/vendor/tui/lib/commands.mjs +123 -0
  11. package/vendor/tui/lib/dscode/chat.mjs +64 -0
  12. package/vendor/tui/lib/dscode/flags.mjs +32 -0
  13. package/vendor/tui/lib/dscode/model-search.mjs +68 -0
  14. package/vendor/tui/lib/dscode/paste.mjs +87 -0
  15. package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
  16. package/vendor/tui/lib/dscode/welcome.mjs +100 -0
  17. package/vendor/tui/lib/editor-keys.mjs +346 -0
  18. package/vendor/tui/lib/editor.mjs +50 -0
  19. package/vendor/tui/lib/fork.mjs +24 -0
  20. package/vendor/tui/lib/git-workflow.mjs +278 -0
  21. package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
  22. package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
  23. package/vendor/tui/lib/index.mjs +2198 -0
  24. package/vendor/tui/lib/input-split.mjs +170 -0
  25. package/vendor/tui/lib/internals.mjs +90 -0
  26. package/vendor/tui/lib/invariant.mjs +22 -0
  27. package/vendor/tui/lib/kernel-panels.mjs +1265 -0
  28. package/vendor/tui/lib/keyboard.mjs +283 -0
  29. package/vendor/tui/lib/language-panel.mjs +41 -0
  30. package/vendor/tui/lib/locales/en.mjs +495 -0
  31. package/vendor/tui/lib/locales/zh.mjs +495 -0
  32. package/vendor/tui/lib/mentions.mjs +150 -0
  33. package/vendor/tui/lib/model-capabilities.mjs +245 -0
  34. package/vendor/tui/lib/models.mjs +215 -0
  35. package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
  36. package/vendor/tui/lib/permissions.mjs +45 -0
  37. package/vendor/tui/lib/plugin-inventory.mjs +27 -0
  38. package/vendor/tui/lib/presets.mjs +46 -0
  39. package/vendor/tui/lib/provider-settings.mjs +600 -0
  40. package/vendor/tui/lib/questions.mjs +116 -0
  41. package/vendor/tui/lib/rainbow.mjs +181 -0
  42. package/vendor/tui/lib/render/animations.mjs +665 -0
  43. package/vendor/tui/lib/render/editor.mjs +437 -0
  44. package/vendor/tui/lib/render/export.mjs +119 -0
  45. package/vendor/tui/lib/render/fuzzy.mjs +79 -0
  46. package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
  47. package/vendor/tui/lib/render/inspector.mjs +93 -0
  48. package/vendor/tui/lib/render/lines.mjs +538 -0
  49. package/vendor/tui/lib/render/markdown.mjs +592 -0
  50. package/vendor/tui/lib/render/projection.mjs +1862 -0
  51. package/vendor/tui/lib/render/status.mjs +591 -0
  52. package/vendor/tui/lib/render/text.mjs +157 -0
  53. package/vendor/tui/lib/render/tool-detail.mjs +187 -0
  54. package/vendor/tui/lib/render/tool-preview.mjs +79 -0
  55. package/vendor/tui/lib/render/usage.mjs +336 -0
  56. package/vendor/tui/lib/render/width.mjs +191 -0
  57. package/vendor/tui/lib/session-directory.mjs +329 -0
  58. package/vendor/tui/lib/session-query.mjs +184 -0
  59. package/vendor/tui/lib/session-switch.mjs +51 -0
  60. package/vendor/tui/lib/settings-file.mjs +75 -0
  61. package/vendor/tui/lib/skills.mjs +119 -0
  62. package/vendor/tui/lib/startup.mjs +111 -0
  63. package/vendor/tui/lib/store.mjs +129 -0
  64. package/vendor/tui/lib/subagents.mjs +200 -0
  65. package/vendor/tui/lib/terminal-title.mjs +186 -0
  66. package/vendor/tui/lib/theme-panel.mjs +60 -0
  67. package/vendor/tui/lib/theme.mjs +379 -0
  68. package/vendor/tui/lib/update-panel.mjs +239 -0
  69. package/vendor/tui/lib/update.mjs +101 -0
  70. package/vendor/tui/lib/version.mjs +105 -0
  71. package/vendor/tui/lib/whale-glyph.mjs +20 -0
  72. package/vendor/tui/LICENSE +0 -21
  73. package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
  74. package/vendor/tui/dscode-email/cli.mjs +0 -32
  75. package/vendor/tui/dscode-email/contacts.mjs +0 -36
  76. package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
  77. package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
  78. package/vendor/tui/dscode-email/gmail.mjs +0 -194
  79. package/vendor/tui/dscode-email/imap.mjs +0 -136
  80. package/vendor/tui/dscode-email/inbox.d.mts +0 -25
  81. package/vendor/tui/dscode-email/inbox.mjs +0 -76
  82. package/vendor/tui/dscode-email/smtp.mjs +0 -69
  83. package/vendor/tui/dscode-email/store.mjs +0 -32
  84. package/vendor/tui/dscode-providers/catalog.mjs +0 -136
  85. package/vendor/tui/dscode-providers/effort.mjs +0 -35
  86. package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
  87. package/vendor/tui/index.mjs +0 -44441
  88. package/vendor/tui/invariant.mjs +0 -21
  89. package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
  90. package/vendor/tui/session-query.mjs +0 -150
  91. package/vendor/tui/startup.mjs +0 -109
  92. package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
  93. package/vendor/tui/types/app.d.ts +0 -348
  94. package/vendor/tui/types/approval.d.ts +0 -59
  95. package/vendor/tui/types/attachments.d.ts +0 -52
  96. package/vendor/tui/types/authorization-panel.d.ts +0 -22
  97. package/vendor/tui/types/authorization.d.ts +0 -36
  98. package/vendor/tui/types/commands.d.ts +0 -52
  99. package/vendor/tui/types/editor-keys.d.ts +0 -105
  100. package/vendor/tui/types/editor.d.ts +0 -6
  101. package/vendor/tui/types/fork.d.ts +0 -8
  102. package/vendor/tui/types/git-workflow.d.ts +0 -121
  103. package/vendor/tui/types/index.d.ts +0 -223
  104. package/vendor/tui/types/input-split.d.ts +0 -54
  105. package/vendor/tui/types/internals.d.ts +0 -26
  106. package/vendor/tui/types/invariant.d.ts +0 -15
  107. package/vendor/tui/types/kernel-panels.d.ts +0 -245
  108. package/vendor/tui/types/keyboard.d.ts +0 -80
  109. package/vendor/tui/types/language-panel.d.ts +0 -12
  110. package/vendor/tui/types/locales/en.d.ts +0 -450
  111. package/vendor/tui/types/locales/zh.d.ts +0 -9
  112. package/vendor/tui/types/mentions.d.ts +0 -85
  113. package/vendor/tui/types/model-capabilities.d.ts +0 -82
  114. package/vendor/tui/types/models.d.ts +0 -133
  115. package/vendor/tui/types/permissions.d.ts +0 -27
  116. package/vendor/tui/types/plugin-inventory.d.ts +0 -11
  117. package/vendor/tui/types/presets.d.ts +0 -22
  118. package/vendor/tui/types/provider-settings.d.ts +0 -239
  119. package/vendor/tui/types/questions.d.ts +0 -54
  120. package/vendor/tui/types/rainbow.d.ts +0 -69
  121. package/vendor/tui/types/render/animations.d.ts +0 -307
  122. package/vendor/tui/types/render/editor.d.ts +0 -163
  123. package/vendor/tui/types/render/export.d.ts +0 -9
  124. package/vendor/tui/types/render/fuzzy.d.ts +0 -21
  125. package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
  126. package/vendor/tui/types/render/inspector.d.ts +0 -62
  127. package/vendor/tui/types/render/lines.d.ts +0 -86
  128. package/vendor/tui/types/render/markdown.d.ts +0 -29
  129. package/vendor/tui/types/render/projection.d.ts +0 -587
  130. package/vendor/tui/types/render/status.d.ts +0 -196
  131. package/vendor/tui/types/render/text.d.ts +0 -64
  132. package/vendor/tui/types/render/tool-detail.d.ts +0 -94
  133. package/vendor/tui/types/render/tool-preview.d.ts +0 -28
  134. package/vendor/tui/types/render/usage.d.ts +0 -113
  135. package/vendor/tui/types/render/width.d.ts +0 -29
  136. package/vendor/tui/types/session-directory.d.ts +0 -190
  137. package/vendor/tui/types/session-query.d.ts +0 -92
  138. package/vendor/tui/types/session-switch.d.ts +0 -17
  139. package/vendor/tui/types/settings-file.d.ts +0 -41
  140. package/vendor/tui/types/skills.d.ts +0 -47
  141. package/vendor/tui/types/startup.d.ts +0 -65
  142. package/vendor/tui/types/store.d.ts +0 -58
  143. package/vendor/tui/types/subagents.d.ts +0 -70
  144. package/vendor/tui/types/terminal-title.d.ts +0 -66
  145. package/vendor/tui/types/theme-panel.d.ts +0 -24
  146. package/vendor/tui/types/theme.d.ts +0 -434
  147. package/vendor/tui/types/update-panel.d.ts +0 -49
  148. package/vendor/tui/types/update.d.ts +0 -75
  149. package/vendor/tui/types/version.d.ts +0 -19
  150. package/vendor/tui/types/whale-glyph.d.ts +0 -6
  151. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
  152. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Session usage for the /usage panel: the provider-reported token totals, the
3
+ * context pressure and composition, and the exact per-turn accounting, folded
4
+ * into the styled rows the panel draws.
5
+ *
6
+ * Every figure comes from the harness token meter. The four token buckets are
7
+ * disjoint, so the prompt side is never double counted; the composition block
8
+ * is a density estimate and is labelled as one.
9
+ *
10
+ * @module @deepseek-ai/dsh-tui/render/usage
11
+ */
12
+ import { t } from '../i18n.mjs';
13
+ import { lineSegment } from './lines.mjs';
14
+ import { visibleColumns } from './markdown.mjs';
15
+ import { formatTokens } from './status.mjs';
16
+ import { truncateColumns } from './text.mjs';
17
+ /** Prompt-side tokens the provider billed: uncached input plus both cache buckets. */
18
+ export function billedInputTokens(totals) {
19
+ return totals.uncachedInputTokens + totals.cacheReadTokens + totals.cacheWriteTokens;
20
+ }
21
+ /** Prompt plus completion tokens over the whole log. */
22
+ export function usageTotalTokens(totals) {
23
+ return billedInputTokens(totals) + totals.outputTokens;
24
+ }
25
+ /**
26
+ * Cache-hit share of billed prompt-side input.
27
+ * @param totals - cumulative provider-reported buckets.
28
+ * @returns percent rounded to one decimal place, or null when nothing was billed.
29
+ */
30
+ export function usageCacheHitPercent(totals) {
31
+ const billed = billedInputTokens(totals);
32
+ return billed === 0 ? null : Math.round(totals.cacheReadTokens / billed * 1_000) / 10;
33
+ }
34
+ /**
35
+ * Split a durable log into COMPLETE turns. A turn still running has no
36
+ * `turn/end` yet, and an unfinished attempt has no exact accounting, so the
37
+ * trailing slice is dropped rather than guessed at.
38
+ * @param events - the whole durable log, in log order.
39
+ * @returns one slice per `turn/start`…`turn/end` span, oldest first.
40
+ */
41
+ export function completedTurns(events) {
42
+ const slices = [];
43
+ let open;
44
+ for (const event of events) {
45
+ if (event.type === 'turn/start') {
46
+ open = { turn: event.data.turn, events: [event] };
47
+ continue;
48
+ }
49
+ if (open === undefined)
50
+ continue;
51
+ open.events.push(event);
52
+ if (event.type === 'turn/end') {
53
+ slices.push(open);
54
+ open = undefined;
55
+ }
56
+ }
57
+ return slices;
58
+ }
59
+ /**
60
+ * Exact usage for every completed turn that can be proven.
61
+ * @param events - the whole durable log, in log order.
62
+ * @param derive - the meter's per-turn fold, injected so this module stays pure.
63
+ * @returns one row per provable turn, oldest first; turns whose attempts did
64
+ * not all report usage are omitted rather than estimated, and so is a turn
65
+ * that billed nothing at all — an empty row is noise in a usage table.
66
+ */
67
+ export function turnUsages(events, derive) {
68
+ const rows = [];
69
+ for (const slice of completedTurns(events)) {
70
+ const usage = derive(slice.events);
71
+ if (usage === undefined || usage.totalTokens === 0)
72
+ continue;
73
+ rows.push({ turn: slice.turn, usage, model: turnModel(slice, usage) });
74
+ }
75
+ return rows;
76
+ }
77
+ /** Pad to a column budget so labels line up across terminal widths. */
78
+ function padColumns(text, columns) {
79
+ return text + ' '.repeat(Math.max(0, columns - visibleColumns(text)));
80
+ }
81
+ /** One `label` + `value` row of a summary block. */
82
+ function pairLine(label, value, labelColumns, style = 'plain') {
83
+ const gap = ' '.repeat(Math.max(1, labelColumns - visibleColumns(label)));
84
+ return { segments: [lineSegment(' ' + label + gap, 'dim'), lineSegment(value, style)] };
85
+ }
86
+ /**
87
+ * Merge the per-turn rows by the model that billed them, biggest spender
88
+ * first. A turn lands in exactly one group — the group named by every model it
89
+ * used — so the sums stay additive and a mid-turn model switch never counts
90
+ * the same tokens twice. Each bucket is merged over the turns that reported
91
+ * it, and {@link ModelUsage.partial} records the ones that stayed silent.
92
+ * @param turns - provable per-turn rows, oldest first.
93
+ * @returns one row per model group.
94
+ */
95
+ export function modelTotals(turns) {
96
+ const groups = new Map();
97
+ for (const row of turns) {
98
+ groups.set(row.model, [...(groups.get(row.model) ?? []), row]);
99
+ }
100
+ const merged = [];
101
+ for (const [model, rows] of groups) {
102
+ const partial = [];
103
+ // A bucket is summed over the turns that reported it. Refusing the whole
104
+ // group because ONE turn stayed silent would contradict the per-turn table
105
+ // right below, where those same turns show real cache reads; the floor is
106
+ // marked instead of thrown away.
107
+ const sum = (key) => {
108
+ const reported = rows.flatMap(row => row.usage[key] === undefined ? [] : [row.usage[key]]);
109
+ if (reported.length === 0)
110
+ return undefined;
111
+ if (reported.length < rows.length)
112
+ partial.push(key);
113
+ return reported.reduce((total, value) => total + value, 0);
114
+ };
115
+ const cacheReadTokens = sum('cacheReadTokens');
116
+ const cacheWriteTokens = sum('cacheWriteTokens');
117
+ const reasoningTokens = sum('reasoningTokens');
118
+ merged.push({
119
+ model,
120
+ turns: rows.length,
121
+ uncachedInputTokens: rows.reduce((sum, row) => sum + row.usage.uncachedInputTokens, 0),
122
+ outputTokens: rows.reduce((sum, row) => sum + row.usage.outputTokens, 0),
123
+ totalTokens: rows.reduce((sum, row) => sum + row.usage.totalTokens, 0),
124
+ cacheReadTokens,
125
+ cacheWriteTokens,
126
+ reasoningTokens,
127
+ partial,
128
+ });
129
+ }
130
+ // Biggest spender first; turns nothing could attribute stay at the bottom,
131
+ // because that row is a gap in the record rather than a model.
132
+ return merged.sort((left, right) => (left.model === '' ? 1 : right.model === '' ? -1 : right.totalTokens - left.totalTokens));
133
+ }
134
+ /** Cut one styled line to a column budget, keeping every style it fits. */
135
+ function boundLine(line, width) {
136
+ const segments = [];
137
+ let left = width;
138
+ for (const segment of line.segments) {
139
+ if (left <= 0)
140
+ break;
141
+ const columns = visibleColumns(segment.text);
142
+ if (columns <= left) {
143
+ segments.push(segment);
144
+ left -= columns;
145
+ continue;
146
+ }
147
+ segments.push(lineSegment(truncateColumns(segment.text, left), segment.style));
148
+ left = 0;
149
+ }
150
+ return { segments };
151
+ }
152
+ /** A block heading. */
153
+ function heading(text) {
154
+ return { segments: [lineSegment(text, 'accentBold')] };
155
+ }
156
+ /** A dim explanatory or empty-state row. */
157
+ function note(text) {
158
+ return { segments: [lineSegment(' ' + text, 'dim')] };
159
+ }
160
+ /**
161
+ * Cache-hit share of one row's billed prompt side, or undefined when the row
162
+ * has no cache read to report. An unreported cache-write bucket counts as
163
+ * zero: the adapters that bill cache writes always report the bucket, so its
164
+ * absence means the route never tracked one — the same reading the status
165
+ * bar's own fold uses.
166
+ */
167
+ function hitShare(row) {
168
+ if (row.cacheReadTokens === undefined)
169
+ return undefined;
170
+ const billed = row.uncachedInputTokens + row.cacheReadTokens + (row.cacheWriteTokens ?? 0);
171
+ if (billed === 0)
172
+ return undefined;
173
+ return Math.round(row.cacheReadTokens / billed * 1_000) / 10;
174
+ }
175
+ /** The token columns, in display order (shared by both tables). */
176
+ /** True when the row's bucket is a floor rather than a complete figure. */
177
+ function isPartial(row, key) {
178
+ return row.partial?.includes(key) === true;
179
+ }
180
+ const BUCKET_COLUMNS = [
181
+ { label: () => t('panel.usage.colTotal'), value: row => formatTokens(row.totalTokens), width: 9 },
182
+ { label: () => t('panel.usage.colUncached'), value: row => formatTokens(row.uncachedInputTokens), width: 9 },
183
+ { label: () => t('panel.usage.colCacheRead'), value: row => optionalTokens(row.cacheReadTokens, isPartial(row, 'cacheReadTokens')), width: 9 },
184
+ {
185
+ label: () => t('panel.usage.colCacheHit'),
186
+ value: row => percent(hitShare(row), isPartial(row, 'cacheReadTokens') || isPartial(row, 'cacheWriteTokens')),
187
+ width: 8,
188
+ },
189
+ { label: () => t('panel.usage.colCacheWrite'), value: row => optionalTokens(row.cacheWriteTokens, isPartial(row, 'cacheWriteTokens')), width: 9 },
190
+ { label: () => t('panel.usage.colOut'), value: row => formatTokens(row.outputTokens), width: 9 },
191
+ { label: () => t('panel.usage.colThink'), value: row => optionalTokens(row.reasoningTokens, isPartial(row, 'reasoningTokens')), width: 9 },
192
+ ];
193
+ /** A percentage, or an em dash when the row cannot prove one; `+` for a floor. */
194
+ function percent(value, partial = false) {
195
+ return value === undefined ? '—' : value + '%' + (partial ? '+' : '');
196
+ }
197
+ /**
198
+ * A bucket as a table cell: an em dash when nothing reported it, and a `+`
199
+ * when the figure counts only the turns of a group that did report it.
200
+ */
201
+ function optionalTokens(value, partial = false) {
202
+ if (value === undefined)
203
+ return '—';
204
+ return formatTokens(value) + (partial ? '+' : '');
205
+ }
206
+ /** What an unattributed turn or group is called in the tables. */
207
+ function modelLabel(model) {
208
+ return model === '' ? t('panel.usage.unknownModel') : model;
209
+ }
210
+ /**
211
+ * The model a turn's assistant messages came from: the one that produced most
212
+ * of them, with a tie naming every model involved. This is the fallback for
213
+ * turns whose meter `routes` were withheld, and it reads only what the log
214
+ * recorded — nothing is inferred from the current selection.
215
+ */
216
+ function dominantModel(events) {
217
+ const counts = new Map();
218
+ for (const event of events) {
219
+ if (event.type !== 'assistant/message')
220
+ continue;
221
+ const source = event.data.message.source;
222
+ if (source.kind !== 'model' || source.model === '')
223
+ continue;
224
+ counts.set(source.model, (counts.get(source.model) ?? 0) + 1);
225
+ }
226
+ if (counts.size === 0)
227
+ return '';
228
+ const most = Math.max(...counts.values());
229
+ return [...counts.entries()]
230
+ .filter(([, count]) => count === most)
231
+ .map(([model]) => model)
232
+ .sort()
233
+ .join(' + ');
234
+ }
235
+ /** The model attribution for one turn: the meter's routes, else its messages. */
236
+ export function turnModel(slice, usage) {
237
+ const routes = usage.routes ?? [];
238
+ if (routes.length > 0)
239
+ return routes.map(route => route.model).join(' + ');
240
+ return dominantModel(slice.events);
241
+ }
242
+ /**
243
+ * Render one table: a dim header plus one row per entry. The free-text cell
244
+ * (the model name) keeps a readable share of the width, so trailing numeric
245
+ * columns drop first on a narrow terminal instead of leaving an ellipsised
246
+ * model name — the name is what the reader came for.
247
+ * @param columns - the numeric columns, most important first.
248
+ * @param rows - one entry per line, already in display order.
249
+ * @param text - how to read the free-text cell of one entry.
250
+ * @param width - the column budget.
251
+ * @param textFirst - true puts the free-text cell before the numbers (the
252
+ * model-group table); false appends it (the per-turn table).
253
+ * @returns the header row plus one row per entry.
254
+ */
255
+ function table(columns, rows, text, width, textFirst) {
256
+ const budget = width - 2;
257
+ const minText = 14;
258
+ const kept = [];
259
+ let used = 0;
260
+ for (const column of columns) {
261
+ if (used + column.width > budget - minText - 1)
262
+ break;
263
+ kept.push(column);
264
+ used += column.width;
265
+ }
266
+ // Capped so a wide terminal does not stretch one column across the panel.
267
+ const textWidth = Math.min(24, Math.max(6, budget - used - 1));
268
+ const numbers = (row) => kept.map(column => padColumns(column.value(row), column.width)).join('');
269
+ const header = kept.map(column => padColumns(column.label(), column.width)).join('');
270
+ const headerText = textFirst
271
+ ? padColumns(t('panel.usage.colModel'), textWidth) + ' ' + header
272
+ : header + ' ' + t('panel.usage.colModel');
273
+ const lines = [
274
+ { segments: [lineSegment(truncateColumns(' ' + headerText, width), 'dim')] },
275
+ ];
276
+ for (const row of rows) {
277
+ const label = truncateColumns(text(row), textWidth);
278
+ lines.push({
279
+ segments: [lineSegment(' ' + (textFirst ? padColumns(label, textWidth) + ' ' + numbers(row) : numbers(row) + ' ' + label), 'plain')],
280
+ });
281
+ }
282
+ return lines;
283
+ }
284
+ /**
285
+ * Render the panel body.
286
+ * @param view - the projection totals plus the derived per-turn rows.
287
+ * @param columns - usable content columns inside the panel border.
288
+ * @returns bounded, styled rows ready to draw.
289
+ */
290
+ export function usageLines(view, columns) {
291
+ const width = Math.max(8, Math.floor(columns));
292
+ const lines = [];
293
+ lines.push(heading(t('panel.usage.totals')));
294
+ const totals = view.totals;
295
+ if (totals === undefined) {
296
+ lines.push(note(t('panel.usage.unavailable')));
297
+ }
298
+ else {
299
+ const label = 12;
300
+ lines.push(pairLine(t('panel.usage.uncached'), formatTokens(totals.uncachedInputTokens), label));
301
+ lines.push(pairLine(t('panel.usage.cacheWrite'), formatTokens(totals.cacheWriteTokens), label));
302
+ lines.push(pairLine(t('panel.usage.cacheRead'), formatTokens(totals.cacheReadTokens), label));
303
+ lines.push(pairLine(t('panel.usage.output'), formatTokens(totals.outputTokens), label));
304
+ lines.push(pairLine(t('panel.usage.total'), formatTokens(usageTotalTokens(totals)), label, 'bold'));
305
+ const hit = usageCacheHitPercent(totals);
306
+ if (hit !== null)
307
+ lines.push(pairLine(t('panel.usage.cacheHit'), hit + '%', label));
308
+ }
309
+ lines.push({ segments: [] });
310
+ const models = modelTotals(view.turns);
311
+ lines.push(heading(models.length === 0 ? t('panel.usage.byModel') : `${t('panel.usage.byModel')} · ${models.length}`));
312
+ if (models.length === 0) {
313
+ lines.push(note(t('panel.usage.noTurns')));
314
+ }
315
+ else {
316
+ lines.push(...table([
317
+ { label: () => t('panel.usage.colTurns'), value: (row) => String(row.turns), width: 7 },
318
+ ...BUCKET_COLUMNS.map(column => ({ ...column, value: (row) => column.value(row) })),
319
+ ], models, row => modelLabel(row.model), width, true));
320
+ if (models.some(row => row.partial.length > 0))
321
+ lines.push(note(t('panel.usage.partialNote')));
322
+ }
323
+ lines.push({ segments: [] });
324
+ lines.push(heading(`${t('panel.usage.turns')} · ${view.turns.length}`));
325
+ if (view.turns.length === 0) {
326
+ lines.push(note(t('panel.usage.noTurns')));
327
+ }
328
+ else {
329
+ const withTurn = [
330
+ { label: () => t('panel.usage.colTurn'), value: (row) => `#${row.turn}`, width: 7 },
331
+ ...BUCKET_COLUMNS.map(column => ({ ...column, value: (row) => column.value(row.usage) })),
332
+ ];
333
+ lines.push(...table(withTurn, [...view.turns].reverse(), row => modelLabel(row.model), width, false));
334
+ }
335
+ return lines.map(line => boundLine(line, width));
336
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Precise terminal-cell width measurement — the single authority every
3
+ * budget, wrap, and truncation path shares. The previous heuristic
4
+ * (`codePoint > 0x2e7f ? 2 : 1)) mis-sized Hangul Jamo (narrow), high
5
+ * non-CJK code points (wide), and emoji: text-default glyphs like ✳ ⚠ ❤
6
+ * counted 2 while terminals draw 1, and VS16 sequences counted 1 while
7
+ * terminals draw 2 — the exact drift class the community dsh-TUI string
8
+ * engine documents (a spinner glyph drifting one column per frame). This
9
+ * module adapts that engine's rules without its Ink-fork renderer: an ASCII
10
+ * fast path, grapheme-cluster iteration via Intl.Segmenter (code-point
11
+ * fallback), a merged East-Asian-Wide/Fullwidth + Emoji_Presentation range
12
+ * table, text-default emoji = 1, VS16 = 2, marks/selectors/ZWJ = 0.
13
+ * @module @deepseek-ai/dsh-code/render/width
14
+ */
15
+ /**
16
+ * Code-point ranges rendered two cells wide: East Asian Wide/Fullwidth
17
+ * blocks merged with the Emoji_Presentation set (default-wide emoji) and
18
+ * regional indicators (flags). Sorted, inclusive, binary-searched.
19
+ */
20
+ const WIDE_RANGES = [
21
+ [0x1100, 0x115f],
22
+ [0x231a, 0x231b],
23
+ [0x2329, 0x232a],
24
+ [0x23e9, 0x23ec],
25
+ [0x23f0, 0x23f0],
26
+ [0x23f3, 0x23f3],
27
+ [0x23f8, 0x23fa],
28
+ [0x25fd, 0x25fe],
29
+ [0x2614, 0x2615],
30
+ [0x2648, 0x2653],
31
+ [0x267f, 0x267f],
32
+ [0x2693, 0x2693],
33
+ [0x26a1, 0x26a1],
34
+ [0x26aa, 0x26ab],
35
+ [0x26bd, 0x26be],
36
+ [0x26c4, 0x26c5],
37
+ [0x26ce, 0x26ce],
38
+ [0x26d4, 0x26d4],
39
+ [0x26ea, 0x26ea],
40
+ [0x26f2, 0x26f3],
41
+ [0x26f5, 0x26f5],
42
+ [0x26fa, 0x26fa],
43
+ [0x26fd, 0x26fd],
44
+ [0x2705, 0x2705],
45
+ [0x270a, 0x270b],
46
+ [0x2728, 0x2728],
47
+ [0x274c, 0x274c],
48
+ [0x274e, 0x274e],
49
+ [0x2753, 0x2755],
50
+ [0x2757, 0x2757],
51
+ [0x2795, 0x2797],
52
+ [0x27b0, 0x27b0],
53
+ [0x27bf, 0x27bf],
54
+ [0x2b1b, 0x2b1c],
55
+ [0x2b50, 0x2b50],
56
+ [0x2b55, 0x2b55],
57
+ [0x2e80, 0x303e],
58
+ [0x3041, 0x33ff],
59
+ [0x3400, 0x4dbf],
60
+ [0x4e00, 0xa4cf],
61
+ [0xa960, 0xa97f],
62
+ [0xac00, 0xd7a3],
63
+ [0xf900, 0xfaff],
64
+ [0xfe10, 0xfe19],
65
+ [0xfe30, 0xfe4f],
66
+ [0xff00, 0xff60],
67
+ [0xffe0, 0xffe6],
68
+ [0x16fe0, 0x16fe4],
69
+ [0x17000, 0x18aff],
70
+ [0x1b000, 0x1b2ff],
71
+ [0x1f004, 0x1f004],
72
+ [0x1f0cf, 0x1f0cf],
73
+ [0x1f18e, 0x1f18e],
74
+ [0x1f191, 0x1f19a],
75
+ [0x1f1e6, 0x1f1ff],
76
+ [0x1f200, 0x1f320],
77
+ [0x1f32d, 0x1f335],
78
+ [0x1f337, 0x1f37c],
79
+ [0x1f37e, 0x1f393],
80
+ [0x1f3a0, 0x1f3ca],
81
+ [0x1f3cf, 0x1f3d3],
82
+ [0x1f3e0, 0x1f3f0],
83
+ [0x1f3f4, 0x1f3f4],
84
+ [0x1f3f8, 0x1f43e],
85
+ [0x1f440, 0x1f440],
86
+ [0x1f442, 0x1f4fc],
87
+ [0x1f4ff, 0x1f53d],
88
+ [0x1f54b, 0x1f54e],
89
+ [0x1f550, 0x1f567],
90
+ [0x1f57a, 0x1f57a],
91
+ [0x1f595, 0x1f596],
92
+ [0x1f5a4, 0x1f5a4],
93
+ [0x1f5fb, 0x1f64f],
94
+ [0x1f680, 0x1f6c5],
95
+ [0x1f6cc, 0x1f6cc],
96
+ [0x1f6d0, 0x1f6d2],
97
+ [0x1f6d5, 0x1f6d7],
98
+ [0x1f6eb, 0x1f6ec],
99
+ [0x1f6f4, 0x1f6fc],
100
+ [0x1f7e0, 0x1f7eb],
101
+ [0x1f90c, 0x1f93a],
102
+ [0x1f93c, 0x1f945],
103
+ [0x1f947, 0x1f9ff],
104
+ [0x1fa70, 0x1fa7c],
105
+ [0x1fa80, 0x1fa89],
106
+ [0x1fa8f, 0x1fac6],
107
+ [0x1face, 0x1fadc],
108
+ [0x1fadf, 0x1fae9],
109
+ [0x1faf0, 0x1faf8],
110
+ [0x20000, 0x2fffd],
111
+ [0x30000, 0x3fffd],
112
+ ];
113
+ /** Whether one code point falls inside a wide range (binary search). */
114
+ function isWideCodePoint(code) {
115
+ let low = 0;
116
+ let high = WIDE_RANGES.length - 1;
117
+ while (low <= high) {
118
+ const mid = (low + high) >> 1;
119
+ const [start, end] = WIDE_RANGES[mid];
120
+ if (code < start)
121
+ high = mid - 1;
122
+ else if (code > end)
123
+ low = mid + 1;
124
+ else
125
+ return true;
126
+ }
127
+ return false;
128
+ }
129
+ /** Zero-width code points: combining marks, enclosing marks, format controls. */
130
+ const ZERO_WIDTH = /^[\p{Mn}\p{Me}\p{Cf}]$/u;
131
+ /** Module-cached grapheme segmenter; undefined when the runtime lacks it. */
132
+ let segmenter;
133
+ try {
134
+ segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
135
+ }
136
+ catch {
137
+ segmenter = undefined;
138
+ }
139
+ /** Split text into grapheme clusters (code points when Segmenter is absent). */
140
+ export function splitGraphemes(text) {
141
+ if (segmenter === undefined)
142
+ return Array.from(text);
143
+ return Array.from(segmenter.segment(text), part => part.segment);
144
+ }
145
+ /** Terminal-cell width of one grapheme cluster. */
146
+ export function graphemeWidth(cluster) {
147
+ // VS16 requests emoji presentation: ❤️ / ✳️ render two cells even when the
148
+ // base glyph is text-default (width 1 without the selector).
149
+ if (cluster.includes('\u{fe0f}', 0))
150
+ return 2;
151
+ const first = cluster.codePointAt(0) ?? 0;
152
+ if (ZERO_WIDTH.test(String.fromCodePoint(first)))
153
+ return 0;
154
+ return isWideCodePoint(first) ? 2 : 1;
155
+ }
156
+ /** Terminal-cell width of one code point (surrogate pairs must stay paired). */
157
+ export function codePointWidth(char) {
158
+ if (ZERO_WIDTH.test(char))
159
+ return 0;
160
+ return isWideCodePoint(char.codePointAt(0) ?? 0) ? 2 : 1;
161
+ }
162
+ /**
163
+ * Terminal-cell width of a string: an ASCII fast path avoids the segmenter
164
+ * for the overwhelmingly common case; everything else sums grapheme clusters.
165
+ * Control characters occupy no cells (display sanitization makes them
166
+ * visible escapes before they ever reach a budget).
167
+ * @param text - display-safe or raw text to measure.
168
+ * @returns the column count the terminal will draw.
169
+ */
170
+ export function stringWidth(text) {
171
+ let asciiOnly = true;
172
+ for (let index = 0; index < text.length; index += 1) {
173
+ if (text.charCodeAt(index) > 0x7f) {
174
+ asciiOnly = false;
175
+ break;
176
+ }
177
+ }
178
+ if (asciiOnly) {
179
+ let columns = 0;
180
+ for (let index = 0; index < text.length; index += 1) {
181
+ const code = text.charCodeAt(index);
182
+ if (code >= 0x20 && code !== 0x7f)
183
+ columns += 1;
184
+ }
185
+ return columns;
186
+ }
187
+ let columns = 0;
188
+ for (const cluster of splitGraphemes(text))
189
+ columns += graphemeWidth(cluster);
190
+ return columns;
191
+ }