praxis-agent 0.35.0 → 0.37.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.
Files changed (111) hide show
  1. package/README.md +20 -2
  2. package/dist/application/background-agent-manager.d.ts +16 -6
  3. package/dist/application/background-agent-manager.js +139 -97
  4. package/dist/application/context-engine.d.ts +56 -0
  5. package/dist/application/context-engine.js +129 -0
  6. package/dist/application/durable-follow-up.d.ts +14 -0
  7. package/dist/application/durable-follow-up.js +52 -0
  8. package/dist/application/native-session-transcript.d.ts +72 -0
  9. package/dist/application/native-session-transcript.js +351 -0
  10. package/dist/application/native-sidechain-transcript.d.ts +34 -0
  11. package/dist/application/native-sidechain-transcript.js +156 -0
  12. package/dist/application/session-service.d.ts +23 -4
  13. package/dist/application/session-service.js +1130 -494
  14. package/dist/application/subagent-service.d.ts +5 -1
  15. package/dist/application/subagent-service.js +749 -320
  16. package/dist/application/team-agent-runtime.d.ts +29 -0
  17. package/dist/application/team-agent-runtime.js +169 -0
  18. package/dist/application/team-capability.d.ts +27 -0
  19. package/dist/application/team-capability.js +41 -0
  20. package/dist/application/team-lead-operations.d.ts +38 -0
  21. package/dist/application/team-lead-operations.js +155 -0
  22. package/dist/application/team-mailbox.d.ts +54 -0
  23. package/dist/application/team-mailbox.js +196 -0
  24. package/dist/application/team-manager.d.ts +113 -0
  25. package/dist/application/team-manager.js +681 -0
  26. package/dist/application/team-workspace.d.ts +29 -0
  27. package/dist/application/team-workspace.js +197 -0
  28. package/dist/application/top-level-agent-manager.d.ts +1 -0
  29. package/dist/application/top-level-agent-manager.js +1025 -403
  30. package/dist/application/transcript-projection.d.ts +35 -0
  31. package/dist/application/transcript-projection.js +139 -0
  32. package/dist/application/turn-lifecycle.d.ts +2 -0
  33. package/dist/application/turn-memory-coordinator.d.ts +43 -0
  34. package/dist/application/turn-memory-coordinator.js +98 -0
  35. package/dist/cli/agents-dashboard.js +10 -5
  36. package/dist/cli/interactive.d.ts +7 -0
  37. package/dist/cli/interactive.js +645 -334
  38. package/dist/cli/tui/claude-style.d.ts +8 -25
  39. package/dist/cli/tui/claude-style.js +299 -277
  40. package/dist/cli/tui/decision-surface-model.d.ts +71 -0
  41. package/dist/cli/tui/decision-surface-model.js +131 -0
  42. package/dist/cli/tui/decision-surface.d.ts +7 -0
  43. package/dist/cli/tui/decision-surface.js +41 -0
  44. package/dist/cli/tui/help-surface-model.d.ts +68 -0
  45. package/dist/cli/tui/help-surface-model.js +131 -0
  46. package/dist/cli/tui/mcp-panel.js +1 -2
  47. package/dist/cli/tui/permission-surface-model.d.ts +195 -0
  48. package/dist/cli/tui/permission-surface-model.js +360 -0
  49. package/dist/cli/tui/permission-surface.d.ts +7 -0
  50. package/dist/cli/tui/permission-surface.js +69 -0
  51. package/dist/cli/tui/presentation-environment.d.ts +32 -0
  52. package/dist/cli/tui/presentation-environment.js +149 -0
  53. package/dist/cli/tui/task-panel.js +10 -1
  54. package/dist/cli/tui/theme.d.ts +46 -3
  55. package/dist/cli/tui/theme.js +414 -56
  56. package/dist/cli/tui/tool-permission.js +16 -8
  57. package/dist/cli/tui/transcript-presentation.d.ts +19 -1
  58. package/dist/cli/tui/transcript-presentation.js +26 -6
  59. package/dist/cli/tui/transcript-viewport.d.ts +18 -0
  60. package/dist/cli/tui/transcript-viewport.js +517 -111
  61. package/dist/cli/tui/transcript-window-model.d.ts +108 -0
  62. package/dist/cli/tui/transcript-window-model.js +772 -0
  63. package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
  64. package/dist/cli/tui/tui-interaction-router.js +127 -0
  65. package/dist/cli/tui/tui-screen-model.d.ts +114 -0
  66. package/dist/cli/tui/tui-screen-model.js +210 -0
  67. package/dist/cli/tui/tui-view-model.d.ts +2 -49
  68. package/dist/cli/tui/tui-view-model.js +1 -68
  69. package/dist/cli-runtime.d.ts +4 -2
  70. package/dist/cli-runtime.js +397 -32
  71. package/dist/compatibility/claude/transcript-codec.js +6 -0
  72. package/dist/core/agent-orchestration.d.ts +31 -0
  73. package/dist/core/agent-orchestration.js +235 -0
  74. package/dist/core/permission-policy.d.ts +4 -0
  75. package/dist/core/permission-policy.js +21 -0
  76. package/dist/core/runtime.d.ts +6 -0
  77. package/dist/core/runtime.js +20 -2
  78. package/dist/core/team-mailbox.d.ts +54 -0
  79. package/dist/core/team-mailbox.js +212 -0
  80. package/dist/core/team-ownership.d.ts +84 -0
  81. package/dist/core/team-ownership.js +505 -0
  82. package/dist/core/tool-execution-scheduler.js +6 -1
  83. package/dist/core/transcript-event.d.ts +5 -1
  84. package/dist/core/transcript-event.js +12 -0
  85. package/dist/persistence/claude-job-store.d.ts +58 -0
  86. package/dist/persistence/claude-job-store.js +483 -1
  87. package/dist/persistence/claude-transcript-store.d.ts +2 -1
  88. package/dist/persistence/claude-transcript-store.js +2 -10
  89. package/dist/persistence/native-team-mailbox.d.ts +68 -0
  90. package/dist/persistence/native-team-mailbox.js +542 -0
  91. package/dist/persistence/native-team-store.d.ts +34 -0
  92. package/dist/persistence/native-team-store.js +257 -0
  93. package/dist/persistence/native-transcript-reader.d.ts +53 -0
  94. package/dist/persistence/native-transcript-reader.js +283 -0
  95. package/dist/persistence/native-transcript-store.d.ts +69 -0
  96. package/dist/persistence/native-transcript-store.js +321 -0
  97. package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
  98. package/dist/persistence/subagent-lifecycle-store.js +373 -146
  99. package/dist/persistence/transcript-file-append.d.ts +2 -0
  100. package/dist/persistence/transcript-file-append.js +10 -0
  101. package/dist/platform/exclusive-file-lease.d.ts +3 -0
  102. package/dist/platform/exclusive-file-lease.js +3 -0
  103. package/dist/tools/claude-capabilities.d.ts +4 -0
  104. package/dist/tools/claude-capabilities.js +18 -2
  105. package/dist/tools/team-lead-tools.d.ts +19 -0
  106. package/dist/tools/team-lead-tools.js +479 -0
  107. package/dist/tools/team-member-tools.d.ts +21 -0
  108. package/dist/tools/team-member-tools.js +259 -0
  109. package/package.json +1 -1
  110. package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
  111. package/dist/cli/tui/fullscreen-renderer.js +0 -13
