dsh-code 1.4.0 → 1.5.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/README.en.md +10 -8
- package/README.md +10 -8
- package/lib/index.mjs +2001 -1688
- package/lib/session-query.mjs +18 -1
- package/lib/types/app.d.ts +21 -15
- package/lib/types/composer.d.ts +1 -1
- package/lib/types/locales/en.d.ts +4 -4
- package/lib/types/render/inspector.d.ts +8 -0
- package/lib/types/render/projection-events.d.ts +33 -0
- package/lib/types/render/transcript-viewport.d.ts +32 -0
- package/lib/types/runner/session-io.d.ts +11 -0
- package/lib/types/session-query.d.ts +11 -0
- package/package.json +3 -2
- package/src/app.ts +286 -123
- package/src/composer.ts +6 -6
- package/src/locales/en.ts +4 -4
- package/src/locales/zh.ts +4 -4
- package/src/render/inspector.ts +12 -0
- package/src/render/lines.ts +45 -18
- package/src/render/projection-events.ts +109 -0
- package/src/render/projection.ts +11 -2
- package/src/render/tool-preview.ts +35 -2
- package/src/render/transcript-viewport.ts +61 -0
- package/src/render/usage.ts +1 -2
- package/src/runner/session-io.ts +43 -4
- package/src/session-query.ts +22 -0
package/src/app.ts
CHANGED
|
@@ -160,10 +160,12 @@ import {
|
|
|
160
160
|
type StatusTone,
|
|
161
161
|
} from './render/status.ts'
|
|
162
162
|
import { displayTail, displayText, padColumns, singleLineText, truncateColumns } from './render/text.ts'
|
|
163
|
+
import { advanceTranscriptViewport, visibleTranscriptRows } from './render/transcript-viewport.ts'
|
|
163
164
|
import {
|
|
164
165
|
clampScroll,
|
|
165
166
|
followInspectorCursor,
|
|
166
167
|
inspectorViewport,
|
|
168
|
+
inspectableTranscriptEntries,
|
|
167
169
|
layoutGutterRows,
|
|
168
170
|
liveRegionBudget,
|
|
169
171
|
moveScroll,
|
|
@@ -179,6 +181,7 @@ import {
|
|
|
179
181
|
styledLines,
|
|
180
182
|
textLines,
|
|
181
183
|
transcriptEntryLines,
|
|
184
|
+
type StyledLine,
|
|
182
185
|
} from './render/lines.ts'
|
|
183
186
|
import {
|
|
184
187
|
composerMaxRows,
|
|
@@ -362,8 +365,8 @@ export interface AppProps {
|
|
|
362
365
|
saveLanguage: (name: LanguageName) => void
|
|
363
366
|
/** Apply and persist one /theme selection; the runner owns the theme.json file. */
|
|
364
367
|
saveTheme?: (name: ThemeName) => void
|
|
365
|
-
/** Whether
|
|
366
|
-
*
|
|
368
|
+
/** Whether decorative animations run at startup (animations.json; on by
|
|
369
|
+
* default). Functional activity indicators remain live when disabled. */
|
|
367
370
|
animations?: boolean
|
|
368
371
|
/** Apply and persist one /animation toggle; the runner owns the file. */
|
|
369
372
|
saveAnimations?: (enabled: boolean) => void
|
|
@@ -377,24 +380,23 @@ export interface AppProps {
|
|
|
377
380
|
applyEditorKeys: () => Promise<string>
|
|
378
381
|
}
|
|
379
382
|
|
|
380
|
-
/**
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
*/
|
|
384
|
-
function Caret({ animated = true }: { animated?: boolean }): ReactElement {
|
|
385
|
-
const tick = useFrames(CARET_BLINK_TICK_MS, animated)
|
|
383
|
+
/** Functional streaming caret: activity remains visible in reduced motion. */
|
|
384
|
+
function Caret(): ReactElement {
|
|
385
|
+
const tick = useFrames(CARET_BLINK_TICK_MS, true)
|
|
386
386
|
return createElement(Text, null, caretVisible(tick) ? '▍' : ' ')
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
/** One resettable input-caret phase shared by the entire composer. */
|
|
390
|
-
function ShimmerLine({ text,
|
|
391
|
-
|
|
390
|
+
function ShimmerLine({ text, themeFlowAnimated = true }: { text: string; themeFlowAnimated?: boolean }): ReactElement {
|
|
391
|
+
// Shimmer is functional activity feedback and always advances. The setting
|
|
392
|
+
// only gates decorative movement through a flowing theme's anchor colors.
|
|
393
|
+
const tick = useFrames(DEEP_DIVING_SHIMMER_TICK_MS, true)
|
|
392
394
|
const palette = getPalette()
|
|
393
395
|
// Flowing themes walk their anchors for the shimmer highlight so
|
|
394
396
|
// streaming text glows along the spectrum; other themes keep the bright
|
|
395
397
|
// accent.
|
|
396
398
|
const flow = themeFlow()
|
|
397
|
-
const highlight = flow !== undefined &&
|
|
399
|
+
const highlight = flow !== undefined && themeFlowAnimated
|
|
398
400
|
? flowColor(tick * DEEP_DIVING_SHIMMER_TICK_MS + flow.phaseMs, flow.anchors)
|
|
399
401
|
: palette.brandBright
|
|
400
402
|
const graphemes = splitGraphemes(text)
|
|
@@ -407,11 +409,9 @@ function ShimmerLine({ text, animated = true }: { text: string; animated?: boole
|
|
|
407
409
|
Text,
|
|
408
410
|
{
|
|
409
411
|
key: `${grapheme.start}-${grapheme.end}`,
|
|
410
|
-
color: inkColor(
|
|
411
|
-
? (
|
|
412
|
-
:
|
|
413
|
-
? deepDivingSparkColor(tick, palette.brandDeep, highlight)
|
|
414
|
-
: deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, highlight)),
|
|
412
|
+
color: inkColor(sparkle
|
|
413
|
+
? deepDivingSparkColor(tick, palette.brandDeep, highlight)
|
|
414
|
+
: deepDivingGradientColor(index, tick, graphemes.length, palette.brandDeep, highlight)),
|
|
415
415
|
bold: sparkle || undefined,
|
|
416
416
|
},
|
|
417
417
|
grapheme.text,
|
|
@@ -426,10 +426,14 @@ function ShimmerLine({ text, animated = true }: { text: string; animated?: boole
|
|
|
426
426
|
* only once the turn has clearly been running (15s) — anchored to `turn/start`
|
|
427
427
|
* so a resumed mid-turn keeps the real time.
|
|
428
428
|
*/
|
|
429
|
-
function DeepDivingLine({ since,
|
|
429
|
+
function DeepDivingLine({ since, themeFlowAnimated = true }: { since: number; themeFlowAnimated?: boolean }): ReactElement {
|
|
430
|
+
// Elapsed time is functional progress, not decoration. Own a low-rate tick
|
|
431
|
+
// so the clock advances independently of transcript/store updates.
|
|
432
|
+
const elapsedTick = useFrames(1_000, true)
|
|
433
|
+
void elapsedTick
|
|
430
434
|
const elapsed = since === 0 ? 0 : Date.now() - since
|
|
431
435
|
const text = elapsed >= 15_000 ? `✻ Deep diving... ${runClock(elapsed)}` : '✻ Deep diving...'
|
|
432
|
-
return createElement(ShimmerLine, { text,
|
|
436
|
+
return createElement(ShimmerLine, { text, themeFlowAnimated })
|
|
433
437
|
}
|
|
434
438
|
|
|
435
439
|
/**
|
|
@@ -451,6 +455,32 @@ export function streamTailBodyColumns(rowColumns: number, prefix: string, contin
|
|
|
451
455
|
return Math.max(1, width - prefixColumns)
|
|
452
456
|
}
|
|
453
457
|
|
|
458
|
+
interface StreamTailLayout {
|
|
459
|
+
readonly text: string
|
|
460
|
+
readonly truncated: boolean
|
|
461
|
+
readonly rows: number
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Shared physical layout for streaming render and viewport row accounting. */
|
|
465
|
+
function streamTailLayout(text: string, columns: number, maxRows: number, prefix: string, continuationPrefix: string): StreamTailLayout {
|
|
466
|
+
const safeRows = Math.max(1, maxRows)
|
|
467
|
+
const contentColumns = streamTailBodyColumns(columns, prefix, continuationPrefix)
|
|
468
|
+
const initial = displayTail(text, contentColumns, safeRows)
|
|
469
|
+
const tail = initial.truncated && safeRows > 1
|
|
470
|
+
? displayTail(text, contentColumns, safeRows - 1)
|
|
471
|
+
: initial
|
|
472
|
+
return {
|
|
473
|
+
text: tail.text,
|
|
474
|
+
truncated: tail.truncated,
|
|
475
|
+
rows: tail.text.split('\n').length + (tail.truncated && safeRows > 1 ? 1 : 0),
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function streamTailPhysicalRows(text: string, columns: number, maxRows: number, prefix = '', continuationPrefix = prefix): number {
|
|
480
|
+
if (text === '' || maxRows <= 0) return 0
|
|
481
|
+
return streamTailLayout(text, columns, maxRows, prefix, continuationPrefix).rows
|
|
482
|
+
}
|
|
483
|
+
|
|
454
484
|
function StreamTail({ text, dim, maxRows, prefix = '', continuationPrefix = prefix, children, columns }: {
|
|
455
485
|
text: string
|
|
456
486
|
dim: boolean
|
|
@@ -463,14 +493,8 @@ function StreamTail({ text, dim, maxRows, prefix = '', continuationPrefix = pref
|
|
|
463
493
|
}): ReactElement {
|
|
464
494
|
const safeRows = Math.max(1, maxRows)
|
|
465
495
|
// Both prefixes participate because every physical row repeats its hanging
|
|
466
|
-
// indent.
|
|
467
|
-
|
|
468
|
-
const contentColumns = streamTailBodyColumns(columns, prefix, continuationPrefix)
|
|
469
|
-
const initial = displayTail(text, contentColumns, safeRows)
|
|
470
|
-
// Reserve one row for the omission marker only when a marker is needed.
|
|
471
|
-
const tail = initial.truncated && safeRows > 1
|
|
472
|
-
? displayTail(text, contentColumns, safeRows - 1)
|
|
473
|
-
: initial
|
|
496
|
+
// indent. Rendering and viewport accounting consume the exact same layout.
|
|
497
|
+
const tail = streamTailLayout(text, columns, safeRows, prefix, continuationPrefix)
|
|
474
498
|
const rows = tail.text.split('\n')
|
|
475
499
|
return createElement(
|
|
476
500
|
Box,
|
|
@@ -1066,7 +1090,9 @@ function StatusLine({ facts, stats, busy, columns, items, onRows, animated }: {
|
|
|
1066
1090
|
const row2Present = layout.row2.left.length > 0
|
|
1067
1091
|
return createElement(
|
|
1068
1092
|
Box,
|
|
1069
|
-
|
|
1093
|
+
// Do not stretch back to the parent width: VS Code autowraps a painted
|
|
1094
|
+
// row at exactly stdout.columns, creating one unreported physical row.
|
|
1095
|
+
{ flexDirection: 'column', width: columns },
|
|
1070
1096
|
renderRow(layout.row1, 's1'),
|
|
1071
1097
|
row2Present ? renderRow(layout.row2, 's2', STATUS_ROW2_INDENT) : undefined,
|
|
1072
1098
|
)
|
|
@@ -1379,16 +1405,18 @@ function StaticTranscript({ items }: { items: ReactElement[] }): ReactElement {
|
|
|
1379
1405
|
|
|
1380
1406
|
const MemoStaticTranscript = memo(StaticTranscript)
|
|
1381
1407
|
|
|
1408
|
+
interface SettledPhysicalRow {
|
|
1409
|
+
/** Stable one-row Static element. */
|
|
1410
|
+
readonly element: ReactElement
|
|
1411
|
+
/** The same row model reused by the mutable viewport tail. */
|
|
1412
|
+
readonly line: StyledLine
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1382
1415
|
interface SettledRowRecord {
|
|
1383
|
-
/**
|
|
1384
|
-
|
|
1385
|
-
/**
|
|
1386
|
-
|
|
1387
|
-
/** The roomy-prompt spacer AFTER the row, or undefined. */
|
|
1388
|
-
after: ReactElement | undefined
|
|
1389
|
-
/** Physical rows this record contributes (row body plus spacers) — the
|
|
1390
|
-
* unit of the rendered-history cap. */
|
|
1391
|
-
rows: number
|
|
1416
|
+
/** Physical rows for one settled entry, including roomy-prompt spacers. */
|
|
1417
|
+
readonly physical: readonly SettledPhysicalRow[]
|
|
1418
|
+
/** Physical rows this record contributes — the rendered-history-cap unit. */
|
|
1419
|
+
readonly rows: number
|
|
1392
1420
|
}
|
|
1393
1421
|
|
|
1394
1422
|
/** The incremental settled-history cache (see `computeSettledRows`). */
|
|
@@ -1409,10 +1437,13 @@ interface SettledRowsCache {
|
|
|
1409
1437
|
epoch: number
|
|
1410
1438
|
/** The terminal width the rows were wrapped for; a change forces a rebuild. */
|
|
1411
1439
|
columns: number
|
|
1412
|
-
/**
|
|
1440
|
+
/** Header/hint followed by one element per physical transcript row. */
|
|
1413
1441
|
flat: ReactElement[]
|
|
1442
|
+
/** Physical transcript rows only (header/hint excluded). */
|
|
1443
|
+
physical: SettledPhysicalRow[]
|
|
1414
1444
|
/** Settled entries dropped from the window's head (rendering only — the
|
|
1415
|
-
* event log keeps everything;
|
|
1445
|
+
* event log keeps everything; /export reads all of it and Ctrl+O reads its
|
|
1446
|
+
* inspectable entries). */
|
|
1416
1447
|
droppedEntries: number
|
|
1417
1448
|
/** Physical rows the window's entries contribute (excludes header/hint). */
|
|
1418
1449
|
totalRows: number
|
|
@@ -1429,25 +1460,29 @@ interface SettledRowsResult {
|
|
|
1429
1460
|
built: number
|
|
1430
1461
|
}
|
|
1431
1462
|
|
|
1432
|
-
/** Build one settled
|
|
1463
|
+
/** Build one settled entry as stable physical rows shared by Static and viewport. */
|
|
1433
1464
|
function buildSettledRow(entry: TranscriptEntry, index: number, showReasoning: boolean, columns: number): SettledRowRecord {
|
|
1434
|
-
// The SAME physical-row pipeline as the live tail (settledEntryLines).
|
|
1435
1465
|
// Every row carries its own two-column prefix (user ❯, reply body, tool
|
|
1436
|
-
// cards), which is the whole gutter
|
|
1437
|
-
//
|
|
1438
|
-
//
|
|
1439
|
-
const roomyPrompt = entry.kind === 'user' && !entry.notice
|
|
1466
|
+
// cards), which is the whole gutter. Physical row identity—not entry
|
|
1467
|
+
// identity—is the viewport currency, so an oversized entry can split cleanly
|
|
1468
|
+
// between native scrollback and the bottom-anchored live tail.
|
|
1440
1469
|
const lines = settledEntryLines(entry, Math.max(10, columns - 2), showReasoning)
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
:
|
|
1449
|
-
|
|
1470
|
+
const physical: SettledPhysicalRow[] = lines.map((line, row) => ({
|
|
1471
|
+
line,
|
|
1472
|
+
element: createElement(Box, { key: `entry-${index}-row-${row}` }, createElement(StyledRows, { lines: [line] })),
|
|
1473
|
+
}))
|
|
1474
|
+
if (entry.kind === 'user' && !entry.notice) {
|
|
1475
|
+
const blank: StyledLine = { segments: [] }
|
|
1476
|
+
physical.unshift({
|
|
1477
|
+
line: blank,
|
|
1478
|
+
element: createElement(Box, { key: `prompt-before-${index}`, paddingX: 1 }, createElement(Text, null, ' ')),
|
|
1479
|
+
})
|
|
1480
|
+
physical.push({
|
|
1481
|
+
line: blank,
|
|
1482
|
+
element: createElement(Box, { key: `prompt-after-${index}`, paddingX: 1 }, createElement(Text, null, ' ')),
|
|
1483
|
+
})
|
|
1450
1484
|
}
|
|
1485
|
+
return { physical, rows: physical.length }
|
|
1451
1486
|
}
|
|
1452
1487
|
|
|
1453
1488
|
/** The dim hint row placed under the header once the window has dropped entries. */
|
|
@@ -1474,8 +1509,9 @@ function settledTrimHint(droppedEntries: number, columns: number): ReactElement
|
|
|
1474
1509
|
*
|
|
1475
1510
|
* RENDERED-HISTORY CAP: the window holds at most `rowCap` physical rows of
|
|
1476
1511
|
* settled transcript (header and hint reserved on top). The cap exists only
|
|
1477
|
-
* here — the event log, the store projection, /export,
|
|
1478
|
-
*
|
|
1512
|
+
* here — the event log, the store projection, /export, and /resume keep the
|
|
1513
|
+
* full history; Ctrl+O keeps its inspectable subset. Ink 5's <Static> is a
|
|
1514
|
+
* consumption counter
|
|
1479
1515
|
* (items.slice(index) keyed on length): deleting head items mid-stream while
|
|
1480
1516
|
* appending tail items can permanently swallow new rows, so the append branch
|
|
1481
1517
|
* NEVER drops the head — it only accounts rows and flags `needsTrim` once the
|
|
@@ -1509,7 +1545,7 @@ export function computeSettledRows(
|
|
|
1509
1545
|
// Full rebuild at the CURRENT fold state, newest-first so the cap keeps
|
|
1510
1546
|
// whole entries and never even parses dropped ones.
|
|
1511
1547
|
const records = new Map<TranscriptEntry, SettledRowRecord>()
|
|
1512
|
-
const
|
|
1548
|
+
const physical: SettledPhysicalRow[] = []
|
|
1513
1549
|
let windowRows = 0
|
|
1514
1550
|
let droppedEntries = 0
|
|
1515
1551
|
let index = settled - 1
|
|
@@ -1524,14 +1560,13 @@ export function computeSettledRows(
|
|
|
1524
1560
|
}
|
|
1525
1561
|
records.set(entry, record)
|
|
1526
1562
|
windowRows += record.rows
|
|
1527
|
-
|
|
1528
|
-
window.unshift(record.box)
|
|
1529
|
-
if (record.before !== undefined) window.unshift(record.before)
|
|
1563
|
+
physical.unshift(...record.physical)
|
|
1530
1564
|
}
|
|
1531
1565
|
const header = createElement(Header, { key: 'header', resumed })
|
|
1566
|
+
const rowElements = physical.map(row => row.element)
|
|
1532
1567
|
const flat = droppedEntries > 0
|
|
1533
|
-
? [header, settledTrimHint(droppedEntries, columns), ...
|
|
1534
|
-
: [header, ...
|
|
1568
|
+
? [header, settledTrimHint(droppedEntries, columns), ...rowElements]
|
|
1569
|
+
: [header, ...rowElements]
|
|
1535
1570
|
return {
|
|
1536
1571
|
cache: {
|
|
1537
1572
|
entries: entries.slice(droppedEntries, settled),
|
|
@@ -1542,6 +1577,7 @@ export function computeSettledRows(
|
|
|
1542
1577
|
epoch,
|
|
1543
1578
|
columns,
|
|
1544
1579
|
flat,
|
|
1580
|
+
physical,
|
|
1545
1581
|
droppedEntries,
|
|
1546
1582
|
totalRows: windowRows,
|
|
1547
1583
|
needsTrim: false,
|
|
@@ -1565,7 +1601,7 @@ export function computeSettledRows(
|
|
|
1565
1601
|
// overflow only flags the cache for one trimming replay.
|
|
1566
1602
|
const records = previous.records
|
|
1567
1603
|
const suffix: TranscriptEntry[] = []
|
|
1568
|
-
const added:
|
|
1604
|
+
const added: SettledPhysicalRow[] = []
|
|
1569
1605
|
let deltaRows = 0
|
|
1570
1606
|
for (let index = previous.entries.length + previous.droppedEntries; index < settled; index++) {
|
|
1571
1607
|
const entry = entries[index]
|
|
@@ -1573,9 +1609,7 @@ export function computeSettledRows(
|
|
|
1573
1609
|
records.set(entry, record)
|
|
1574
1610
|
suffix.push(entry)
|
|
1575
1611
|
deltaRows += record.rows
|
|
1576
|
-
|
|
1577
|
-
added.push(record.box)
|
|
1578
|
-
if (record.after !== undefined) added.push(record.after)
|
|
1612
|
+
added.push(...record.physical)
|
|
1579
1613
|
}
|
|
1580
1614
|
const totalRows = previous.totalRows + deltaRows
|
|
1581
1615
|
const needsTrim = rowCap > 0 && totalRows > rowCap + Math.floor(rowCap / 4)
|
|
@@ -1588,7 +1622,8 @@ export function computeSettledRows(
|
|
|
1588
1622
|
showReasoning,
|
|
1589
1623
|
epoch: previous.epoch,
|
|
1590
1624
|
columns: previous.columns,
|
|
1591
|
-
flat: previous.flat.concat(added),
|
|
1625
|
+
flat: previous.flat.concat(added.map(row => row.element)),
|
|
1626
|
+
physical: previous.physical.concat(added),
|
|
1592
1627
|
droppedEntries: previous.droppedEntries,
|
|
1593
1628
|
totalRows,
|
|
1594
1629
|
needsTrim,
|
|
@@ -1935,6 +1970,12 @@ export function App(props: AppProps): ReactElement {
|
|
|
1935
1970
|
// The Ctrl+O inspector is the one surface the composer already yields to
|
|
1936
1971
|
// through verboseOpen; it rides the same gate without a panel row.
|
|
1937
1972
|
const inspectorVisible = verboseOpen && !approvalPending && !questionPending
|
|
1973
|
+
// Filtering is inspector-only: the durable log and ordinary transcript keep
|
|
1974
|
+
// every reasoning settlement. Avoid scanning long histories while closed.
|
|
1975
|
+
const verboseEntries = useMemo(
|
|
1976
|
+
() => verboseOpen ? inspectableTranscriptEntries(view.entries) : [],
|
|
1977
|
+
[verboseOpen, view.entries],
|
|
1978
|
+
)
|
|
1938
1979
|
const modalVisible = openPanel !== undefined || inspectorVisible || approvalPending || questionPending
|
|
1939
1980
|
// While a deletion waits for y/n, the composer takes the keys (the resume
|
|
1940
1981
|
// panel yields): the confirm is typed IN the input box, not as an invisible
|
|
@@ -1955,21 +1996,17 @@ export function App(props: AppProps): ReactElement {
|
|
|
1955
1996
|
setVerboseOpen(false)
|
|
1956
1997
|
}, [approvalPending, questionPending])
|
|
1957
1998
|
|
|
1958
|
-
// Append-only transcript
|
|
1959
|
-
//
|
|
1960
|
-
//
|
|
1961
|
-
//
|
|
1962
|
-
//
|
|
1963
|
-
// region below stays small: the streaming tail, modals, composer, and its
|
|
1964
|
-
// status footer. Live stream frames preserve `entries` identity.
|
|
1999
|
+
// Append-only transcript with a physical-row viewport: old FINAL rows flush
|
|
2000
|
+
// through Ink's `<Static>` into native scrollback, while one terminal-sized
|
|
2001
|
+
// suffix remains mutable beside running/streaming rows. The suffix contains
|
|
2002
|
+
// real transcript content—not padding—so once content naturally fills the
|
|
2003
|
+
// screen the composer stays at the bottom through live-to-settled changes.
|
|
1965
2004
|
//
|
|
1966
|
-
// `computeSettledRows`
|
|
1967
|
-
//
|
|
1968
|
-
//
|
|
1969
|
-
//
|
|
1970
|
-
//
|
|
1971
|
-
// rebuilds the CURRENT row set from index 0,
|
|
1972
|
-
// so the replay stays complete and never ghosts a pending/running tail.
|
|
2005
|
+
// `computeSettledRows` builds stable one-row records incrementally. The
|
|
2006
|
+
// viewport owns a monotonic row-level flush cursor over those records, so an
|
|
2007
|
+
// oversized entry may split cleanly between Static and the live tail. A
|
|
2008
|
+
// source-backed replay (`refreshEpoch` bump: resize / Ctrl+L) rebuilds and
|
|
2009
|
+
// flushes the complete current row set exactly once.
|
|
1973
2010
|
// Hook order is unconditional. Its dimensions drive every live-region
|
|
1974
2011
|
// budget before any dynamic rows are constructed.
|
|
1975
2012
|
const appStdout = useStdout().stdout
|
|
@@ -1979,7 +2016,8 @@ export function App(props: AppProps): ReactElement {
|
|
|
1979
2016
|
}))
|
|
1980
2017
|
const terminalSizeRef = useRef(terminalSize)
|
|
1981
2018
|
const settledRowsCache = useRef<SettledRowsCache | undefined>(undefined)
|
|
1982
|
-
const
|
|
2019
|
+
const viewportFlushRef = useRef({ sessionKey: props.sessionKey, epoch: refreshEpoch, rows: 0 })
|
|
2020
|
+
const settledRowsResult = useMemo(() => {
|
|
1983
2021
|
const result = computeSettledRows(
|
|
1984
2022
|
settledRowsCache.current,
|
|
1985
2023
|
view.entries,
|
|
@@ -1990,7 +2028,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
1990
2028
|
terminalSize.columns,
|
|
1991
2029
|
)
|
|
1992
2030
|
settledRowsCache.current = result.cache
|
|
1993
|
-
return result
|
|
2031
|
+
return result
|
|
1994
2032
|
}, [view.entries, settled, showReasoning, props.resumed, refreshEpoch, terminalSize.columns])
|
|
1995
2033
|
|
|
1996
2034
|
// One pending synchronized frame covers a debounced resize or explicit
|
|
@@ -2084,41 +2122,93 @@ export function App(props: AppProps): ReactElement {
|
|
|
2084
2122
|
// session title; cleared on unmount so the host shell regains its default.
|
|
2085
2123
|
const tabTitle = view.title === '' ? DEFAULT_TERMINAL_TITLE : view.title
|
|
2086
2124
|
useTerminalTitle(tabTitle)
|
|
2087
|
-
|
|
2125
|
+
// A single physical-row ledger owns the Static/live split. Keep at most one
|
|
2126
|
+
// maximum transcript viewport mutable; as durable rows append, only overflow
|
|
2127
|
+
// crosses the monotonic flush cursor into native scrollback. Temporary chrome
|
|
2128
|
+
// merely slices the retained rows and can reveal them again when it closes.
|
|
2129
|
+
const maximumTranscriptRows = liveRegionBudget({
|
|
2130
|
+
terminalRows,
|
|
2131
|
+
composerRows: 1,
|
|
2132
|
+
statusBarRows: 1,
|
|
2133
|
+
menuRows: 0,
|
|
2134
|
+
gutterRows: composerGutterRows,
|
|
2135
|
+
notice: false,
|
|
2136
|
+
todo: false,
|
|
2137
|
+
agents: false,
|
|
2138
|
+
})
|
|
2139
|
+
const settledPhysical = settledRowsResult.cache.physical
|
|
2140
|
+
const viewportStep = advanceTranscriptViewport(
|
|
2141
|
+
{
|
|
2142
|
+
sessionKey: viewportFlushRef.current.sessionKey,
|
|
2143
|
+
epoch: viewportFlushRef.current.epoch,
|
|
2144
|
+
flushedRows: viewportFlushRef.current.rows,
|
|
2145
|
+
},
|
|
2146
|
+
{
|
|
2147
|
+
sessionKey: props.sessionKey,
|
|
2148
|
+
epoch: refreshEpoch,
|
|
2149
|
+
totalRows: settledPhysical.length,
|
|
2150
|
+
retainedRows: maximumTranscriptRows,
|
|
2151
|
+
},
|
|
2152
|
+
)
|
|
2153
|
+
const flushedRows = viewportStep.staticRows
|
|
2154
|
+
viewportFlushRef.current = {
|
|
2155
|
+
sessionKey: viewportStep.cursor.sessionKey,
|
|
2156
|
+
epoch: viewportStep.cursor.epoch,
|
|
2157
|
+
rows: viewportStep.cursor.flushedRows,
|
|
2158
|
+
}
|
|
2159
|
+
const staticPrefixRows = settledRowsResult.cache.flat.length - settledPhysical.length
|
|
2160
|
+
const settledRows = settledRowsResult.cache.flat.slice(0, staticPrefixRows + flushedRows)
|
|
2161
|
+
const retainedSettledLines = settledPhysical.slice(flushedRows).map(row => row.line)
|
|
2162
|
+
const mutableLiveLines = useMemo(
|
|
2088
2163
|
() => view.entries.slice(settled).flatMap(
|
|
2089
2164
|
// Width shrinks with the real terminal (no 10-column floor: on a
|
|
2090
|
-
// narrower terminal the floor silently overflowed every row).
|
|
2165
|
+
// narrower terminal the floor silently overflowed every mutable row).
|
|
2091
2166
|
entry => transcriptEntryLines(entry, Math.max(1, terminalColumns - 2), showReasoning),
|
|
2092
2167
|
),
|
|
2093
2168
|
[view.entries, settled, terminalColumns, showReasoning],
|
|
2094
2169
|
)
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
//
|
|
2098
|
-
|
|
2170
|
+
const allLiveLines = retainedSettledLines.concat(mutableLiveLines)
|
|
2171
|
+
|
|
2172
|
+
// Give streams a bounded provisional maximum, then reclaim every row they
|
|
2173
|
+
// do not ACTUALLY paint for real retained history. Once transcript content
|
|
2174
|
+
// has naturally filled the viewport, this keeps composer geometry constant
|
|
2175
|
+
// through stream growth, settlement, and running-tool replacement—without
|
|
2176
|
+
// synthetic blank rows.
|
|
2177
|
+
const provisionalLiveBudget = busy || streamingActive
|
|
2099
2178
|
? Math.max(1, Math.floor(dynamicRows / 3))
|
|
2100
2179
|
: Math.max(0, dynamicRows - (deepDivingVisible ? 1 : 0))
|
|
2101
|
-
const
|
|
2180
|
+
const provisionalLiveRows = Math.min(allLiveLines.length, provisionalLiveBudget)
|
|
2181
|
+
const provisionalStreamRows = Math.max(1, dynamicRows - provisionalLiveRows)
|
|
2182
|
+
const provisionalReasoningRows = view.streamingReasoning === ''
|
|
2183
|
+
? 0
|
|
2184
|
+
: view.streaming === ''
|
|
2185
|
+
? provisionalStreamRows
|
|
2186
|
+
: provisionalStreamRows <= 1
|
|
2187
|
+
? 0
|
|
2188
|
+
: showReasoning
|
|
2189
|
+
? Math.max(1, Math.floor(provisionalStreamRows / 3))
|
|
2190
|
+
: 1
|
|
2191
|
+
const provisionalAnswerRows = view.streaming === ''
|
|
2192
|
+
? 0
|
|
2193
|
+
: Math.max(1, provisionalStreamRows - provisionalReasoningRows)
|
|
2194
|
+
const streamColumns = Math.max(1, terminalColumns - 2)
|
|
2195
|
+
const reasoningRows = view.streamingReasoning === '' || provisionalReasoningRows === 0
|
|
2196
|
+
? 0
|
|
2197
|
+
: showReasoning
|
|
2198
|
+
? streamTailPhysicalRows(view.streamingReasoning, streamColumns, provisionalReasoningRows, '✻ ', ' ')
|
|
2199
|
+
: 1
|
|
2200
|
+
const answerRows = view.streaming === '' || provisionalAnswerRows === 0
|
|
2201
|
+
? 0
|
|
2202
|
+
: streamTailPhysicalRows(view.streaming, streamColumns, provisionalAnswerRows, ' ')
|
|
2203
|
+
const nonHistoryRows = reasoningRows + answerRows + (deepDivingVisible ? 1 : 0)
|
|
2204
|
+
const visibleHistoryRows = visibleTranscriptRows(allLiveLines.length, nonHistoryRows, dynamicRows)
|
|
2205
|
+
const visibleLiveLines = visibleHistoryRows === 0 ? [] : allLiveLines.slice(-visibleHistoryRows)
|
|
2102
2206
|
|
|
2103
2207
|
// The screen refresh used by /clear and Ctrl+L: a raw ANSI clear (wipe
|
|
2104
2208
|
// screen AND scrollback, home the cursor) then a Static remount via the
|
|
2105
2209
|
// key change, which re-flushes the current items from index 0. NEVER
|
|
2106
2210
|
// console.clear() — it desyncs Ink's internal line ledger against the
|
|
2107
2211
|
// flushed static rows and garbles every frame after.
|
|
2108
|
-
const streamRows = Math.max(1, dynamicRows - visibleLiveLines.length)
|
|
2109
|
-
const reasoningRows = view.streamingReasoning === ''
|
|
2110
|
-
? 0
|
|
2111
|
-
: view.streaming === ''
|
|
2112
|
-
? streamRows
|
|
2113
|
-
: streamRows <= 1
|
|
2114
|
-
? 0
|
|
2115
|
-
: showReasoning
|
|
2116
|
-
? Math.max(1, Math.floor(streamRows / 3))
|
|
2117
|
-
: 1
|
|
2118
|
-
const answerRows = view.streaming === '' ? 0 : Math.max(1, streamRows - reasoningRows)
|
|
2119
|
-
// Dynamic-height tripwire: the allocation must fit dynamicRows by
|
|
2120
|
-
// construction; a future edit that breaks the derivation clamps here
|
|
2121
|
-
// (answer, then reasoning, then settled live rows) and warns once.
|
|
2122
2212
|
const liveAudit = clampLiveAllocation(
|
|
2123
2213
|
{ live: visibleLiveLines.length, reasoning: reasoningRows, answer: answerRows },
|
|
2124
2214
|
dynamicRows,
|
|
@@ -2136,6 +2226,15 @@ export function App(props: AppProps): ReactElement {
|
|
|
2136
2226
|
: visibleLiveLines.slice(-liveAudit.allocation.live)
|
|
2137
2227
|
const auditedReasoningRows = liveAudit.allocation.reasoning
|
|
2138
2228
|
const auditedAnswerRows = liveAudit.allocation.answer
|
|
2229
|
+
const transcriptViewportFilled = allLiveLines.length + nonHistoryRows >= dynamicRows
|
|
2230
|
+
const modalViewportFilled = allLiveLines.length >= dynamicRows
|
|
2231
|
+
const anchoredSurfaceRows = dynamicRows
|
|
2232
|
+
+ (transcriptVisible && view.todos.length > 0 ? 1 : 0)
|
|
2233
|
+
+ (transcriptVisible && agentRows.length > 0 ? 1 : 0)
|
|
2234
|
+
const surfaceAnchored = transcriptVisible ? transcriptViewportFilled : modalViewportFilled
|
|
2235
|
+
const frozenModalLines = surfaceAnchored && !transcriptVisible
|
|
2236
|
+
? allLiveLines.slice(-dynamicRows)
|
|
2237
|
+
: []
|
|
2139
2238
|
// The surface that currently owns the keyboard, named in the frozen band:
|
|
2140
2239
|
// an empty composer under a panel must not advertise typing it cannot
|
|
2141
2240
|
// accept — every key actually feeds the panel (which may or may not
|
|
@@ -2206,7 +2305,8 @@ export function App(props: AppProps): ReactElement {
|
|
|
2206
2305
|
const attachedView = useSyncExternalStore(subscribeAttached, readAttached)
|
|
2207
2306
|
const attachedSettled = useMemo(() => settledEntryCount(attachedView.entries), [attachedView.entries])
|
|
2208
2307
|
const attachedRowsCache = useRef<SettledRowsCache | undefined>(undefined)
|
|
2209
|
-
const
|
|
2308
|
+
const attachedViewportFlushRef = useRef({ sessionKey: 'none', epoch: refreshEpoch, rows: 0 })
|
|
2309
|
+
const attachedSettledRowsResult = useMemo(() => {
|
|
2210
2310
|
const result = computeSettledRows(
|
|
2211
2311
|
attachedRowsCache.current,
|
|
2212
2312
|
attachedView.entries,
|
|
@@ -2217,8 +2317,34 @@ export function App(props: AppProps): ReactElement {
|
|
|
2217
2317
|
terminalSize.columns,
|
|
2218
2318
|
)
|
|
2219
2319
|
attachedRowsCache.current = result.cache
|
|
2220
|
-
return result
|
|
2320
|
+
return result
|
|
2221
2321
|
}, [attachedView.entries, attachedSettled, showReasoning, refreshEpoch, terminalSize.columns])
|
|
2322
|
+
const attachDynamicRows = Math.max(1, terminalRows - 3 - 1 - 1 - 2)
|
|
2323
|
+
const attachedPhysical = attachedSettledRowsResult.cache.physical
|
|
2324
|
+
const attachedViewportStep = advanceTranscriptViewport(
|
|
2325
|
+
{
|
|
2326
|
+
sessionKey: attachedViewportFlushRef.current.sessionKey,
|
|
2327
|
+
epoch: attachedViewportFlushRef.current.epoch,
|
|
2328
|
+
flushedRows: attachedViewportFlushRef.current.rows,
|
|
2329
|
+
},
|
|
2330
|
+
{
|
|
2331
|
+
sessionKey: attachment?.id ?? 'none',
|
|
2332
|
+
epoch: refreshEpoch,
|
|
2333
|
+
totalRows: attachedPhysical.length,
|
|
2334
|
+
retainedRows: attachDynamicRows,
|
|
2335
|
+
},
|
|
2336
|
+
)
|
|
2337
|
+
attachedViewportFlushRef.current = {
|
|
2338
|
+
sessionKey: attachedViewportStep.cursor.sessionKey,
|
|
2339
|
+
epoch: attachedViewportStep.cursor.epoch,
|
|
2340
|
+
rows: attachedViewportStep.cursor.flushedRows,
|
|
2341
|
+
}
|
|
2342
|
+
const attachedStaticPrefixRows = attachedSettledRowsResult.cache.flat.length - attachedPhysical.length
|
|
2343
|
+
const attachedSettledRows = attachedSettledRowsResult.cache.flat.slice(
|
|
2344
|
+
0,
|
|
2345
|
+
attachedStaticPrefixRows + attachedViewportStep.staticRows,
|
|
2346
|
+
)
|
|
2347
|
+
const attachedRetainedLines = attachedPhysical.slice(attachedViewportStep.staticRows).map(row => row.line)
|
|
2222
2348
|
/** Detach and hand the keyboard back to the /agents list. */
|
|
2223
2349
|
const detach = useCallback((): void => {
|
|
2224
2350
|
setAttachment(current => {
|
|
@@ -2611,18 +2737,18 @@ export function App(props: AppProps): ReactElement {
|
|
|
2611
2737
|
// ride <Static> above (items switch), and the live region mirrors the
|
|
2612
2738
|
// main conversation's tail with a simplified chrome budget (readonly
|
|
2613
2739
|
// bar band + one status row + one gutter + Ink's two spare rows).
|
|
2614
|
-
const attachDynamicRows = Math.max(1, terminalRows - 3 - 1 - 1 - 2)
|
|
2615
2740
|
const attachBusy = attachedView.busySince !== undefined
|
|
2616
2741
|
const attachStreaming = attachedView.streaming !== '' || attachedView.streamingReasoning !== ''
|
|
2617
|
-
const
|
|
2742
|
+
const attachMutableLines = attachedView.entries.slice(attachedSettled).flatMap(
|
|
2618
2743
|
entry => transcriptEntryLines(entry, Math.max(1, terminalColumns - 2), showReasoning),
|
|
2619
2744
|
)
|
|
2620
|
-
const
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
const
|
|
2745
|
+
const attachAllLiveLines = attachedRetainedLines.concat(attachMutableLines)
|
|
2746
|
+
const attachProvisionalLiveRows = Math.min(
|
|
2747
|
+
attachAllLiveLines.length,
|
|
2748
|
+
attachBusy || attachStreaming ? Math.max(1, Math.floor(attachDynamicRows / 3)) : attachDynamicRows,
|
|
2749
|
+
)
|
|
2750
|
+
const attachStreamRows = Math.max(1, attachDynamicRows - attachProvisionalLiveRows)
|
|
2751
|
+
const attachReasoningLimit = attachedView.streamingReasoning === ''
|
|
2626
2752
|
? 0
|
|
2627
2753
|
: attachedView.streaming === ''
|
|
2628
2754
|
? attachStreamRows
|
|
@@ -2631,11 +2757,30 @@ export function App(props: AppProps): ReactElement {
|
|
|
2631
2757
|
: showReasoning
|
|
2632
2758
|
? Math.max(1, Math.floor(attachStreamRows / 3))
|
|
2633
2759
|
: 1
|
|
2634
|
-
const
|
|
2760
|
+
const attachAnswerLimit = attachedView.streaming === '' ? 0 : Math.max(1, attachStreamRows - attachReasoningLimit)
|
|
2761
|
+
const attachReasoningRows = attachedView.streamingReasoning === '' || attachReasoningLimit === 0
|
|
2762
|
+
? 0
|
|
2763
|
+
: showReasoning
|
|
2764
|
+
? streamTailPhysicalRows(attachedView.streamingReasoning, Math.max(1, terminalColumns - 2), attachReasoningLimit, '✻ ', ' ')
|
|
2765
|
+
: 1
|
|
2766
|
+
const attachAnswerRows = attachedView.streaming === '' || attachAnswerLimit === 0
|
|
2767
|
+
? 0
|
|
2768
|
+
: streamTailPhysicalRows(attachedView.streaming, Math.max(1, terminalColumns - 2), attachAnswerLimit, ' ')
|
|
2769
|
+
const attachNonHistoryRows = attachReasoningRows + attachAnswerRows
|
|
2770
|
+
+ (attachBusy && !attachStreaming ? 1 : 0)
|
|
2771
|
+
const attachVisibleHistoryRows = visibleTranscriptRows(
|
|
2772
|
+
attachAllLiveLines.length,
|
|
2773
|
+
attachNonHistoryRows,
|
|
2774
|
+
attachDynamicRows,
|
|
2775
|
+
)
|
|
2776
|
+
const attachVisibleLive = attachVisibleHistoryRows === 0
|
|
2777
|
+
? []
|
|
2778
|
+
: attachAllLiveLines.slice(-attachVisibleHistoryRows)
|
|
2635
2779
|
const attachAudit = clampLiveAllocation(
|
|
2636
2780
|
{ live: attachVisibleLive.length, reasoning: attachReasoningRows, answer: attachAnswerRows },
|
|
2637
2781
|
attachDynamicRows,
|
|
2638
2782
|
)
|
|
2783
|
+
const attachViewportFilled = attachAllLiveLines.length + attachNonHistoryRows >= attachDynamicRows
|
|
2639
2784
|
const attachTurns = attachedView.entries.filter(entry => entry.kind === 'turn-marker').length
|
|
2640
2785
|
return createElement(
|
|
2641
2786
|
Box,
|
|
@@ -2646,7 +2791,9 @@ export function App(props: AppProps): ReactElement {
|
|
|
2646
2791
|
}),
|
|
2647
2792
|
createElement(
|
|
2648
2793
|
Box,
|
|
2649
|
-
|
|
2794
|
+
attachViewportFilled
|
|
2795
|
+
? { flexDirection: 'column', height: attachDynamicRows }
|
|
2796
|
+
: { flexDirection: 'column' },
|
|
2650
2797
|
attachAudit.allocation.live === attachVisibleLive.length && attachVisibleLive.length > 0
|
|
2651
2798
|
? createElement(StyledRows, { lines: attachVisibleLive })
|
|
2652
2799
|
: undefined,
|
|
@@ -2667,10 +2814,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
2667
2814
|
maxRows: attachAudit.allocation.answer,
|
|
2668
2815
|
prefix: ' ',
|
|
2669
2816
|
columns: Math.max(1, terminalColumns - 2),
|
|
2670
|
-
}, attachBusy ? createElement(Caret
|
|
2817
|
+
}, attachBusy ? createElement(Caret) : undefined)
|
|
2671
2818
|
: undefined,
|
|
2672
2819
|
attachBusy && attachedView.streaming === '' && attachedView.streamingReasoning === ''
|
|
2673
|
-
? createElement(DeepDivingLine, { since: attachedView.busySince,
|
|
2820
|
+
? createElement(DeepDivingLine, { since: attachedView.busySince, themeFlowAnimated: animations })
|
|
2674
2821
|
: undefined,
|
|
2675
2822
|
),
|
|
2676
2823
|
createElement(
|
|
@@ -2696,7 +2843,19 @@ export function App(props: AppProps): ReactElement {
|
|
|
2696
2843
|
key: refreshEpoch,
|
|
2697
2844
|
items: attachment === undefined ? settledRows : attachedSettledRows,
|
|
2698
2845
|
}),
|
|
2699
|
-
|
|
2846
|
+
createElement(
|
|
2847
|
+
Box,
|
|
2848
|
+
surfaceAnchored
|
|
2849
|
+
? { flexDirection: 'column', height: anchoredSurfaceRows }
|
|
2850
|
+
: { flexDirection: 'column' },
|
|
2851
|
+
frozenModalLines.length === 0
|
|
2852
|
+
? undefined
|
|
2853
|
+
: createElement(
|
|
2854
|
+
Box,
|
|
2855
|
+
{ flexDirection: 'column', height: 0, flexGrow: 1, flexShrink: 1, overflow: 'hidden', justifyContent: 'flex-end' },
|
|
2856
|
+
createElement(StyledRows, { lines: frozenModalLines }),
|
|
2857
|
+
),
|
|
2858
|
+
transcriptVisible
|
|
2700
2859
|
? createElement(
|
|
2701
2860
|
Box,
|
|
2702
2861
|
// No container padding: every live row carries its own two-column
|
|
@@ -2722,7 +2881,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
2722
2881
|
// marker falls back to the static dim row — same as Deep diving
|
|
2723
2882
|
// always yields the live region to streaming content.
|
|
2724
2883
|
: view.streaming === ''
|
|
2725
|
-
? createElement(ShimmerLine, { text: '✻ Thinking… (Ctrl/Alt+R to expand)',
|
|
2884
|
+
? createElement(ShimmerLine, { text: '✻ Thinking… (Ctrl/Alt+R to expand)', themeFlowAnimated: animations })
|
|
2726
2885
|
: createElement(StreamTail, {
|
|
2727
2886
|
text: 'Thinking… (Ctrl/Alt+R to expand)',
|
|
2728
2887
|
prefix: '✻ ',
|
|
@@ -2738,10 +2897,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
2738
2897
|
// The same two-column gutter as settled replies: streamed text
|
|
2739
2898
|
// lands exactly where the assembled message will render.
|
|
2740
2899
|
{ text: view.streaming, dim: false, maxRows: auditedAnswerRows, prefix: ' ', columns: Math.max(1, terminalColumns - 2) },
|
|
2741
|
-
busy ? createElement(Caret
|
|
2900
|
+
busy ? createElement(Caret) : undefined,
|
|
2742
2901
|
)
|
|
2743
2902
|
: undefined,
|
|
2744
|
-
deepDivingVisible ? createElement(DeepDivingLine, { since: view.busySince,
|
|
2903
|
+
deepDivingVisible ? createElement(DeepDivingLine, { since: view.busySince, themeFlowAnimated: animations }) : undefined,
|
|
2745
2904
|
)
|
|
2746
2905
|
: undefined,
|
|
2747
2906
|
transcriptVisible ? createElement(TodoPanel, { todos: view.todos }) : undefined,
|
|
@@ -2802,7 +2961,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
2802
2961
|
: undefined,
|
|
2803
2962
|
verboseOpen && !approvalPending && !questionPending
|
|
2804
2963
|
? createElement(MemoVerbosePanel, {
|
|
2805
|
-
entries:
|
|
2964
|
+
entries: verboseEntries,
|
|
2806
2965
|
onClose: closeInspector,
|
|
2807
2966
|
columns: terminalColumns,
|
|
2808
2967
|
rows: terminalRows,
|
|
@@ -2987,6 +3146,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
2987
3146
|
close: () => setSubagentOpen(false),
|
|
2988
3147
|
})
|
|
2989
3148
|
: undefined,
|
|
3149
|
+
),
|
|
2990
3150
|
notice === undefined
|
|
2991
3151
|
? undefined
|
|
2992
3152
|
: createElement(NoticeLine, {
|
|
@@ -3190,7 +3350,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
3190
3350
|
stats: view.stats,
|
|
3191
3351
|
busy,
|
|
3192
3352
|
animated: animations,
|
|
3193
|
-
|
|
3353
|
+
// Leave one physical terminal column unused. VS Code autowraps a row
|
|
3354
|
+
// painted at EXACTLY stdout.columns; the first-token status update can
|
|
3355
|
+
// otherwise create an unreported row and lift composer + status.
|
|
3356
|
+
columns: Math.max(1, terminalColumns - 1),
|
|
3194
3357
|
items: statuslineItems,
|
|
3195
3358
|
onRows: handleStatusRows,
|
|
3196
3359
|
}),
|