@xp266/dshtui 0.1.0
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/bin/dshtui.js +65 -0
- package/cordis.patch.yml +81 -0
- package/lib/contract/index.d.mts +2 -0
- package/lib/contract/index.mjs +5 -0
- package/lib/dialog.d.mts +156 -0
- package/lib/dialog.mjs +3 -0
- package/lib/index-DqnaSXqP.d.mts +823 -0
- package/lib/index.d.mts +17 -0
- package/lib/index.mjs +11404 -0
- package/lib/prod-react-Dzof4qJx.mjs +4 -0
- package/lib/surface-DdxzIgIY.mjs +3126 -0
- package/lib/vendor.d.mts +3 -0
- package/lib/vendor.mjs +4 -0
- package/package.json +128 -0
- package/src/apply-theme.ts +8 -0
- package/src/boot-log.ts +35 -0
- package/src/chat/blocks.ts +41 -0
- package/src/chat/bridge.ts +1076 -0
- package/src/chat/builtin-tool-views.ts +53 -0
- package/src/chat/chat-face.ts +16 -0
- package/src/chat/chat-nodes.ts +53 -0
- package/src/chat/efforts.ts +4 -0
- package/src/chat/interactions.ts +329 -0
- package/src/chat/models.ts +263 -0
- package/src/chat/partial-json.ts +125 -0
- package/src/chat/presets.ts +26 -0
- package/src/chat/question-view.ts +106 -0
- package/src/chat/retry-status.ts +33 -0
- package/src/chat/session-groups.ts +34 -0
- package/src/chat/session-list.ts +311 -0
- package/src/chat/store.ts +636 -0
- package/src/chat/todo-view.ts +66 -0
- package/src/chat/tool-view.ts +311 -0
- package/src/chat/tool-views.ts +114 -0
- package/src/contract/index.ts +704 -0
- package/src/contract/upstream.ts +165 -0
- package/src/core/caret-nonce.ts +19 -0
- package/src/core/composer-layout.ts +65 -0
- package/src/core/edit.ts +152 -0
- package/src/core/field-view.ts +52 -0
- package/src/core/fields.ts +185 -0
- package/src/core/metrics.ts +26 -0
- package/src/core/paste.ts +149 -0
- package/src/core/segments.ts +112 -0
- package/src/core/text.ts +290 -0
- package/src/dialog.ts +11 -0
- package/src/env.ts +38 -0
- package/src/harness-home.ts +22 -0
- package/src/hot-theme.ts +56 -0
- package/src/index.tsx +185 -0
- package/src/kernel/registry.ts +102 -0
- package/src/kernel/surface.ts +33 -0
- package/src/log.ts +249 -0
- package/src/model/message.ts +89 -0
- package/src/model/selection.ts +63 -0
- package/src/performance-guard.ts +28 -0
- package/src/runtime/prod-react.ts +10 -0
- package/src/terminal/background.ts +66 -0
- package/src/terminal/capabilities.ts +54 -0
- package/src/terminal/clipboard-backends.ts +69 -0
- package/src/terminal/clipboard.ts +161 -0
- package/src/terminal/cursor-shape.ts +13 -0
- package/src/terminal/glyphs.ts +44 -0
- package/src/terminal/mouse.ts +154 -0
- package/src/terminal/probe.ts +61 -0
- package/src/terminal/screen.ts +349 -0
- package/src/theme-settings.ts +31 -0
- package/src/theme.ts +319 -0
- package/src/ui/app.tsx +454 -0
- package/src/ui/chrome/caps.tsx +9 -0
- package/src/ui/chrome/hint-service.ts +87 -0
- package/src/ui/chrome/input-status.ts +27 -0
- package/src/ui/chrome/key-gate.tsx +29 -0
- package/src/ui/chrome/status-bar.tsx +147 -0
- package/src/ui/contributions.ts +47 -0
- package/src/ui/dialog/defaults-dialog.tsx +152 -0
- package/src/ui/dialog/dialog-item.tsx +38 -0
- package/src/ui/dialog/dialog.tsx +386 -0
- package/src/ui/dialog/effort-dialog.tsx +45 -0
- package/src/ui/dialog/geometry.ts +131 -0
- package/src/ui/dialog/items.ts +124 -0
- package/src/ui/dialog/list-dialog.tsx +63 -0
- package/src/ui/dialog/models-dialog.tsx +203 -0
- package/src/ui/dialog/presets-dialog.tsx +46 -0
- package/src/ui/dialog/providers-dialog.tsx +313 -0
- package/src/ui/dialog/sessions-dialog.tsx +163 -0
- package/src/ui/dialog/sizes.ts +7 -0
- package/src/ui/dialog/status-lines.ts +11 -0
- package/src/ui/dialog/todo-dialog.tsx +29 -0
- package/src/ui/dialog/use-dialog-input.ts +186 -0
- package/src/ui/extension-point.ts +188 -0
- package/src/ui/home-logo.ts +132 -0
- package/src/ui/hooks/use-async-action.ts +25 -0
- package/src/ui/hooks/use-async-list.ts +71 -0
- package/src/ui/hooks/use-caret.ts +24 -0
- package/src/ui/hooks/use-chat-events.ts +153 -0
- package/src/ui/hooks/use-mouse-selection.ts +352 -0
- package/src/ui/hooks/use-scroll.ts +49 -0
- package/src/ui/hooks/use-terminal-size.ts +20 -0
- package/src/ui/input/commands.ts +118 -0
- package/src/ui/input/composer-bus.ts +14 -0
- package/src/ui/input/composer-fields.ts +111 -0
- package/src/ui/input/composer-keys.ts +21 -0
- package/src/ui/input/composer-paste.ts +20 -0
- package/src/ui/input/input-bar.tsx +279 -0
- package/src/ui/input/use-composer.ts +495 -0
- package/src/ui/key-arbiter.ts +37 -0
- package/src/ui/keymap.ts +15 -0
- package/src/ui/layout-service.ts +77 -0
- package/src/ui/message/layout.ts +763 -0
- package/src/ui/message/md/block.ts +247 -0
- package/src/ui/message/md/engine.ts +430 -0
- package/src/ui/message/md/extensions.ts +58 -0
- package/src/ui/message/md/highlight.ts +626 -0
- package/src/ui/message/md/index.ts +14 -0
- package/src/ui/message/md/inline.ts +97 -0
- package/src/ui/message/md/palette.ts +87 -0
- package/src/ui/message/message-list.tsx +69 -0
- package/src/ui/message/message-row.tsx +137 -0
- package/src/ui/message/message-views.ts +16 -0
- package/src/ui/message/renderers.ts +39 -0
- package/src/ui/message/tool-diff.ts +188 -0
- package/src/ui/message/warmup.ts +41 -0
- package/src/ui/message/wave.ts +98 -0
- package/src/ui/overlay.ts +22 -0
- package/src/ui/panels/approval-panel.tsx +193 -0
- package/src/ui/panels/exports.ts +3 -0
- package/src/ui/panels/question-model.ts +424 -0
- package/src/ui/panels/question-panel.tsx +154 -0
- package/src/ui/panels/surface.tsx +106 -0
- package/src/ui/panels-builtin.tsx +86 -0
- package/src/ui/pointer/builtins.ts +281 -0
- package/src/ui/pointer/registry.ts +25 -0
- package/src/ui/region.tsx +31 -0
- package/src/ui/selection/service.ts +92 -0
- package/src/ui/selection-registry.ts +88 -0
- package/src/ui/selection.tsx +148 -0
- package/src/ui/spinner-tick.tsx +24 -0
- package/src/ui/use-available-windows.ts +31 -0
- package/src/ui/widgets/actions.tsx +84 -0
- package/src/ui/widgets/button.tsx +57 -0
- package/src/ui/widgets/checkbox.tsx +58 -0
- package/src/ui/widgets/header.tsx +36 -0
- package/src/ui/widgets/index.ts +8 -0
- package/src/ui/widgets/input.tsx +76 -0
- package/src/ui/widgets/registry.ts +32 -0
- package/src/ui/widgets/search.tsx +59 -0
- package/src/ui/widgets/select.tsx +97 -0
- package/src/ui/widgets/static.tsx +37 -0
- package/src/ui/widgets/types.ts +1 -0
- package/src/ui/window-services-bridge.ts +71 -0
- package/src/ui/window-services.ts +20 -0
- package/src/ui/windows/models-window.tsx +15 -0
- package/src/ui/windows/providers-window.tsx +14 -0
- package/src/ui/windows/service-window.tsx +18 -0
- package/src/ui/windows/sessions-window.tsx +25 -0
- package/src/ui/windows-builtin.tsx +94 -0
- package/src/ui/windows.ts +18 -0
- package/src/vendor.ts +12 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import type { Message, ToolCardMessage } from '../../model/message.ts'
|
|
2
|
+
import type { CustomMessage } from '../../contract/index.ts'
|
|
3
|
+
import { colToCharIndex, textWidth, truncate, wrapLines } from '../../core/text.ts'
|
|
4
|
+
import { hasFieldChar } from '../../core/fields.ts'
|
|
5
|
+
import { expandFieldChars, fieldRowSegments } from '../../core/field-view.ts'
|
|
6
|
+
import { selectedRange } from '../../model/selection.ts'
|
|
7
|
+
import type { LineSelection } from '../../model/selection.ts'
|
|
8
|
+
import { sliceByColumns } from '../selection-registry.ts'
|
|
9
|
+
import { renderMarkdown, renderMarkdownStreaming, clearMarkdownStreamStates } from './md/index.ts'
|
|
10
|
+
import { renderToolDiffBody, renderToolReadBody } from './tool-diff.ts'
|
|
11
|
+
import { trimTrailingBlanks } from '../../chat/tool-view.ts'
|
|
12
|
+
import { messageViewOf } from './message-views.ts'
|
|
13
|
+
import { segmentsKey } from '../../core/segments.ts'
|
|
14
|
+
import type { Segment } from '../../core/segments.ts'
|
|
15
|
+
import { COLORS } from '../../theme.ts'
|
|
16
|
+
import { glyphs } from '../../terminal/glyphs.ts'
|
|
17
|
+
import { BUBBLE_WIDTH_OFFSET, HEADER_LABEL_COL } from '../../core/metrics.ts'
|
|
18
|
+
import { messageBackgroundWidth } from '../layout-service.ts'
|
|
19
|
+
import { messageRendererOf } from './renderers.ts'
|
|
20
|
+
|
|
21
|
+
export { HEADER_LABEL_COL }
|
|
22
|
+
|
|
23
|
+
export type RowKind = 'pad' | 'text' | 'header' | 'blank'
|
|
24
|
+
|
|
25
|
+
export interface RowInfo {
|
|
26
|
+
messageIndex: number
|
|
27
|
+
messageId: string
|
|
28
|
+
lineNo: number
|
|
29
|
+
kind: RowKind
|
|
30
|
+
text: string
|
|
31
|
+
colStart: number
|
|
32
|
+
selectable: boolean
|
|
33
|
+
clickable: boolean
|
|
34
|
+
background: boolean
|
|
35
|
+
backgroundWidth: number
|
|
36
|
+
muted: boolean
|
|
37
|
+
label: string
|
|
38
|
+
collapsed: boolean
|
|
39
|
+
thinking: boolean
|
|
40
|
+
role: 'user' | 'assistant' | 'error' | undefined
|
|
41
|
+
segments?: Segment[]
|
|
42
|
+
segKey?: string
|
|
43
|
+
spinner?: boolean
|
|
44
|
+
wave?: boolean
|
|
45
|
+
accent?: string
|
|
46
|
+
lineBg?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface BodyRendered {
|
|
50
|
+
lines: string[]
|
|
51
|
+
rows: Segment[][] | null
|
|
52
|
+
bgs: (string | undefined)[] | null
|
|
53
|
+
customLabel?: string
|
|
54
|
+
customMuted?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type PlanRow =
|
|
58
|
+
| { type: 'pad'; role: RowInfo['role'] }
|
|
59
|
+
| { type: 'blank' }
|
|
60
|
+
| { type: 'header'; label: string; collapsed: boolean; clickable: boolean }
|
|
61
|
+
| {
|
|
62
|
+
type: 'body'
|
|
63
|
+
line: number
|
|
64
|
+
colStart: number
|
|
65
|
+
bg: boolean
|
|
66
|
+
muted: boolean
|
|
67
|
+
selectable: boolean
|
|
68
|
+
role: RowInfo['role']
|
|
69
|
+
wave?: boolean
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
type: 'lit'
|
|
73
|
+
text: string
|
|
74
|
+
segments?: Segment[]
|
|
75
|
+
colStart: number
|
|
76
|
+
bg: boolean
|
|
77
|
+
muted: boolean
|
|
78
|
+
selectable: boolean
|
|
79
|
+
role: RowInfo['role']
|
|
80
|
+
wave?: boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface MessageRender extends BodyRendered {
|
|
84
|
+
plan: PlanRow[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let layoutEpoch = 0
|
|
88
|
+
|
|
89
|
+
export function clearLayoutCache(): void {
|
|
90
|
+
layoutEpoch += 1
|
|
91
|
+
clearMarkdownStreamStates()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const padRow = (role: RowInfo['role']): PlanRow => ({ type: 'pad', role })
|
|
95
|
+
const blankRow = (): PlanRow => ({ type: 'blank' })
|
|
96
|
+
|
|
97
|
+
function bodyRows(count: number, options: { colStart: number; bg: boolean; muted: boolean; selectable: boolean; role: RowInfo['role'] }): PlanRow[] {
|
|
98
|
+
const out: PlanRow[] = []
|
|
99
|
+
for (let line = 0; line < count; line++) {
|
|
100
|
+
out.push({ type: 'body', line, ...options })
|
|
101
|
+
}
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderBody(message: Message, width: number): BodyRendered {
|
|
106
|
+
const inner = width - BUBBLE_WIDTH_OFFSET
|
|
107
|
+
const override = messageRendererOf(message.kind)
|
|
108
|
+
if (override !== undefined) {
|
|
109
|
+
const rendered = override.render(message, inner)
|
|
110
|
+
if (rendered !== undefined) {
|
|
111
|
+
return {
|
|
112
|
+
lines: rendered.lines,
|
|
113
|
+
rows: rendered.rows ?? null,
|
|
114
|
+
bgs: rendered.bgs ?? null,
|
|
115
|
+
...(rendered.customLabel === undefined ? {} : { customLabel: rendered.customLabel }),
|
|
116
|
+
...(rendered.customMuted === undefined ? {} : { customMuted: rendered.customMuted }),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
switch (message.kind) {
|
|
121
|
+
case 'bubble': {
|
|
122
|
+
if (message.role !== 'user' && message.origin === 'command') {
|
|
123
|
+
const lines = wrapLines(message.content, inner)
|
|
124
|
+
return { lines, rows: null, bgs: null }
|
|
125
|
+
}
|
|
126
|
+
if (message.role === 'assistant') {
|
|
127
|
+
const rendered = message.streaming === true
|
|
128
|
+
? renderMarkdownStreaming(message.content, inner, false, `bubble:${message.id}`)
|
|
129
|
+
: renderMarkdown(message.content, inner, false, `bubble:${message.id}`)
|
|
130
|
+
return { lines: rendered.lines, rows: rendered.rows, bgs: null }
|
|
131
|
+
}
|
|
132
|
+
const lines = wrapLines(message.content, inner)
|
|
133
|
+
if (hasFieldChar(message.content)) {
|
|
134
|
+
return {
|
|
135
|
+
lines: lines.map(expandFieldChars),
|
|
136
|
+
rows: lines.map(line => fieldRowSegments(line, { background: COLORS.userBubbleBackground })),
|
|
137
|
+
bgs: null,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { lines, rows: null, bgs: null }
|
|
141
|
+
}
|
|
142
|
+
case 'collapsible': {
|
|
143
|
+
if (message.thinking === true && !message.collapsed) {
|
|
144
|
+
const streaming = message.running === true || message.streaming === true
|
|
145
|
+
const rendered = streaming
|
|
146
|
+
? renderMarkdownStreaming(message.body, inner, true, `thinking:${message.id}`)
|
|
147
|
+
: renderMarkdown(message.body, inner, true, `thinking:${message.id}`)
|
|
148
|
+
return { lines: rendered.lines, rows: rendered.rows, bgs: null }
|
|
149
|
+
}
|
|
150
|
+
const lines = wrapLines(message.body, inner)
|
|
151
|
+
return { lines, rows: null, bgs: null }
|
|
152
|
+
}
|
|
153
|
+
case 'tool-card': {
|
|
154
|
+
if (message.diff !== undefined && message.diff.hunks.length > 0) {
|
|
155
|
+
const rendered = renderToolDiffBody({
|
|
156
|
+
tool: message.tool,
|
|
157
|
+
path: message.diff.path,
|
|
158
|
+
hunks: message.diff.hunks,
|
|
159
|
+
error: message.error,
|
|
160
|
+
...(message.diff.backgrounds === undefined ? {} : { backgrounds: message.diff.backgrounds }),
|
|
161
|
+
}, inner)
|
|
162
|
+
return { lines: rendered.lines, rows: rendered.rows, bgs: rendered.bgs }
|
|
163
|
+
}
|
|
164
|
+
if (message.read !== undefined) {
|
|
165
|
+
const rendered = renderToolReadBody({
|
|
166
|
+
...(message.read.path === undefined ? {} : { path: message.read.path }),
|
|
167
|
+
lines: message.read.lines,
|
|
168
|
+
...(message.read.offset === undefined ? {} : { offset: message.read.offset }),
|
|
169
|
+
...(message.read.totalLines === undefined ? {} : { totalLines: message.read.totalLines }),
|
|
170
|
+
...(message.read.lang === undefined ? {} : { lang: message.read.lang }),
|
|
171
|
+
}, inner)
|
|
172
|
+
return { lines: rendered.lines, rows: rendered.rows, bgs: rendered.bgs }
|
|
173
|
+
}
|
|
174
|
+
const lines: string[] = []
|
|
175
|
+
const rows: Segment[][] = []
|
|
176
|
+
const push = (text: string, segments?: Segment[]): void => {
|
|
177
|
+
lines.push(text)
|
|
178
|
+
if (segments !== undefined) rows[lines.length - 1] = segments
|
|
179
|
+
}
|
|
180
|
+
const hasError = message.error !== undefined && message.error !== ''
|
|
181
|
+
// `failed` is a render state, not content: the result text of an
|
|
182
|
+
// errored call IS the error message, so it renders in red instead of
|
|
183
|
+
// gaining a synthetic "failed" line next to it.
|
|
184
|
+
const failed = message.failed === true && !hasError
|
|
185
|
+
const resultBody = message.resultBody !== undefined && message.resultBody.trimEnd() !== ''
|
|
186
|
+
? trimTrailingBlanks(message.resultBody)
|
|
187
|
+
: undefined
|
|
188
|
+
const pill = message.exitCode !== undefined
|
|
189
|
+
? `[exit code: ${message.exitCode}]`
|
|
190
|
+
: message.signal !== undefined ? `[killed by signal: ${message.signal}]` : undefined
|
|
191
|
+
const hasArgs = message.argsBody !== ''
|
|
192
|
+
const hasNested = (message.nested?.length ?? 0) > 0
|
|
193
|
+
if (!hasArgs && resultBody === undefined && pill === undefined && !hasNested && !hasError && !failed) {
|
|
194
|
+
return { lines: [], rows: null, bgs: null }
|
|
195
|
+
}
|
|
196
|
+
if (hasArgs) {
|
|
197
|
+
for (const line of wrapLines(message.argsBody, inner)) push(line)
|
|
198
|
+
}
|
|
199
|
+
if (resultBody !== undefined) {
|
|
200
|
+
if (hasArgs) push('')
|
|
201
|
+
for (const line of wrapLines(resultBody, inner)) {
|
|
202
|
+
push(line, failed ? [{ text: line, style: { color: COLORS.errorText } }] : undefined)
|
|
203
|
+
}
|
|
204
|
+
} else if (failed) {
|
|
205
|
+
if (hasArgs) push('')
|
|
206
|
+
push('failed', [{ text: 'failed', style: { color: COLORS.errorText } }])
|
|
207
|
+
}
|
|
208
|
+
if (pill !== undefined) push(pill)
|
|
209
|
+
if (hasNested) {
|
|
210
|
+
const prefix = `${glyphs.treeBranch} `
|
|
211
|
+
for (const child of message.nested!) {
|
|
212
|
+
const state = child.running
|
|
213
|
+
? ''
|
|
214
|
+
: child.error !== undefined
|
|
215
|
+
? ` ${glyphs.separator} error`
|
|
216
|
+
: child.resultBody === undefined || child.resultBody === '' ? '' : ` ${glyphs.separator} ${firstLine(child.resultBody)}`
|
|
217
|
+
const label = truncate(child.label, Math.max(4, inner - 6))
|
|
218
|
+
push(
|
|
219
|
+
`${prefix}${label}${state}`,
|
|
220
|
+
[
|
|
221
|
+
{ text: prefix, style: { color: COLORS.toolBodyText } },
|
|
222
|
+
{ text: `${label}${state}`, style: {} },
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (hasError) {
|
|
228
|
+
if (lines.length > 0 && lines[lines.length - 1] !== '') push('')
|
|
229
|
+
const text = truncate(message.error!, Math.max(8, inner))
|
|
230
|
+
push(text, [{ text, style: { color: COLORS.errorText } }])
|
|
231
|
+
}
|
|
232
|
+
return { lines, rows: rows.length === 0 ? null : rows, bgs: null }
|
|
233
|
+
}
|
|
234
|
+
case 'compaction': {
|
|
235
|
+
if (message.summary === '' && message.error === undefined) return { lines: [], rows: null, bgs: null }
|
|
236
|
+
const streaming = message.running === true && message.summary !== ''
|
|
237
|
+
const rendered = message.summary === ''
|
|
238
|
+
? { lines: [], rows: [] }
|
|
239
|
+
: streaming
|
|
240
|
+
? renderMarkdownStreaming(message.summary, inner, false, `compaction:${message.id}`)
|
|
241
|
+
: renderMarkdown(message.summary, inner, false, `compaction:${message.id}`)
|
|
242
|
+
const lines = [...rendered.lines]
|
|
243
|
+
const rows = [...rendered.rows]
|
|
244
|
+
if (message.error !== undefined && message.error !== '') {
|
|
245
|
+
const text = truncate(`error: ${message.error}`, Math.max(8, inner))
|
|
246
|
+
lines.push(text)
|
|
247
|
+
rows.push([{ text, style: { color: COLORS.errorText } }])
|
|
248
|
+
}
|
|
249
|
+
return { lines, rows, bgs: null }
|
|
250
|
+
}
|
|
251
|
+
case 'home-logo': {
|
|
252
|
+
const lines: string[] = []
|
|
253
|
+
const rows: Segment[][] = []
|
|
254
|
+
message.lines.forEach((line, i) => {
|
|
255
|
+
lines.push(line)
|
|
256
|
+
rows.push([{ text: line, style: { color: message.colors[i] } }])
|
|
257
|
+
})
|
|
258
|
+
for (const line of message.info) lines.push(truncate(line, Math.max(8, inner)))
|
|
259
|
+
return { lines, rows: rows.length === 0 ? null : rows, bgs: null }
|
|
260
|
+
}
|
|
261
|
+
case 'custom': {
|
|
262
|
+
const view = messageViewOf(message.view)
|
|
263
|
+
if (view === undefined) {
|
|
264
|
+
return { lines: wrapLines(describeCustomData(message.data), inner), rows: null, bgs: null }
|
|
265
|
+
}
|
|
266
|
+
const rendered = view.render({ message, width: inner })
|
|
267
|
+
const lines = rendered.wrap === false
|
|
268
|
+
? [...rendered.lines]
|
|
269
|
+
: rendered.lines.flatMap(line => wrapLines(line, inner))
|
|
270
|
+
return {
|
|
271
|
+
lines,
|
|
272
|
+
rows: null,
|
|
273
|
+
bgs: null,
|
|
274
|
+
customLabel: rendered.label ?? message.view,
|
|
275
|
+
customMuted: rendered.muted ?? true,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function describeCustomData(data: unknown): string {
|
|
282
|
+
if (typeof data === 'string') return data
|
|
283
|
+
try {
|
|
284
|
+
return JSON.stringify(data, null, 2) ?? ''
|
|
285
|
+
} catch {
|
|
286
|
+
return String(data)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function firstLine(text: string): string {
|
|
291
|
+
const at = text.indexOf('\n')
|
|
292
|
+
return at === -1 ? text : text.slice(0, at)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function planFor(message: Message, body: BodyRendered): PlanRow[] {
|
|
296
|
+
switch (message.kind) {
|
|
297
|
+
case 'bubble': {
|
|
298
|
+
// User bubbles sit inside the background box; assistant, error, and
|
|
299
|
+
// command output float without it.
|
|
300
|
+
const floating = message.role === 'error' || message.role === 'assistant' || message.origin === 'command'
|
|
301
|
+
const texts = bodyRows(body.lines.length, {
|
|
302
|
+
colStart: 4,
|
|
303
|
+
bg: !floating,
|
|
304
|
+
muted: false,
|
|
305
|
+
selectable: true,
|
|
306
|
+
role: message.role,
|
|
307
|
+
})
|
|
308
|
+
return floating ? [...texts, blankRow()] : [padRow(message.role), ...texts, padRow(message.role), blankRow()]
|
|
309
|
+
}
|
|
310
|
+
case 'collapsible': {
|
|
311
|
+
const header: PlanRow = {
|
|
312
|
+
type: 'header',
|
|
313
|
+
label: message.label,
|
|
314
|
+
collapsed: message.collapsed,
|
|
315
|
+
clickable: !Boolean(message.streaming),
|
|
316
|
+
}
|
|
317
|
+
if (message.collapsed) return [header, blankRow()]
|
|
318
|
+
return [
|
|
319
|
+
header,
|
|
320
|
+
blankRow(),
|
|
321
|
+
...bodyRows(body.lines.length, { colStart: HEADER_LABEL_COL, bg: false, muted: true, selectable: true, role: 'assistant' }),
|
|
322
|
+
blankRow(),
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
case 'tool-card': {
|
|
326
|
+
const header: PlanRow = {
|
|
327
|
+
type: 'lit',
|
|
328
|
+
text: message.label,
|
|
329
|
+
colStart: 4,
|
|
330
|
+
bg: true,
|
|
331
|
+
muted: true,
|
|
332
|
+
selectable: true,
|
|
333
|
+
role: 'assistant',
|
|
334
|
+
...(message.streaming === true || message.running === true ? { wave: true } : {}),
|
|
335
|
+
}
|
|
336
|
+
if (body.lines.length === 0 && message.running === true) {
|
|
337
|
+
return [padRow('assistant'), header, padRow('assistant'), blankRow()]
|
|
338
|
+
}
|
|
339
|
+
return [
|
|
340
|
+
padRow('assistant'),
|
|
341
|
+
header,
|
|
342
|
+
padRow('assistant'),
|
|
343
|
+
...bodyRows(body.lines.length, { colStart: 4, bg: true, muted: false, selectable: true, role: 'assistant' }),
|
|
344
|
+
padRow('assistant'),
|
|
345
|
+
blankRow(),
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
case 'compaction': {
|
|
349
|
+
const header: PlanRow = {
|
|
350
|
+
type: 'lit',
|
|
351
|
+
text: 'Compact',
|
|
352
|
+
segments: [{ text: 'Compact', style: { color: COLORS.sectionHeader, bold: true } }],
|
|
353
|
+
colStart: 4,
|
|
354
|
+
bg: true,
|
|
355
|
+
muted: false,
|
|
356
|
+
selectable: false,
|
|
357
|
+
role: 'assistant',
|
|
358
|
+
...(message.running ? { wave: true } : {}),
|
|
359
|
+
}
|
|
360
|
+
if (message.running) {
|
|
361
|
+
return [padRow('assistant'), header, padRow('assistant'), blankRow()]
|
|
362
|
+
}
|
|
363
|
+
return [
|
|
364
|
+
padRow('assistant'),
|
|
365
|
+
header,
|
|
366
|
+
padRow('assistant'),
|
|
367
|
+
...bodyRows(body.lines.length, { colStart: 4, bg: true, muted: false, selectable: true, role: 'assistant' }),
|
|
368
|
+
padRow('assistant'),
|
|
369
|
+
blankRow(),
|
|
370
|
+
]
|
|
371
|
+
}
|
|
372
|
+
case 'home-logo':
|
|
373
|
+
return [
|
|
374
|
+
...bodyRows(body.lines.length, { colStart: 4, bg: false, muted: false, selectable: true, role: 'assistant' }),
|
|
375
|
+
blankRow(),
|
|
376
|
+
]
|
|
377
|
+
case 'custom': {
|
|
378
|
+
const header: PlanRow = {
|
|
379
|
+
type: 'header',
|
|
380
|
+
label: body.customLabel ?? message.view,
|
|
381
|
+
collapsed: false,
|
|
382
|
+
clickable: false,
|
|
383
|
+
}
|
|
384
|
+
return [
|
|
385
|
+
header,
|
|
386
|
+
blankRow(),
|
|
387
|
+
...bodyRows(body.lines.length, {
|
|
388
|
+
colStart: HEADER_LABEL_COL,
|
|
389
|
+
bg: false,
|
|
390
|
+
muted: body.customMuted ?? true,
|
|
391
|
+
selectable: true,
|
|
392
|
+
role: 'assistant',
|
|
393
|
+
}),
|
|
394
|
+
blankRow(),
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
type RenderFingerprint =
|
|
401
|
+
| { kind: 'bubble'; content: string; role: string | undefined; origin: string | undefined; streaming: boolean | undefined }
|
|
402
|
+
| { kind: 'collapsible'; body: string; label: string; collapsed: boolean; running: boolean; thinking: boolean | undefined; streaming: boolean | undefined }
|
|
403
|
+
| { kind: 'tool-card'; tool: string; label: string; argsBody: string; resultBody: string | undefined; diff: ToolCardMessage['diff']; read: ToolCardMessage['read']; nested: ToolCardMessage['nested']; error: string | undefined; failed: boolean | undefined; exitCode: number | undefined; signal: string | undefined; bodyCol: number | undefined; running: boolean; streaming: boolean | undefined }
|
|
404
|
+
| { kind: 'compaction'; summary: string; running: boolean; error: string | undefined }
|
|
405
|
+
| { kind: 'custom'; view: string; data: unknown; running: boolean | undefined; streaming: boolean | undefined }
|
|
406
|
+
| { kind: 'home-logo'; lines: readonly string[]; colors: readonly string[]; info: readonly string[] }
|
|
407
|
+
|
|
408
|
+
interface RenderMemoEntry {
|
|
409
|
+
epoch: number
|
|
410
|
+
width: number
|
|
411
|
+
fingerprint: RenderFingerprint
|
|
412
|
+
render: MessageRender
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const renderMemo = new WeakMap<Message, RenderMemoEntry>()
|
|
416
|
+
|
|
417
|
+
function fingerprintOf(message: Message): RenderFingerprint {
|
|
418
|
+
switch (message.kind) {
|
|
419
|
+
case 'bubble':
|
|
420
|
+
return {
|
|
421
|
+
kind: 'bubble',
|
|
422
|
+
content: message.content,
|
|
423
|
+
role: message.role,
|
|
424
|
+
origin: message.origin,
|
|
425
|
+
streaming: message.streaming,
|
|
426
|
+
}
|
|
427
|
+
case 'collapsible':
|
|
428
|
+
return {
|
|
429
|
+
kind: 'collapsible',
|
|
430
|
+
body: message.body,
|
|
431
|
+
label: message.label,
|
|
432
|
+
collapsed: message.collapsed,
|
|
433
|
+
running: message.running,
|
|
434
|
+
thinking: message.thinking,
|
|
435
|
+
streaming: message.streaming,
|
|
436
|
+
}
|
|
437
|
+
case 'tool-card':
|
|
438
|
+
return {
|
|
439
|
+
kind: 'tool-card',
|
|
440
|
+
tool: message.tool,
|
|
441
|
+
label: message.label,
|
|
442
|
+
argsBody: message.argsBody,
|
|
443
|
+
resultBody: message.resultBody,
|
|
444
|
+
diff: message.diff,
|
|
445
|
+
read: message.read,
|
|
446
|
+
nested: message.nested,
|
|
447
|
+
error: message.error,
|
|
448
|
+
failed: message.failed,
|
|
449
|
+
exitCode: message.exitCode,
|
|
450
|
+
signal: message.signal,
|
|
451
|
+
bodyCol: message.bodyCol,
|
|
452
|
+
running: message.running,
|
|
453
|
+
streaming: message.streaming,
|
|
454
|
+
}
|
|
455
|
+
case 'compaction':
|
|
456
|
+
return {
|
|
457
|
+
kind: 'compaction',
|
|
458
|
+
summary: message.summary,
|
|
459
|
+
running: message.running,
|
|
460
|
+
error: message.error,
|
|
461
|
+
}
|
|
462
|
+
case 'custom':
|
|
463
|
+
return {
|
|
464
|
+
kind: 'custom',
|
|
465
|
+
view: message.view,
|
|
466
|
+
data: message.data,
|
|
467
|
+
running: message.running,
|
|
468
|
+
streaming: message.streaming,
|
|
469
|
+
}
|
|
470
|
+
case 'home-logo':
|
|
471
|
+
return {
|
|
472
|
+
kind: 'home-logo',
|
|
473
|
+
lines: message.lines,
|
|
474
|
+
colors: message.colors,
|
|
475
|
+
info: message.info,
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function fingerprintMatches(entry: RenderFingerprint, message: Message): boolean {
|
|
481
|
+
switch (entry.kind) {
|
|
482
|
+
case 'bubble':
|
|
483
|
+
return message.kind === 'bubble'
|
|
484
|
+
&& entry.content === message.content
|
|
485
|
+
&& entry.role === message.role
|
|
486
|
+
&& entry.origin === message.origin
|
|
487
|
+
&& entry.streaming === message.streaming
|
|
488
|
+
case 'collapsible':
|
|
489
|
+
return message.kind === 'collapsible'
|
|
490
|
+
&& entry.body === message.body
|
|
491
|
+
&& entry.label === message.label
|
|
492
|
+
&& entry.collapsed === message.collapsed
|
|
493
|
+
&& entry.running === message.running
|
|
494
|
+
&& entry.thinking === message.thinking
|
|
495
|
+
&& entry.streaming === message.streaming
|
|
496
|
+
case 'tool-card':
|
|
497
|
+
return message.kind === 'tool-card'
|
|
498
|
+
&& entry.tool === message.tool
|
|
499
|
+
&& entry.label === message.label
|
|
500
|
+
&& entry.argsBody === message.argsBody
|
|
501
|
+
&& entry.resultBody === message.resultBody
|
|
502
|
+
&& entry.diff === message.diff
|
|
503
|
+
&& entry.read === message.read
|
|
504
|
+
&& entry.nested === message.nested
|
|
505
|
+
&& entry.error === message.error
|
|
506
|
+
&& entry.failed === message.failed
|
|
507
|
+
&& entry.exitCode === message.exitCode
|
|
508
|
+
&& entry.signal === message.signal
|
|
509
|
+
&& entry.bodyCol === message.bodyCol
|
|
510
|
+
&& entry.running === message.running
|
|
511
|
+
&& entry.streaming === message.streaming
|
|
512
|
+
case 'compaction':
|
|
513
|
+
return message.kind === 'compaction'
|
|
514
|
+
&& entry.summary === message.summary
|
|
515
|
+
&& entry.running === message.running
|
|
516
|
+
&& entry.error === message.error
|
|
517
|
+
case 'custom':
|
|
518
|
+
return message.kind === 'custom'
|
|
519
|
+
&& entry.view === message.view
|
|
520
|
+
&& entry.data === message.data
|
|
521
|
+
&& entry.running === message.running
|
|
522
|
+
&& entry.streaming === message.streaming
|
|
523
|
+
case 'home-logo':
|
|
524
|
+
return message.kind === 'home-logo'
|
|
525
|
+
&& entry.lines === message.lines
|
|
526
|
+
&& entry.colors === message.colors
|
|
527
|
+
&& entry.info === message.info
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function renderFor(message: Message, width: number): MessageRender {
|
|
532
|
+
const memo = renderMemo.get(message)
|
|
533
|
+
if (memo !== undefined && memo.epoch === layoutEpoch && memo.width === width && fingerprintMatches(memo.fingerprint, message)) {
|
|
534
|
+
return memo.render
|
|
535
|
+
}
|
|
536
|
+
const body = renderBody(message, width)
|
|
537
|
+
const render: MessageRender = { ...body, plan: planFor(message, body) }
|
|
538
|
+
renderMemo.set(message, { epoch: layoutEpoch, width, fingerprint: fingerprintOf(message), render })
|
|
539
|
+
return render
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function lineCount(message: Message, width: number): number {
|
|
543
|
+
return renderFor(message, width).plan.length
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export interface RowIndex {
|
|
547
|
+
total: number
|
|
548
|
+
rowAt(row: number): RowInfo | null
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function buildRowIndex(messages: Message[], width: number): RowIndex {
|
|
552
|
+
const starts = new Array<number>(messages.length)
|
|
553
|
+
const counts = new Array<number>(messages.length)
|
|
554
|
+
let total = 0
|
|
555
|
+
for (let i = 0; i < messages.length; i++) {
|
|
556
|
+
starts[i] = total
|
|
557
|
+
const count = lineCount(messages[i]!, width)
|
|
558
|
+
counts[i] = count
|
|
559
|
+
total += count
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
total,
|
|
563
|
+
rowAt(row: number): RowInfo | null {
|
|
564
|
+
if (row < 0 || row >= total) return null
|
|
565
|
+
let low = 0
|
|
566
|
+
let high = messages.length - 1
|
|
567
|
+
while (low < high) {
|
|
568
|
+
const mid = (low + high + 1) >> 1
|
|
569
|
+
if (starts[mid]! <= row) low = mid
|
|
570
|
+
else high = mid - 1
|
|
571
|
+
}
|
|
572
|
+
return rowInfo(messages[low]!, low, row - starts[low]!, width)
|
|
573
|
+
},
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function rowCount(messages: Message[], width: number): number {
|
|
578
|
+
return rowIndexFor(messages, width).total
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
let indexMessages: Message[] | undefined
|
|
582
|
+
let indexWidth = -1
|
|
583
|
+
let indexCache: RowIndex | undefined
|
|
584
|
+
|
|
585
|
+
export function rowIndexFor(messages: Message[], width: number): RowIndex {
|
|
586
|
+
if (indexCache !== undefined && indexMessages === messages && indexWidth === width) {
|
|
587
|
+
return indexCache
|
|
588
|
+
}
|
|
589
|
+
indexMessages = messages
|
|
590
|
+
indexWidth = width
|
|
591
|
+
indexCache = buildRowIndex(messages, width)
|
|
592
|
+
return indexCache
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function rowInfoAt(messages: Message[], width: number, row: number): RowInfo | null {
|
|
596
|
+
return rowIndexFor(messages, width).rowAt(row)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const segKeyMemo = new WeakMap<Segment[], string>()
|
|
600
|
+
|
|
601
|
+
function segmentsKeyCached(segments: Segment[]): string {
|
|
602
|
+
const hit = segKeyMemo.get(segments)
|
|
603
|
+
if (hit !== undefined) return hit
|
|
604
|
+
const key = segmentsKey(segments)
|
|
605
|
+
segKeyMemo.set(segments, key)
|
|
606
|
+
return key
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function rowInfo(message: Message, index: number, offset: number, width: number): RowInfo {
|
|
610
|
+
const render = renderFor(message, width)
|
|
611
|
+
const plan = render.plan[offset]
|
|
612
|
+
const base: RowInfo = {
|
|
613
|
+
messageIndex: index,
|
|
614
|
+
messageId: message.id,
|
|
615
|
+
lineNo: offset,
|
|
616
|
+
kind: 'blank',
|
|
617
|
+
text: '',
|
|
618
|
+
colStart: 0,
|
|
619
|
+
selectable: false,
|
|
620
|
+
clickable: false,
|
|
621
|
+
background: false,
|
|
622
|
+
backgroundWidth: messageBackgroundWidth(width),
|
|
623
|
+
muted: false,
|
|
624
|
+
label: '',
|
|
625
|
+
collapsed: false,
|
|
626
|
+
thinking: false,
|
|
627
|
+
role: undefined,
|
|
628
|
+
}
|
|
629
|
+
if (plan === undefined) return base
|
|
630
|
+
switch (plan.type) {
|
|
631
|
+
case 'pad':
|
|
632
|
+
return { ...base, kind: 'pad', background: true, role: plan.role }
|
|
633
|
+
case 'blank':
|
|
634
|
+
return base
|
|
635
|
+
case 'header':
|
|
636
|
+
return {
|
|
637
|
+
...base,
|
|
638
|
+
kind: 'header',
|
|
639
|
+
colStart: HEADER_LABEL_COL,
|
|
640
|
+
clickable: plan.clickable,
|
|
641
|
+
label: fitLabel(plan.label, width),
|
|
642
|
+
collapsed: plan.collapsed,
|
|
643
|
+
spinner: Boolean(
|
|
644
|
+
(message.kind === 'collapsible' || message.kind === 'custom')
|
|
645
|
+
&& (message.running || message.streaming),
|
|
646
|
+
) || undefined,
|
|
647
|
+
}
|
|
648
|
+
case 'lit':
|
|
649
|
+
return {
|
|
650
|
+
...base,
|
|
651
|
+
kind: 'text',
|
|
652
|
+
text: fitLabel(plan.text, width),
|
|
653
|
+
colStart: plan.colStart,
|
|
654
|
+
selectable: plan.selectable,
|
|
655
|
+
background: plan.bg,
|
|
656
|
+
muted: plan.muted,
|
|
657
|
+
role: plan.role,
|
|
658
|
+
...(plan.wave === true ? { wave: true } : {}),
|
|
659
|
+
...(plan.segments === undefined ? {} : { segments: plan.segments, segKey: segmentsKeyCached(plan.segments) }),
|
|
660
|
+
}
|
|
661
|
+
case 'body': {
|
|
662
|
+
const lineBg = render.bgs?.[plan.line]
|
|
663
|
+
const segments = render.rows?.[plan.line]
|
|
664
|
+
return {
|
|
665
|
+
...base,
|
|
666
|
+
kind: 'text',
|
|
667
|
+
text: render.lines[plan.line] ?? '',
|
|
668
|
+
colStart: plan.colStart,
|
|
669
|
+
selectable: plan.selectable,
|
|
670
|
+
background: plan.bg || lineBg !== undefined,
|
|
671
|
+
muted: plan.muted,
|
|
672
|
+
role: plan.role,
|
|
673
|
+
...(plan.wave === true ? { wave: true } : {}),
|
|
674
|
+
...(lineBg === undefined ? {} : { lineBg }),
|
|
675
|
+
...(segments === undefined ? {} : { segments, segKey: segmentsKeyCached(segments) }),
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export function fitLabel(label: string, width: number): string {
|
|
682
|
+
const available = Math.max(1, width - 8)
|
|
683
|
+
if (textWidth(label) <= available) return label
|
|
684
|
+
const bracket = label.indexOf('[')
|
|
685
|
+
if (bracket < 0) {
|
|
686
|
+
const cut = truncate(label, Math.max(1, available - 3))
|
|
687
|
+
return cut.length === label.length ? cut : `${cut}...`
|
|
688
|
+
}
|
|
689
|
+
const name = label.slice(0, bracket + 1)
|
|
690
|
+
const rest = label.slice(bracket + 1)
|
|
691
|
+
const budget = Math.max(1, available - textWidth(name) - 4)
|
|
692
|
+
const cut = truncate(rest, budget)
|
|
693
|
+
return `${name}${cut.length === rest.length ? rest : `${cut}...`}]`
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export function headerSymbol(collapsed: boolean): string {
|
|
697
|
+
return collapsed ? glyphs.headerCollapsed : glyphs.headerExpanded
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export interface ScrollbarGeometry {
|
|
701
|
+
top: number
|
|
702
|
+
height: number
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export function scrollbarGeometry(total: number, height: number, scrollTop: number): ScrollbarGeometry | null {
|
|
706
|
+
if (total <= height || height < 1) return null
|
|
707
|
+
const thumbHeight = Math.max(1, Math.floor((height * height) / total))
|
|
708
|
+
const maxScroll = total - height
|
|
709
|
+
const travel = height - thumbHeight
|
|
710
|
+
const top = maxScroll === 0 ? 0 : Math.min(travel, Math.round((travel * scrollTop) / maxScroll))
|
|
711
|
+
return { top, height: thumbHeight }
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function selectionText(messages: Message[], width: number, selection: LineSelection): string {
|
|
715
|
+
const index = rowIndexFor(messages, width)
|
|
716
|
+
const total = index.total
|
|
717
|
+
const top = Math.max(0, Math.min(selection.anchorRow, selection.focusRow))
|
|
718
|
+
const bottom = Math.min(Math.max(selection.anchorRow, selection.focusRow), total - 1)
|
|
719
|
+
const envStart = Math.min(selection.anchorCol, selection.focusCol)
|
|
720
|
+
const envEnd = Math.max(selection.anchorCol, selection.focusCol)
|
|
721
|
+
const lines: string[] = []
|
|
722
|
+
for (let row = top; row <= bottom; row++) {
|
|
723
|
+
const info = index.rowAt(row)
|
|
724
|
+
if (info === null) continue
|
|
725
|
+
if (info.kind === 'pad') continue
|
|
726
|
+
const range = selectedRange(selection, row)
|
|
727
|
+
if (range === null) continue
|
|
728
|
+
if (info.kind === 'header') {
|
|
729
|
+
let line = ''
|
|
730
|
+
let prevEnd = -1
|
|
731
|
+
const pieces: Array<{ text: string; col: number }> = [
|
|
732
|
+
{ text: ` ${headerSymbol(info.collapsed)} `, col: 0 },
|
|
733
|
+
{ text: info.label, col: HEADER_LABEL_COL },
|
|
734
|
+
]
|
|
735
|
+
for (const piece of pieces) {
|
|
736
|
+
const pieceWidth = textWidth(piece.text)
|
|
737
|
+
if (!(envEnd > piece.col && envStart < piece.col + pieceWidth)) continue
|
|
738
|
+
const start = Math.max(range.start, piece.col)
|
|
739
|
+
const end = Math.min(range.end, piece.col + pieceWidth)
|
|
740
|
+
if (start >= end) continue
|
|
741
|
+
const text = sliceByColumns(piece.text, start - piece.col, end - piece.col)
|
|
742
|
+
if (text === '') continue
|
|
743
|
+
if (line !== '') line += prevEnd === piece.col ? '' : ' '
|
|
744
|
+
line += text
|
|
745
|
+
prevEnd = piece.col + pieceWidth
|
|
746
|
+
}
|
|
747
|
+
lines.push(line)
|
|
748
|
+
continue
|
|
749
|
+
}
|
|
750
|
+
const line = info.text
|
|
751
|
+
const lineWidth = textWidth(line)
|
|
752
|
+
const left = Math.max(range.start, info.colStart)
|
|
753
|
+
const right = Math.min(range.end, info.colStart + lineWidth)
|
|
754
|
+
if (left < right) {
|
|
755
|
+
const startIndex = colToCharIndex(line, left - info.colStart)
|
|
756
|
+
const endIndex = colToCharIndex(line, right - info.colStart)
|
|
757
|
+
lines.push(expandFieldChars(line.slice(startIndex, endIndex)))
|
|
758
|
+
continue
|
|
759
|
+
}
|
|
760
|
+
lines.push('')
|
|
761
|
+
}
|
|
762
|
+
return lines.join('\n').replace(/[ \t]+$/gm, '').replace(/\n+$/, '')
|
|
763
|
+
}
|