@@ -128,47 +128,77 @@ function hardWrappedRows(word, width, startColumn) {
128
128
  }
129
129
  return { rows, column };
130
130
  }
131
- function wrappedLineRows(line, width) {
131
+ function wrappedLineLayout(line, width, output) {
132
132
  const expanded = expandTabs(line);
133
- if (isPrintableAscii(expanded) && expanded.length <= width)
133
+ if (isPrintableAscii(expanded) && expanded.length <= width) {
134
+ output?.push(expanded);
135
+ return 1;
136
+ }
137
+ if (expanded.length === 0) {
138
+ output?.push('');
134
139
  return 1;
140
+ }
135
141
  const words = expanded.split(' ');
136
- let rows = 1;
142
+ let completedRows = 0;
143
+ let row = '';
137
144
  let column = 0;
145
+ const pushRow = () => {
146
+ output?.push(row);
147
+ row = '';
148
+ column = 0;
149
+ completedRows += 1;
150
+ };
151
+ const appendHardWrappedWord = (word) => {
152
+ if (!output) {
153
+ const wrapped = hardWrappedRows(word, width, column);
154
+ completedRows += wrapped.rows;
155
+ column = wrapped.column;
156
+ return;
157
+ }
158
+ const clusters = graphemes(word);
159
+ for (const [index, cluster] of clusters.entries()) {
160
+ const clusterWidth = graphemeWidth(cluster);
161
+ if (column + clusterWidth > width)
162
+ pushRow();
163
+ row += cluster;
164
+ column += clusterWidth;
165
+ if (column >= width && index < clusters.length - 1)
166
+ pushRow();
167
+ }
168
+ };
138
169
  for (let index = 0; index < words.length; index += 1) {
139
170
  const word = words[index] ?? '';
140
171
  const wordWidth = isPrintableAscii(word)
141
172
  ? word.length
142
173
  : terminalStringWidth(word);
143
174
  if (index > 0) {
144
- if (column >= width) {
145
- rows += 1;
146
- column = 0;
147
- }
175
+ if (column >= width)
176
+ pushRow();
177
+ if (output)
178
+ row += ' ';
148
179
  column += 1;
149
180
  }
150
181
  if (wordWidth > width) {
151
182
  const current = hardWrappedRows(word, width, column);
152
183
  const fresh = hardWrappedRows(word, width, 0);
153
184
  if (column > 0 && current.rows > fresh.rows) {
154
- rows += 1;
155
- column = 0;
156
- rows += fresh.rows;
157
- column = fresh.column;
158
- }
159
- else {
160
- rows += current.rows;
161
- column = current.column;
185
+ pushRow();
162
186
  }
187
+ appendHardWrappedWord(word);
163
188
  continue;
164
189
  }
165
190
  if (column + wordWidth > width && column > 0 && word.length > 0) {
166
- rows += 1;
167
- column = 0;
191
+ pushRow();
168
192
  }
193
+ if (output)
194
+ row += word;
169
195
  column += wordWidth;
170
196
  }
171
- return rows;
197
+ pushRow();
198
+ return completedRows;
199
+ }
200
+ function wrappedLineRows(line, width) {
201
+ return wrappedLineLayout(line, width);
172
202
  }
173
203
  function wrappedLineCount(text, width) {
174
204
  const usable = Math.max(1, width);
@@ -315,7 +345,76 @@ function textTail(text, budget, width) {
315
345
  }
316
346
  return `${TRANSCRIPT_TRUNCATION_MARKER}${kept.join('\n')}`;
317
347
  }
318
- export function estimateTranscriptEntryLines(entry, width, mode) {
348
+ function textHead(text, budget, width) {
349
+ const usableBudget = Math.max(1, budget);
350
+ const usableWidth = Math.max(1, width);
351
+ if (wrappedLineCount(text, usableWidth) <= usableBudget)
352
+ return text;
353
+ const marker = `${TRANSCRIPT_TRUNCATION_MARKER}`;
354
+ const lines = text.split('\n');
355
+ const kept = [];
356
+ for (const line of lines) {
357
+ const candidate = [...kept, line, marker].join('\n');
358
+ if (wrappedLineCount(candidate, usableWidth) > usableBudget)
359
+ break;
360
+ kept.push(line);
361
+ }
362
+ let candidate = [...kept, marker].join('\n');
363
+ if (wrappedLineCount(candidate, usableWidth) <= usableBudget)
364
+ return candidate;
365
+ const remaining = Math.max(0, usableBudget - wrappedLineCount(marker, usableWidth));
366
+ const prefix = kept.join('\n');
367
+ const clusters = graphemes(prefix);
368
+ let low = 0;
369
+ let high = clusters.length;
370
+ let best = '';
371
+ while (low <= high) {
372
+ const middle = Math.floor((low + high) / 2);
373
+ const next = clusters.slice(0, middle).join('');
374
+ if (wrappedLineCount(`${next}${next ? '\n' : ''}${marker}`, usableWidth) <=
375
+ usableBudget) {
376
+ best = next;
377
+ low = middle + 1;
378
+ }
379
+ else
380
+ high = middle - 1;
381
+ }
382
+ candidate = `${best}${best ? '\n' : ''}${marker}`;
383
+ return wrappedLineCount(candidate, usableWidth) <= usableBudget
384
+ ? candidate
385
+ : marker.slice(0, Math.max(1, remaining));
386
+ }
387
+ function textVisualRows(text, width) {
388
+ const usableWidth = Math.max(1, width);
389
+ const visualRows = [];
390
+ const visible = text.includes('\u001b') || text.includes('\u009b')
391
+ ? stripVTControlCharacters(text)
392
+ : text;
393
+ for (const logicalLine of visible.replace(/\r\n?/gu, '\n').split('\n'))
394
+ wrappedLineLayout(logicalLine, usableWidth, visualRows);
395
+ return visualRows;
396
+ }
397
+ function textRange(text, startRow, budget, width) {
398
+ const visualRows = textVisualRows(text, width);
399
+ const limit = Math.max(1, budget);
400
+ const start = Math.min(Math.max(0, startRow), visualRows.length);
401
+ const clippedBefore = start > 0 && limit >= 2;
402
+ const available = Math.max(1, limit - (clippedBefore ? 1 : 0));
403
+ let contentBudget = available;
404
+ let end = Math.min(visualRows.length, start + contentBudget);
405
+ const clippedAfter = end < visualRows.length && available >= 2;
406
+ if (clippedAfter) {
407
+ contentBudget = available - 1;
408
+ end = Math.min(visualRows.length, start + contentBudget);
409
+ }
410
+ const selected = visualRows.slice(start, end);
411
+ return [
412
+ ...(clippedBefore ? [TRANSCRIPT_TRUNCATION_MARKER] : []),
413
+ ...selected,
414
+ ...(clippedAfter ? [TRANSCRIPT_TRUNCATION_MARKER] : []),
415
+ ].join('\n');
416
+ }
417
+ function estimateRenderedTranscriptEntryLines(entry, width, mode) {
319
418
  if (entry.kind === 'read-summary')
320
419
  return wrappedLineCount(` Read ${entry.count} file${entry.count === 1 ? '' : 's'} (ctrl+o to expand)`, width);
321
420
  if (entry.kind === 'item') {
@@ -347,7 +446,9 @@ export function estimateTranscriptEntryLines(entry, width, mode) {
347
446
  }
348
447
  if (item.kind === 'compact') {
349
448
  if (mode === 'screen-reader')
350
- return 2 + wrappedLineCount(item.summary, markdownContentWidth(width));
449
+ return (1 +
450
+ wrappedLineCount('Conversation compacted', width) +
451
+ wrappedLineCount(item.summary, markdownContentWidth(width)));
351
452
  return (1 +
352
453
  wrappedLineCount('✻ Conversation compacted (ctrl+o for history)', width) +
353
454
  (mode === 'audit'
@@ -411,38 +512,292 @@ export function estimateTranscriptEntryLines(entry, width, mode) {
411
512
  rows += visibleOutputLineCount(output, width, mode);
412
513
  return rows;
413
514
  }
515
+ export function estimateTranscriptEntryLines(entry, width, mode) {
516
+ if (entry.viewportSlice)
517
+ return entry.viewportSlice.rows;
518
+ const rendered = estimateRenderedTranscriptEntryLines(entry, width, mode);
519
+ const hasBoundedAuthoritativeText = (entry.kind === 'orphan-tool-result' && entry.item.text.length > 500) ||
520
+ (entry.kind === 'tool' &&
521
+ entry.result?.isError === true &&
522
+ entry.result.text.length > 500);
523
+ if (!hasBoundedAuthoritativeText)
524
+ return rendered;
525
+ return entryViewportRows(entry, width, mode)?.length ?? rendered;
526
+ }
414
527
  export function transcriptPresentationLineCount(entries, width, mode) {
415
528
  return entries.reduce((n, entry) => n + estimateTranscriptEntryLines(entry, width, mode), 0);
416
529
  }
530
+ function viewportOutputRows(text, width, mode) {
531
+ const lines = contentLines(text);
532
+ const visible = mode === 'normal' ? lines.slice(0, 3) : lines;
533
+ const outputWidth = Math.max(1, width - 2);
534
+ const rows = visible.flatMap((line, index) => textVisualRows(`${index === 0 ? '⎿ ' : ' '}${line || ' '}`, outputWidth));
535
+ if (mode === 'normal' && lines.length > visible.length) {
536
+ rows.push(...textVisualRows(` ${TRANSCRIPT_TRUNCATION_MARKER} +${lines.length - visible.length} lines (ctrl+o to expand)`, outputWidth));
537
+ }
538
+ return rows;
539
+ }
540
+ function entryViewportRows(entry, width, mode) {
541
+ if (entry.kind === 'read-summary')
542
+ return textVisualRows(` Read ${entry.count} file${entry.count === 1 ? '' : 's'} (ctrl+o to expand)`, width);
543
+ if (entry.kind === 'item') {
544
+ const item = entry.item;
545
+ if (item.kind === 'context')
546
+ return undefined;
547
+ if (item.kind === 'user')
548
+ return [
549
+ '',
550
+ ...(mode === 'screen-reader' ? [''] : []),
551
+ ...textVisualRows(`${mode === 'screen-reader' ? 'You: ' : '❯ '}${item.text}`, width),
552
+ ];
553
+ if (item.kind === 'assistant')
554
+ return [
555
+ '',
556
+ ...(mode === 'screen-reader' ? ['Praxis:'] : []),
557
+ ...textVisualRows(item.text, mode === 'screen-reader'
558
+ ? Math.max(1, width - 7)
559
+ : markdownContentWidth(width)),
560
+ ];
561
+ if (item.kind === 'thinking') {
562
+ const summary = item.text.replace(/\s+/gu, ' ').trim();
563
+ const heading = mode === 'screen-reader'
564
+ ? 'Thinking:Thought for a moment'
565
+ : `✻ Thought for a moment${summary ? ` · ${summary.slice(0, 160)}` : ''}`;
566
+ if (mode === 'normal')
567
+ return [
568
+ '',
569
+ ...textVisualRows(heading, width),
570
+ ...(summary.length > 160
571
+ ? textVisualRows(' ctrl+o to expand thinking', width)
572
+ : []),
573
+ ];
574
+ return [
575
+ '',
576
+ ...textVisualRows(heading, width),
577
+ ...textVisualRows(item.text, mode === 'screen-reader' ? width : markdownContentWidth(width)),
578
+ ];
579
+ }
580
+ if (item.kind === 'compact') {
581
+ const heading = mode === 'screen-reader'
582
+ ? 'Conversation compacted'
583
+ : '✻ Conversation compacted (ctrl+o for history)';
584
+ return [
585
+ '',
586
+ ...textVisualRows(heading, width),
587
+ ...(mode === 'normal'
588
+ ? []
589
+ : textVisualRows(item.summary, markdownContentWidth(width))),
590
+ ];
591
+ }
592
+ if (item.kind === 'local-result')
593
+ return textVisualRows(`⎿ ${item.text}`, Math.max(1, width - 2));
594
+ if (item.kind === 'warning')
595
+ return textVisualRows(`⚠ ${item.text}`, width);
596
+ if (item.kind === 'notice')
597
+ return textVisualRows(`· ${item.text}`, width);
598
+ return textVisualRows(item.text, width);
599
+ }
600
+ if (entry.kind === 'orphan-tool-result')
601
+ return [
602
+ entry.item.isError ? '└ Error' : '└ Result',
603
+ ...textVisualRows(entry.item.text, Math.max(1, width - 2)),
604
+ ];
605
+ if (entry.kind === 'orphan-shell-result')
606
+ return textVisualRows(`⎿ ${orphanShellOutput(entry.item.stdout, entry.item.stderr)}`, width);
607
+ if (entry.kind === 'shell') {
608
+ const rows = [
609
+ '',
610
+ ...textVisualRows(`${mode === 'screen-reader' ? 'Shell command: ' : '! '}${entry.item.command}`, width),
611
+ ];
612
+ if (!entry.result)
613
+ return rows;
614
+ const output = combinedShellOutput(entry.result.stdout, entry.result.stderr);
615
+ return [
616
+ ...rows,
617
+ ...(output
618
+ ? viewportOutputRows(output, width, mode)
619
+ : textVisualRows('⎿ ', Math.max(1, width - 2))),
620
+ ];
621
+ }
622
+ if (mode === 'normal' &&
623
+ entry.item.call.name === 'Read' &&
624
+ entry.result &&
625
+ !entry.result.isError)
626
+ return textVisualRows(' Read 1 file (ctrl+o to expand)', width);
627
+ const rows = ['', ...textVisualRows(`⏺ ${toolHeading(entry)}`, width)];
628
+ if (!['Bash', 'Read', 'Edit'].includes(entry.item.call.name) &&
629
+ entry.item.detail)
630
+ rows.push(...textVisualRows(` ${entry.item.detail}`, width));
631
+ if (!entry.result)
632
+ return rows;
633
+ if (entry.result.isError) {
634
+ rows.push(...textVisualRows(`⎿ Error: ${entry.result.text}`, Math.max(1, width - 2)));
635
+ return rows;
636
+ }
637
+ if (entry.item.call.name === 'Edit') {
638
+ const oldLines = contentLines(inputString(entry, 'old_string'));
639
+ const newLines = contentLines(inputString(entry, 'new_string'));
640
+ rows.push(...textVisualRows(`⎿ Added ${newLines.length} line${newLines.length === 1 ? '' : 's'}, removed ${oldLines.length} line${oldLines.length === 1 ? '' : 's'}`, Math.max(1, width - 2)));
641
+ if (mode !== 'normal') {
642
+ for (const [index, line] of oldLines.entries())
643
+ rows.push(...textVisualRows(` ${index + 1} -${line}`, Math.max(1, width - 2)));
644
+ for (const [index, line] of newLines.entries())
645
+ rows.push(...textVisualRows(` ${index + 1} +${line}`, Math.max(1, width - 2)));
646
+ }
647
+ return rows;
648
+ }
649
+ rows.push(...viewportOutputRows(entry.result.text, width, mode));
650
+ return rows;
651
+ }
652
+ function markdownViewportRows(text, width) {
653
+ const rows = [];
654
+ let insideFence = false;
655
+ let fenceLabel = 'code';
656
+ for (const logicalLine of text.replace(/\r\n?/gu, '\n').split('\n')) {
657
+ if (logicalLine.startsWith('```')) {
658
+ const before = insideFence;
659
+ if (!insideFence)
660
+ fenceLabel = logicalLine.slice(3) || 'code';
661
+ insideFence = !insideFence;
662
+ rows.push({
663
+ text: logicalLine,
664
+ fenceBefore: before,
665
+ fenceAfter: insideFence,
666
+ fenceLabel,
667
+ });
668
+ continue;
669
+ }
670
+ for (const row of textVisualRows(logicalLine, width))
671
+ rows.push({
672
+ text: row,
673
+ fenceBefore: insideFence,
674
+ fenceAfter: insideFence,
675
+ fenceLabel,
676
+ });
677
+ }
678
+ return rows;
679
+ }
680
+ export function createTranscriptEntryViewportIndex(entry, width, mode) {
681
+ if (entry.kind === 'item' && entry.item.kind === 'assistant') {
682
+ const contentWidth = mode === 'screen-reader'
683
+ ? Math.max(1, width - 7)
684
+ : markdownContentWidth(width);
685
+ const assistantMarkdownRows = markdownViewportRows(entry.item.text, contentWidth);
686
+ return {
687
+ entry,
688
+ width,
689
+ mode,
690
+ rows: [
691
+ '',
692
+ ...(mode === 'screen-reader' ? ['Praxis:'] : []),
693
+ ...assistantMarkdownRows.map((row) => row.text),
694
+ ],
695
+ assistantMarkdownRows,
696
+ };
697
+ }
698
+ const rows = entryViewportRows(entry, width, mode);
699
+ return rows ? { entry, width, mode, rows } : undefined;
700
+ }
701
+ function matchingViewportIndex(index, entry, width, mode) {
702
+ return index?.entry === entry && index.width === width && index.mode === mode
703
+ ? index
704
+ : undefined;
705
+ }
706
+ function projectAssistantMarkdownSlice(source, sourceText, startRow, budget, width, mode, viewportIndex) {
707
+ const limit = Math.max(1, budget);
708
+ const sourceChromeRows = 1 + (mode === 'screen-reader' ? 1 : 0);
709
+ const marginTop = startRow <= 0 ? 1 : 0;
710
+ const projectedChromeRows = marginTop + (mode === 'screen-reader' ? 1 : 0);
711
+ const contentBudget = Math.max(1, limit - projectedChromeRows);
712
+ const contentWidth = mode === 'screen-reader'
713
+ ? Math.max(1, width - 7)
714
+ : markdownContentWidth(width);
715
+ const sourceRows = matchingViewportIndex(viewportIndex, source, width, mode)
716
+ ?.assistantMarkdownRows ??
717
+ markdownViewportRows(sourceText ?? '', contentWidth);
718
+ const contentStart = Math.min(Math.max(0, startRow - sourceChromeRows + (startRow > 0 ? 1 : 0)), sourceRows.length);
719
+ const clippedBefore = contentStart > 0 && contentBudget > 1;
720
+ let remaining = Math.max(1, contentBudget - (clippedBefore ? 1 : 0));
721
+ let contentEnd = Math.min(sourceRows.length, contentStart + remaining);
722
+ const clippedAfter = contentEnd < sourceRows.length && remaining > 1;
723
+ if (clippedAfter) {
724
+ remaining -= 1;
725
+ contentEnd = Math.min(sourceRows.length, contentStart + remaining);
726
+ }
727
+ const selected = sourceRows.slice(contentStart, contentEnd);
728
+ const first = selected[0];
729
+ const last = selected.at(-1);
730
+ const needsSyntheticOpen = clippedBefore && first?.fenceBefore === true;
731
+ const needsSyntheticClose = clippedAfter && last?.fenceAfter === true;
732
+ const lines = [
733
+ ...(clippedBefore
734
+ ? [
735
+ needsSyntheticOpen
736
+ ? `\`\`\`${first?.fenceLabel || 'code'}`
737
+ : TRANSCRIPT_TRUNCATION_MARKER,
738
+ ]
739
+ : []),
740
+ ...selected.map((row) => row.text),
741
+ ...(clippedAfter
742
+ ? [needsSyntheticClose ? '```' : TRANSCRIPT_TRUNCATION_MARKER]
743
+ : []),
744
+ ];
745
+ const text = lines.join('\n');
746
+ return {
747
+ ...source,
748
+ item: { kind: 'assistant', text },
749
+ viewportSlice: {
750
+ text,
751
+ rows: projectedChromeRows + lines.length,
752
+ assistantMarkdown: { marginTop },
753
+ },
754
+ };
755
+ }
756
+ function withViewportSlice(projected, source, startRow, budget, width, mode, viewportIndex) {
757
+ const retainedIndex = matchingViewportIndex(viewportIndex, source, width, mode);
758
+ if (source.kind === 'item' &&
759
+ source.item.kind === 'assistant' &&
760
+ (retainedIndex?.rows.length ??
761
+ estimateRenderedTranscriptEntryLines(source, width, mode)) > budget)
762
+ return projectAssistantMarkdownSlice(source, retainedIndex ? undefined : source.item.text, startRow, budget, width, mode, retainedIndex);
763
+ const sourceRows = retainedIndex?.rows ?? entryViewportRows(source, width, mode);
764
+ if (!sourceRows || sourceRows.length <= budget)
765
+ return projected;
766
+ const limit = Math.max(1, budget);
767
+ const start = Math.min(Math.max(0, startRow + (startRow > 0 ? 1 : 0)), Math.max(0, sourceRows.length - 1));
768
+ const clippedBefore = start > 0 && limit > 1;
769
+ let remaining = Math.max(1, limit - (clippedBefore ? 1 : 0));
770
+ let end = Math.min(sourceRows.length, start + remaining);
771
+ const clippedAfter = end < sourceRows.length && remaining > 1;
772
+ if (clippedAfter) {
773
+ remaining -= 1;
774
+ end = Math.min(sourceRows.length, start + remaining);
775
+ }
776
+ const rows = [
777
+ ...(clippedBefore ? [TRANSCRIPT_TRUNCATION_MARKER] : []),
778
+ ...sourceRows.slice(start, end),
779
+ ...(clippedAfter ? [TRANSCRIPT_TRUNCATION_MARKER] : []),
780
+ ];
781
+ return {
782
+ ...projected,
783
+ viewportSlice: { text: rows.join('\n'), rows: rows.length },
784
+ };
785
+ }
417
786
  function fitEntryText(entry, value, replace, budget, viewportWidth, contentWidth, mode) {
418
787
  if (estimateTranscriptEntryLines(entry, viewportWidth, mode) <= budget ||
419
788
  !value)
420
789
  return entry;
421
790
  const withoutValue = replace(entry, '');
422
- const remaining = budget - estimateTranscriptEntryLines(withoutValue, viewportWidth, mode);
791
+ const remaining = budget -
792
+ Math.max(0, estimateTranscriptEntryLines(withoutValue, viewportWidth, mode) - 1);
423
793
  if (remaining <= 0)
424
794
  return withoutValue;
425
- const candidate = textTail(value, remaining, contentWidth);
426
- const projected = replace(entry, candidate);
427
- if (estimateTranscriptEntryLines(projected, viewportWidth, mode) <= budget)
428
- return projected;
429
- const clusters = graphemes(candidate);
430
- let low = 0;
431
- let high = clusters.length;
432
- let suffix = '';
433
- while (low <= high) {
434
- const middle = Math.floor((low + high) / 2);
435
- const next = clusters.slice(clusters.length - middle).join('');
436
- const nextEntry = replace(entry, next);
437
- if (estimateTranscriptEntryLines(nextEntry, viewportWidth, mode) <= budget) {
438
- suffix = next;
439
- low = middle + 1;
440
- }
441
- else {
442
- high = middle - 1;
443
- }
795
+ for (let allowed = remaining; allowed >= 1; allowed -= 1) {
796
+ const projected = replace(entry, textTail(value, allowed, contentWidth));
797
+ if (estimateTranscriptEntryLines(projected, viewportWidth, mode) <= budget)
798
+ return projected;
444
799
  }
445
- return suffix ? replace(entry, suffix) : withoutValue;
800
+ return withoutValue;
446
801
  }
447
802
  function replaceToolInput(entry, key, value) {
448
803
  return {
@@ -456,7 +811,23 @@ function replaceToolInput(entry, key, value) {
456
811
  },
457
812
  };
458
813
  }
459
- function projectOversizedEntry(entry, budget, width, mode) {
814
+ function rangeEntryText(entry, value, replace, startRow, budget, viewportWidth, contentWidth, mode) {
815
+ if (!value)
816
+ return entry;
817
+ const withoutValue = replace(entry, '');
818
+ // Empty renderer fields still account for one placeholder row. The source
819
+ // content begins before that placeholder, while the two clipping markers
820
+ // occupy the boundary rows of a middle projection.
821
+ const fixedRows = Math.max(0, estimateTranscriptEntryLines(withoutValue, viewportWidth, mode) - 1);
822
+ const contentStart = Math.max(0, startRow - fixedRows + (startRow > 0 ? 2 : 0));
823
+ for (let contentBudget = Math.max(1, budget - fixedRows); contentBudget >= 1; contentBudget -= 1) {
824
+ const projected = replace(entry, textRange(value, contentStart, contentBudget, contentWidth));
825
+ if (estimateTranscriptEntryLines(projected, viewportWidth, mode) <= budget)
826
+ return projected;
827
+ }
828
+ return withoutValue;
829
+ }
830
+ function projectOversizedEntryLegacy(entry, budget, width, mode, fromStart = false) {
460
831
  if (entry.kind === 'item') {
461
832
  const item = entry.item;
462
833
  if (item.kind === 'thinking')
@@ -464,7 +835,7 @@ function projectOversizedEntry(entry, budget, width, mode) {
464
835
  ...entry,
465
836
  item: {
466
837
  ...item,
467
- text: textTail(item.text, Math.max(1, budget - 2), markdownContentWidth(width)),
838
+ text: (fromStart ? textHead : textTail)(item.text, Math.max(1, budget - 2), markdownContentWidth(width)),
468
839
  },
469
840
  };
470
841
  if (item.kind === 'compact')
@@ -472,7 +843,7 @@ function projectOversizedEntry(entry, budget, width, mode) {
472
843
  ...entry,
473
844
  item: {
474
845
  ...item,
475
- summary: textTail(item.summary, Math.max(1, budget - 2), markdownContentWidth(width)),
846
+ summary: (fromStart ? textHead : textTail)(item.summary, Math.max(1, budget - 2), markdownContentWidth(width)),
476
847
  },
477
848
  };
478
849
  if ('text' in item)
@@ -480,7 +851,7 @@ function projectOversizedEntry(entry, budget, width, mode) {
480
851
  ...entry,
481
852
  item: {
482
853
  ...item,
483
- text: textTail(item.text, Math.max(1, budget -
854
+ text: (fromStart ? textHead : textTail)(item.text, Math.max(1, budget -
484
855
  (item.kind === 'user'
485
856
  ? 1
486
857
  : item.kind === 'assistant'
@@ -561,6 +932,85 @@ function projectOversizedEntry(entry, budget, width, mode) {
561
932
  }
562
933
  return entry;
563
934
  }
935
+ function projectOversizedEntry(entry, budget, width, mode, fromStart = false) {
936
+ const projected = projectOversizedEntryLegacy(entry, budget, width, mode, fromStart);
937
+ const rows = entryViewportRows(entry, width, mode);
938
+ if (!rows || rows.length <= budget)
939
+ return projected;
940
+ const visibleRows = Math.max(1, budget);
941
+ const start = fromStart ? 0 : Math.max(0, rows.length - visibleRows);
942
+ return withViewportSlice(projected, entry, start, budget, width, mode);
943
+ }
944
+ function projectTranscriptEntryRangeLegacy(entry, startRow, budget, width, mode) {
945
+ if (entry.kind === 'item' && 'text' in entry.item) {
946
+ const item = entry.item;
947
+ return rangeEntryText(entry, item.text, (source, text) => ({
948
+ ...source,
949
+ item: { ...source.item, text },
950
+ }), startRow, budget, width, item.kind === 'assistant'
951
+ ? mode === 'screen-reader'
952
+ ? Math.max(1, width - 7)
953
+ : markdownContentWidth(width)
954
+ : item.kind === 'user'
955
+ ? Math.max(1, width - (mode === 'screen-reader' ? 5 : 2))
956
+ : Math.max(1, width - (item.kind === 'local-result' ? 4 : 2)), mode);
957
+ }
958
+ if (entry.kind === 'item' && entry.item.kind === 'compact')
959
+ return rangeEntryText(entry, entry.item.summary, (source, summary) => ({
960
+ ...source,
961
+ item: { ...source.item, summary },
962
+ }), startRow, budget, width, markdownContentWidth(width), mode);
963
+ if (entry.kind === 'tool') {
964
+ if (entry.result && entry.item.call.name !== 'Edit') {
965
+ const result = entry.result;
966
+ return rangeEntryText(entry, result.text, (source, text) => ({
967
+ ...source,
968
+ result: { ...result, text },
969
+ }), startRow, budget, width, Math.max(1, width - 5), mode);
970
+ }
971
+ const inputKey = ['command', 'new_string', 'old_string', 'file_path'].find((key) => inputString(entry, key).length > 0);
972
+ if (inputKey)
973
+ return rangeEntryText(entry, inputString(entry, inputKey), (source, value) => replaceToolInput(source, inputKey, value), startRow, budget, width, Math.max(1, width - 8), mode);
974
+ if (entry.item.detail)
975
+ return rangeEntryText(entry, entry.item.detail, (source, detail) => ({
976
+ ...source,
977
+ item: { ...source.item, detail },
978
+ }), startRow, budget, width, Math.max(1, width - 1), mode);
979
+ }
980
+ if (entry.kind === 'shell') {
981
+ if (entry.result) {
982
+ const result = entry.result;
983
+ const combined = combinedShellOutput(result.stdout, result.stderr);
984
+ return rangeEntryText(entry, combined, (source, stdout) => ({
985
+ ...source,
986
+ result: { ...result, stdout, stderr: '' },
987
+ }), startRow, budget, width, Math.max(1, width - 5), mode);
988
+ }
989
+ return rangeEntryText(entry, entry.item.command, (source, command) => ({
990
+ ...source,
991
+ item: { ...source.item, command },
992
+ }), startRow, budget, width, Math.max(1, width - 4), mode);
993
+ }
994
+ if (entry.kind === 'orphan-tool-result')
995
+ return rangeEntryText(entry, entry.item.text, (source, text) => ({
996
+ ...source,
997
+ item: { ...source.item, text },
998
+ }), startRow, budget, width, Math.max(1, width - 2), mode);
999
+ if (entry.kind === 'orphan-shell-result') {
1000
+ const combined = orphanShellOutput(entry.item.stdout, entry.item.stderr);
1001
+ return rangeEntryText(entry, combined, (source, stdout) => ({
1002
+ ...source,
1003
+ item: { ...source.item, stdout, stderr: '' },
1004
+ }), startRow, budget, width, Math.max(1, width - 2), mode);
1005
+ }
1006
+ return projectOversizedEntry(entry, budget, width, mode);
1007
+ }
1008
+ export function projectTranscriptEntryRange(entry, startRow, budget, width, mode, viewportIndex) {
1009
+ const retainedIndex = matchingViewportIndex(viewportIndex, entry, width, mode);
1010
+ return withViewportSlice(retainedIndex
1011
+ ? entry
1012
+ : projectTranscriptEntryRangeLegacy(entry, startRow, budget, width, mode), entry, startRow, budget, width, mode, retainedIndex);
1013
+ }
564
1014
  export function projectTranscriptPresentationTail(entries, budget, width, mode) {
565
1015
  if (!entries.length)
566
1016
  return [];
@@ -583,24 +1033,32 @@ export function projectTranscriptPresentationTail(entries, budget, width, mode)
583
1033
  }
584
1034
  return entries;
585
1035
  }
586
- function projectTranscriptPresentationStart(entries, budget, width, mode) {
1036
+ export function projectTranscriptPresentationRange(entries, budget, width, startRow, mode) {
587
1037
  const limit = Math.max(1, budget);
588
- let rows = 0;
589
- let end = 0;
1038
+ const totalRows = transcriptPresentationLineCount(entries, width, mode);
1039
+ const rangeStart = Math.min(Math.max(0, startRow), totalRows);
1040
+ const rangeEnd = Math.min(totalRows, rangeStart + limit);
1041
+ const selected = [];
1042
+ let cursor = 0;
590
1043
  for (const entry of entries) {
591
1044
  const count = estimateTranscriptEntryLines(entry, width, mode);
592
- if (rows + count > limit) {
593
- if (end > 0)
594
- break;
595
- const projected = projectOversizedEntry(entry, limit, width, mode);
596
- return estimateTranscriptEntryLines(projected, width, mode) <= limit
597
- ? [projected]
598
- : [];
1045
+ const entryEnd = cursor + count;
1046
+ const overlapStart = Math.max(rangeStart, cursor);
1047
+ const overlapEnd = Math.min(rangeEnd, entryEnd);
1048
+ if (overlapStart < overlapEnd) {
1049
+ const localStart = overlapStart - cursor;
1050
+ const overlapRows = overlapEnd - overlapStart;
1051
+ const projected = localStart === 0 && overlapRows === count
1052
+ ? entry
1053
+ : projectTranscriptEntryRange(entry, localStart, overlapRows, width, mode);
1054
+ if (estimateTranscriptEntryLines(projected, width, mode) <= overlapRows)
1055
+ selected.push(projected);
599
1056
  }
600
- rows += count;
601
- end += 1;
1057
+ cursor = entryEnd;
1058
+ if (cursor >= rangeEnd)
1059
+ break;
602
1060
  }
603
- return entries.slice(0, end);
1061
+ return selected;
604
1062
  }
605
1063
  export function projectTranscriptPresentationWindow(entries, budget, width, scrollOffset, mode) {
606
1064
  if (scrollOffset <= 0)
@@ -608,58 +1066,6 @@ export function projectTranscriptPresentationWindow(entries, budget, width, scro
608
1066
  const limit = Math.max(1, budget);
609
1067
  const totalRows = transcriptPresentationLineCount(entries, width, mode);
610
1068
  const clampedOffset = Math.min(Math.max(0, scrollOffset), Math.max(0, totalRows - limit));
611
- const startRows = Math.max(0, totalRows - clampedOffset - limit);
612
- const endRows = totalRows - clampedOffset;
613
- let rows = 0;
614
- let start = entries.length;
615
- let startBeforeRows = 0;
616
- let startEntryRows = 0;
617
- for (let i = 0; i < entries.length; i += 1) {
618
- const entry = entries[i];
619
- if (!entry)
620
- continue;
621
- const count = estimateTranscriptEntryLines(entry, width, mode);
622
- const next = rows + count;
623
- if (start === entries.length && next > startRows) {
624
- start = i;
625
- startBeforeRows = rows;
626
- startEntryRows = count;
627
- }
628
- rows = next;
629
- }
630
- if (start === entries.length)
631
- return [];
632
- let completeEnd = start - 1;
633
- rows = 0;
634
- for (let i = 0; i < entries.length; i += 1) {
635
- const entry = entries[i];
636
- if (!entry)
637
- continue;
638
- rows += estimateTranscriptEntryLines(entry, width, mode);
639
- if (rows <= endRows)
640
- completeEnd = i;
641
- if (rows >= endRows)
642
- break;
643
- }
644
- const offsetWithinStart = startRows - startBeforeRows;
645
- const remainingStartRows = startEntryRows - offsetWithinStart;
646
- if (completeEnd < start) {
647
- const entry = entries[start];
648
- if (!entry)
649
- return [];
650
- return projectTranscriptPresentationStart([entry], limit, width, mode);
651
- }
652
- let candidates = entries.slice(start, completeEnd + 1);
653
- if (offsetWithinStart > 0) {
654
- const entry = candidates[0];
655
- if (entry) {
656
- const allowance = Math.min(limit, Math.max(1, remainingStartRows));
657
- candidates = [
658
- projectOversizedEntry(entry, allowance, width, mode),
659
- ...candidates.slice(1),
660
- ];
661
- }
662
- }
663
- return projectTranscriptPresentationStart(candidates, limit, width, mode);
1069
+ return projectTranscriptPresentationRange(entries, limit, width, Math.max(0, totalRows - clampedOffset - limit), mode);
664
1070
  }
665
1071
  //# sourceMappingURL=transcript-viewport.js.map