@deepseek-harness-tui/dsh-tui 0.8.6 → 0.8.8

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 (120) hide show
  1. package/README.md +15 -5
  2. package/bin/dsh-tui.js +269 -218
  3. package/cordis.patch.yml +48 -22
  4. package/lib/types/cc/figures.d.ts.map +1 -1
  5. package/lib/types/cc/figures.js +14 -13
  6. package/lib/types/cc/syntaxRuns.d.ts +16 -0
  7. package/lib/types/cc/syntaxRuns.d.ts.map +1 -0
  8. package/lib/types/cc/syntaxRuns.js +79 -0
  9. package/lib/types/commands.d.ts +14 -0
  10. package/lib/types/commands.d.ts.map +1 -1
  11. package/lib/types/commands.js +20 -1
  12. package/lib/types/components/Chat/SubagentMessage.d.ts +19 -0
  13. package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -0
  14. package/lib/types/components/Chat/SubagentMessage.js +75 -0
  15. package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
  16. package/lib/types/components/EffortInputBorder.js +2 -2
  17. package/lib/types/components/EffortTierBadge.js +1 -1
  18. package/lib/types/components/FileSuggestions.d.ts +8 -4
  19. package/lib/types/components/FileSuggestions.d.ts.map +1 -1
  20. package/lib/types/components/FileSuggestions.js +22 -12
  21. package/lib/types/components/HelpMenu.d.ts +8 -1
  22. package/lib/types/components/HelpMenu.d.ts.map +1 -1
  23. package/lib/types/components/HelpMenu.js +15 -4
  24. package/lib/types/components/LogoV2.d.ts +7 -1
  25. package/lib/types/components/LogoV2.d.ts.map +1 -1
  26. package/lib/types/components/LogoV2.js +13 -4
  27. package/lib/types/components/MessageList.d.ts +6 -2
  28. package/lib/types/components/MessageList.d.ts.map +1 -1
  29. package/lib/types/components/MessageList.js +17 -6
  30. package/lib/types/components/ModelPicker.js +2 -2
  31. package/lib/types/components/PromptInput.d.ts +1 -1
  32. package/lib/types/components/PromptInput.d.ts.map +1 -1
  33. package/lib/types/components/PromptInput.js +100 -36
  34. package/lib/types/components/SkillsPicker.js +2 -2
  35. package/lib/types/components/SplitDiffView.d.ts +1 -13
  36. package/lib/types/components/SplitDiffView.d.ts.map +1 -1
  37. package/lib/types/components/SplitDiffView.js +6 -105
  38. package/lib/types/components/SubagentCard.d.ts +9 -0
  39. package/lib/types/components/SubagentCard.d.ts.map +1 -0
  40. package/lib/types/components/SubagentCard.js +26 -0
  41. package/lib/types/components/SubagentDashboard.d.ts +13 -0
  42. package/lib/types/components/SubagentDashboard.d.ts.map +1 -0
  43. package/lib/types/components/SubagentDashboard.js +50 -0
  44. package/lib/types/components/SubagentDetailScene.d.ts +16 -0
  45. package/lib/types/components/SubagentDetailScene.d.ts.map +1 -0
  46. package/lib/types/components/SubagentDetailScene.js +141 -0
  47. package/lib/types/components/SyntaxText.d.ts +15 -0
  48. package/lib/types/components/SyntaxText.d.ts.map +1 -0
  49. package/lib/types/components/SyntaxText.js +34 -0
  50. package/lib/types/components/ThinkingToggle.d.ts +3 -10
  51. package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
  52. package/lib/types/components/ThinkingToggle.js +4 -9
  53. package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
  54. package/lib/types/components/design-system/ListItem.js +1 -1
  55. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  56. package/lib/types/components/messages/AssistantThinkingMessage.js +18 -7
  57. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +2 -0
  58. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  59. package/lib/types/components/messages/AssistantToolUseMessage.js +42 -6
  60. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  61. package/lib/types/components/messages/UserPromptMessage.js +15 -2
  62. package/lib/types/dsh-adapter/channel.d.ts +64 -3
  63. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  64. package/lib/types/dsh-adapter/channel.js +387 -37
  65. package/lib/types/dsh-adapter/contract.d.ts +67 -4
  66. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  67. package/lib/types/dsh-adapter/contract.js +149 -15
  68. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  69. package/lib/types/dsh-adapter/index.js +6 -6
  70. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  71. package/lib/types/dsh-adapter/plugin.js +38 -0
  72. package/lib/types/dsh-adapter/shortcuts.js +3 -3
  73. package/lib/types/dsh-adapter/subagents.d.ts +87 -0
  74. package/lib/types/dsh-adapter/subagents.d.ts.map +1 -0
  75. package/lib/types/dsh-adapter/subagents.js +204 -0
  76. package/lib/types/herdr.d.ts +28 -0
  77. package/lib/types/herdr.d.ts.map +1 -0
  78. package/lib/types/herdr.js +133 -0
  79. package/lib/types/i18n.d.ts +233 -32
  80. package/lib/types/i18n.d.ts.map +1 -1
  81. package/lib/types/i18n.js +87 -16
  82. package/lib/types/ink/ink.d.ts +4 -0
  83. package/lib/types/ink/ink.d.ts.map +1 -1
  84. package/lib/types/ink/ink.js +84 -38
  85. package/lib/types/ink/render-node-to-output.d.ts +6 -3
  86. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  87. package/lib/types/ink/render-node-to-output.js +33 -7
  88. package/lib/types/ink/selection.d.ts +29 -4
  89. package/lib/types/ink/selection.d.ts.map +1 -1
  90. package/lib/types/ink/selection.js +41 -5
  91. package/lib/types/minimalMode.d.ts +13 -0
  92. package/lib/types/minimalMode.d.ts.map +1 -0
  93. package/lib/types/minimalMode.js +17 -0
  94. package/lib/types/screens/Chat.d.ts +1 -1
  95. package/lib/types/screens/Chat.d.ts.map +1 -1
  96. package/lib/types/screens/Chat.js +94 -43
  97. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  98. package/lib/types/screens/StatusLine.js +6 -2
  99. package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
  100. package/lib/types/screens/TrajectoryScene.js +4 -2
  101. package/lib/types/theme.d.ts +8 -0
  102. package/lib/types/theme.d.ts.map +1 -1
  103. package/lib/types/theme.js +38 -14
  104. package/lib/types/tips.d.ts.map +1 -1
  105. package/lib/types/tips.js +25 -7
  106. package/lib/types/update.d.ts +18 -0
  107. package/lib/types/update.d.ts.map +1 -1
  108. package/lib/types/update.js +73 -2
  109. package/lib/types/utils/execFileNoThrow.d.ts +3 -20
  110. package/lib/types/utils/execFileNoThrow.d.ts.map +1 -1
  111. package/lib/types/utils/execFileNoThrow.js +53 -32
  112. package/lib/types/utils/externalEditor.d.ts +12 -11
  113. package/lib/types/utils/externalEditor.d.ts.map +1 -1
  114. package/lib/types/utils/externalEditor.js +11 -11
  115. package/lib/types/utils/fileSuggestions.d.ts +19 -0
  116. package/lib/types/utils/fileSuggestions.d.ts.map +1 -0
  117. package/lib/types/utils/fileSuggestions.js +40 -0
  118. package/lib/types/utils/mentions.d.ts.map +1 -1
  119. package/lib/types/utils/mentions.js +3 -1
  120. package/package.json +81 -74
@@ -30,7 +30,7 @@ import { applyPositionedHighlight, scanPositions } from './render-to-screen.js';
30
30
  import createRenderer from './renderer.js';
31
31
  import { CharPool, cellAt, createScreen, HyperlinkPool, isEmptyCellAt, migrateScreenPools, StylePool } from './screen.js';
32
32
  import { applySearchHighlight } from './searchHighlight.js';
33
- import { applySelectionOverlay, captureScrolledRows, clearSelection, createSelectionState, extendSelection, findPlainTextUrlAt, getSelectedText, hasSelection, moveFocus, selectLineAt, selectWordAt, shiftAnchor, shiftSelection, shiftSelectionForFollow, startSelection, updateSelection } from './selection.js';
33
+ import { applySelectionOverlay, captureScrolledRows, clearSelection, createSelectionState, extendSelection, findPlainTextUrlAt, getSelectedText, hasSelection, moveFocus, pickFollowForSelection, selectLineAt, selectWordAt, shiftAnchor, shiftSelection, shiftSelectionForFollow, startSelection, updateSelection } from './selection.js';
34
34
  import { isDecstbmSafe, SYNC_OUTPUT_SUPPORTED, supportsExtendedKeys, supportsWin32InputMode, writeDiffToTerminal } from './terminal.js';
35
35
  import { CURSOR_HOME, cursorMove, cursorPosition, DISABLE_KITTY_KEYBOARD, DISABLE_MODIFY_OTHER_KEYS, DISABLE_WIN32_INPUT_MODE, ENABLE_KITTY_KEYBOARD, ENABLE_MODIFY_OTHER_KEYS, ENABLE_WIN32_INPUT_MODE, ERASE_SCREEN, ERASE_SCROLLBACK, SGR_RESET } from './termio/csi.js';
36
36
  import { DBP, DFE, DISABLE_MOUSE_TRACKING, ENABLE_MOUSE_TRACKING, ENTER_ALT_SCREEN, EXIT_ALT_SCREEN, SHOW_CURSOR } from './termio/dec.js';
@@ -87,6 +87,10 @@ export default class Ink {
87
87
  backFrame;
88
88
  lastPoolResetTime = performance.now();
89
89
  drainTimer = null;
90
+ // Every scheduled microtask carries the generation that created it. Immediate
91
+ // renders invalidate older trailing work before it can append an old frame.
92
+ renderGeneration = 0;
93
+ pendingRenderGeneration = null;
90
94
  lastYogaCounters = {
91
95
  ms: 0,
92
96
  visited: 0,
@@ -195,8 +199,19 @@ export default class Ink {
195
199
  // effects have committed, so the native cursor tracks the caret without
196
200
  // a one-keystroke lag. Same event-loop tick, so throughput is unchanged.
197
201
  // Test env uses onImmediateRender (direct onRender, no throttle) so
198
- // existing synchronous lastFrame() tests are unaffected.
199
- const deferredRender = () => queueMicrotask(this.onRender);
202
+ // existing synchronous lastFrame() tests are unaffected. Keep a
203
+ // generation on the microtask: an immediate render may supersede the
204
+ // leading frame before its deferred callback runs.
205
+ const deferredRender = () => {
206
+ const generation = ++this.renderGeneration;
207
+ this.pendingRenderGeneration = generation;
208
+ queueMicrotask(() => {
209
+ if (this.pendingRenderGeneration !== generation || this.renderGeneration !== generation)
210
+ return;
211
+ this.pendingRenderGeneration = null;
212
+ this.onRender();
213
+ });
214
+ };
200
215
  this.scheduleRender = throttle(deferredRender, FRAME_INTERVAL_MS, {
201
216
  leading: true,
202
217
  trailing: true
@@ -220,7 +235,7 @@ export default class Ink {
220
235
  this.rootNode.focusManager = this.focusManager;
221
236
  this.renderer = createRenderer(this.rootNode, this.stylePool);
222
237
  this.rootNode.onRender = this.scheduleRender;
223
- this.rootNode.onImmediateRender = this.onRender;
238
+ this.rootNode.onImmediateRender = this.renderNow;
224
239
  this.rootNode.onComputeLayout = () => {
225
240
  // Calculate layout during React's commit phase so useLayoutEffect hooks
226
241
  // have access to fresh layout data
@@ -452,6 +467,17 @@ export default class Ink {
452
467
  return;
453
468
  this.log.requestViewportReanchor();
454
469
  }
470
+ /** Render synchronously and invalidate older trailing/drain callbacks. */
471
+ renderNow() {
472
+ this.renderGeneration++;
473
+ this.pendingRenderGeneration = null;
474
+ this.scheduleRender.cancel?.();
475
+ if (this.drainTimer !== null) {
476
+ clearTimeout(this.drainTimer);
477
+ this.drainTimer = null;
478
+ }
479
+ this.onRender();
480
+ }
455
481
  onRender() {
456
482
  if (this.isUnmounted || this.isPaused) {
457
483
  return;
@@ -481,10 +507,11 @@ export default class Ink {
481
507
  prevFrameContaminated: this.prevFrameContaminated
482
508
  });
483
509
  const rendererMs = performance.now() - renderStart;
484
- // Sticky/auto-follow scrolled the ScrollBox this frame. Translate the
485
- // selection by the same delta so the highlight stays anchored to the
486
- // TEXT (native terminal behavior — the selection walks up the screen
487
- // as content scrolls, eventually clipping at the top). frontFrame
510
+ // Sticky/auto-follow or wheel-drain scrolled one or more ScrollBoxes
511
+ // this frame. Translate the selection by the same delta so the highlight
512
+ // stays anchored to the TEXT (native terminal behavior — the
513
+ // selection walks up the screen as content scrolls, eventually
514
+ // clipping at the top). frontFrame
488
515
  // still holds the PREVIOUS frame's screen (swap is at ~500 below), so
489
516
  // captureScrolledRows reads the rows that are about to scroll out
490
517
  // before they're overwritten — the text stays copyable until the
@@ -492,16 +519,28 @@ export default class Ink {
492
519
  // (screen-local) so only anchor shifts — selection grows toward the
493
520
  // mouse as the anchor walks up. After release, both ends are text-
494
521
  // anchored and move as a block.
495
- const follow = consumeFollowScroll();
496
- if (follow && this.selection.anchor &&
497
- // Only translate if the selection is ON scrollbox content. Selections
498
- // in the footer/prompt/StickyPromptHeader are on static text — the
499
- // scroll doesn't move what's under them. Without this guard, a
500
- // footer selection would be shifted by -delta then clamped to
501
- // viewportBottom, teleporting it into the scrollbox. Mirror the
502
- // bounds check the deleted check() in ScrollKeybindingHandler had.
503
- this.selection.anchor.row >= follow.viewportTop && this.selection.anchor.row <= follow.viewportBottom) {
522
+ const follow = pickFollowForSelection(consumeFollowScroll(), this.selection.anchor?.row ?? null);
523
+ // pickFollowForSelection already checked anchor-in-viewport (that IS
524
+ // the "selection is on scrollbox content" guard — footer/prompt
525
+ // selections on static text match no viewport and follow nothing).
526
+ // Innermost-viewport wins attributes correctly when several boxes
527
+ // scrolled this frame (transcript draining while an overlay panel's
528
+ // box scrolls): panels render on top, so overlap-row selections
529
+ // belong to the panel, not the covered transcript.
530
+ if (follow && this.selection.anchor) {
504
531
  const { delta, viewportTop, viewportBottom } = follow;
532
+ // Signed delta: >0 = content moved up (at-bottom follow or
533
+ // wheel-down drain); <0 = content moved down (wheel-up drain, #438).
534
+ // The capture window is the viewport-edge rows about to scroll out
535
+ // (top edge when content moves up, bottom edge when it moves down),
536
+ // and the shift re-anchors the endpoints by the same amount in the
537
+ // OPPOSITE direction so they track the text, not the screen.
538
+ const rows = Math.abs(delta);
539
+ const up = delta > 0;
540
+ const firstRow = up ? viewportTop : viewportBottom - rows + 1;
541
+ const lastRow = up ? viewportTop + rows - 1 : viewportBottom;
542
+ const side = up ? 'above' : 'below';
543
+ const shift = up ? -rows : rows;
505
544
  // captureScrolledRows and shift* are a pair: capture grabs rows about
506
545
  // to scroll off, shift moves the selection endpoint so the same rows
507
546
  // won't intersect again next frame. Capturing without shifting leaves
@@ -511,9 +550,9 @@ export default class Ink {
511
550
  // each shift branch so the pairing can't be broken by a new guard.
512
551
  if (this.selection.isDragging) {
513
552
  if (hasSelection(this.selection)) {
514
- captureScrolledRows(this.selection, this.frontFrame.screen, viewportTop, viewportTop + delta - 1, 'above');
553
+ captureScrolledRows(this.selection, this.frontFrame.screen, firstRow, lastRow, side);
515
554
  }
516
- shiftAnchor(this.selection, -delta, viewportTop, viewportBottom);
555
+ shiftAnchor(this.selection, shift, viewportTop, viewportBottom);
517
556
  }
518
557
  else if (
519
558
  // Flag-3 guard: the anchor check above only proves ONE endpoint is
@@ -530,14 +569,16 @@ export default class Ink {
530
569
  // is correct there even when focus is in the footer).
531
570
  !this.selection.focus || this.selection.focus.row >= viewportTop && this.selection.focus.row <= viewportBottom) {
532
571
  if (hasSelection(this.selection)) {
533
- captureScrolledRows(this.selection, this.frontFrame.screen, viewportTop, viewportTop + delta - 1, 'above');
572
+ captureScrolledRows(this.selection, this.frontFrame.screen, firstRow, lastRow, side);
534
573
  }
535
- const cleared = shiftSelectionForFollow(this.selection, -delta, viewportTop, viewportBottom);
536
- // Auto-clear (both ends overshot minRow) must notify React-land
537
- // so useHasSelection re-renders and the footer copy/escape hint
538
- // disappears. notifySelectionChange() would recurse into onRender;
539
- // fire the listeners directly — they schedule a React update for
540
- // LATER, they don't re-enter this frame.
574
+ const cleared = shiftSelectionForFollow(this.selection, shift, viewportTop, viewportBottom);
575
+ // Auto-clear (both ends overshot an edge — off the top via
576
+ // follow/wheel-down, off the bottom via wheel-up) must notify
577
+ // React-land so useHasSelection re-renders and the footer
578
+ // copy/escape hint disappears. notifySelectionChange() would
579
+ // recurse into onRender; fire the listeners directly — they
580
+ // schedule a React update for LATER, they don't re-enter this
581
+ // frame.
541
582
  if (cleared)
542
583
  for (const cb of this.selectionListeners)
543
584
  cb();
@@ -689,6 +730,12 @@ export default class Ink {
689
730
  // and no move is emitted.
690
731
  const decl = this.cursorDeclaration;
691
732
  const rect = decl !== null ? nodeCache.get(decl.node) : undefined;
733
+ // Keep the declared target in the same full-frame coordinate system as
734
+ // frame.cursor and displayCursor. Main-screen cursor moves are relative:
735
+ // subtracting the scrollback height from target alone makes the physical
736
+ // cursor climb that height on every park/preamble cycle, so later streaming
737
+ // diffs overwrite thinking, tool, and assistant rows. The terminal maps the
738
+ // full-frame relative move onto its viewport/scrollback position itself.
692
739
  const target = decl !== null && rect !== undefined ? {
693
740
  x: rect.x + decl.relativeX,
694
741
  y: rect.y + decl.relativeY
@@ -781,15 +828,14 @@ export default class Ink {
781
828
  // trailing-edge throttle invocation, timerId is undefined, and lodash's
782
829
  // debounce sees timeSinceLastCall >= wait (last call was at the start
783
830
  // of this window) → leadingEdge fires IMMEDIATELY → double render ~0.1ms
784
- // apart → jank. Use a plain timeout. If a wheel event arrives first,
785
- // its scheduleRender path fires a render which clears this timer at
786
- // the top of onRender — no double.
831
+ // apart → jank. Use a plain timeout. If a wheel event or immediate
832
+ // render arrives first, renderNow cancels this timer — no double.
787
833
  //
788
834
  // Drain frames are cheap (DECSTBM + ~10 patches, ~200 bytes) so run at
789
835
  // quarter interval (~250fps, setTimeout practical floor) for max scroll
790
836
  // speed. Regular renders stay at FRAME_INTERVAL_MS via the throttle.
791
837
  if (frame.scrollDrainPending) {
792
- this.drainTimer = setTimeout(() => this.onRender(), FRAME_INTERVAL_MS >> 2);
838
+ this.drainTimer = setTimeout(this.renderNow, FRAME_INTERVAL_MS >> 2);
793
839
  }
794
840
  const yogaMs = getLastYogaMs();
795
841
  const commitMs = getLastCommitMs();
@@ -825,12 +871,12 @@ export default class Ink {
825
871
  // Flush pending React updates and render before pausing.
826
872
  // @ts-ignore -- ported CC build; type drift tolerated flushSyncFromReconciler exists in react-reconciler 0.31 but not in @types/react-reconciler
827
873
  reconciler.flushSyncFromReconciler();
828
- this.onRender();
874
+ this.renderNow();
829
875
  this.isPaused = true;
830
876
  }
831
877
  resume() {
832
878
  this.isPaused = false;
833
- this.onRender();
879
+ this.renderNow();
834
880
  }
835
881
  /**
836
882
  * Reset frame buffers so the next render writes the full screen from scratch.
@@ -872,7 +918,7 @@ export default class Ink {
872
918
  // diff sees no content. onRender resets the flag at frame end.
873
919
  this.prevFrameContaminated = true;
874
920
  }
875
- this.onRender();
921
+ this.renderNow();
876
922
  }
877
923
  /**
878
924
  * Establish a genuinely fresh terminal page: clear both the visible screen
@@ -895,7 +941,7 @@ export default class Ink {
895
941
  this.repaint();
896
942
  this.prevFrameContaminated = true;
897
943
  }
898
- this.onRender();
944
+ this.renderNow();
899
945
  }
900
946
  /**
901
947
  * Mark the previous frame as untrustworthy for blit, forcing the next
@@ -1004,7 +1050,7 @@ export default class Ink {
1004
1050
  // cursor position. Idempotent when nothing drifted — the user sees
1005
1051
  // no change, at O(viewport) bytes once per >5s idle gap.
1006
1052
  this.log.requestViewportReanchor();
1007
- this.onRender();
1053
+ this.renderNow();
1008
1054
  return;
1009
1055
  }
1010
1056
  // Mouse tracking — idempotent, safe to re-assert on every stdin gap.
@@ -1171,7 +1217,7 @@ export default class Ink {
1171
1217
  // drops it silently unless allow-passthrough is on — no regression).
1172
1218
  void setClipboard(text).then(raw => {
1173
1219
  if (raw)
1174
- this.options.stdout.write(raw);
1220
+ this.writeRaw(raw);
1175
1221
  });
1176
1222
  }
1177
1223
  return text;
@@ -1388,7 +1434,7 @@ export default class Ink {
1388
1434
  return () => this.selectionListeners.delete(cb);
1389
1435
  }
1390
1436
  notifySelectionChange() {
1391
- this.onRender();
1437
+ this.renderNow();
1392
1438
  for (const cb of this.selectionListeners)
1393
1439
  cb();
1394
1440
  }
@@ -1596,7 +1642,7 @@ export default class Ink {
1596
1642
  if (this.isUnmounted) {
1597
1643
  return;
1598
1644
  }
1599
- this.onRender();
1645
+ this.renderNow();
1600
1646
  this.unsubscribeExit();
1601
1647
  if (typeof this.restoreConsole === 'function') {
1602
1648
  this.restoreConsole();
@@ -47,10 +47,13 @@ export type FollowScroll = {
47
47
  viewportBottom: number;
48
48
  };
49
49
  /**
50
- * Read and clear the follow-scroll event recorded this frame.
51
- * @returns the follow-scroll delta and viewport bounds, or null.
50
+ * Read and clear the follow-scroll events recorded this frame. At most one
51
+ * per ScrollBox (a box that follows AND drains reports only the follow —
52
+ * the follow branch clears pendingScrollDelta before the drain runs);
53
+ * several boxes may each report when they scroll in the same frame.
54
+ * @returns this frame's follow-scroll events; empty when none.
52
55
  */
53
- export declare function consumeFollowScroll(): FollowScroll | null;
56
+ export declare function consumeFollowScroll(): FollowScroll[];
54
57
  /**
55
58
  * Build a mapping from each character position in the plain text to its segment index.
56
59
  * Returns an array where charToSegment[i] is the segment index for character i.
@@ -1 +1 @@
1
- {"version":3,"file":"render-node-to-output.d.ts","sourceRoot":"","sources":["../../../src/ink/render-node-to-output.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAK1C,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EACL,KAAK,aAAa,EAEnB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAqBxC,6CAA6C;AAC7C,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAOD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAWvE,qFAAqF;AACrF,wBAAgB,eAAe,IAAI,IAAI,CAItC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AASD,8DAA8D;AAC9D,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,UAAU,GAAG,IAAI,CAEtD;AAUD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAGD;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,YAAY,GAAG,IAAI,CAIzD;AAoFD;;;;;GAKG;AACH,iBAAS,qBAAqB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,MAAM,EAAE,CASlE;AAED;;;;;;;;;;;GAWG;AACH,iBAAS,wBAAwB,CAC/B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,aAAa,EAAE,EACzB,aAAa,EAAE,MAAM,EAAE,EACvB,aAAa,EAAE,MAAM,EACrB,WAAW,GAAE,OAAe,GAC3B,MAAM,CA0GR;AA+DD;;;;;;;;;GASG;AACH,iBAAS,kBAAkB,CACzB,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;IACP,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IAM9B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,wBAAwB,CAAC,EAAE,KAAK,CAAA;CACjC,GACA,IAAI,CA62BN;AAyOD,OAAO,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,CAAA;AAE1D,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"render-node-to-output.d.ts","sourceRoot":"","sources":["../../../src/ink/render-node-to-output.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAK1C,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EACL,KAAK,aAAa,EAEnB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAqBxC,6CAA6C;AAC7C,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAOD;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAWvE,qFAAqF;AACrF,wBAAgB,eAAe,IAAI,IAAI,CAItC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AASD,8DAA8D;AAC9D,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,UAAU,GAAG,IAAI,CAEtD;AAgBD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAGD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,IAAI,YAAY,EAAE,CAIpD;AAoFD;;;;;GAKG;AACH,iBAAS,qBAAqB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,MAAM,EAAE,CASlE;AAED;;;;;;;;;;;GAWG;AACH,iBAAS,wBAAwB,CAC/B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,aAAa,EAAE,EACzB,aAAa,EAAE,MAAM,EAAE,EACvB,aAAa,EAAE,MAAM,EACrB,WAAW,GAAE,OAAe,GAC3B,MAAM,CA0GR;AA+DD;;;;;;;;;GASG;AACH,iBAAS,kBAAkB,CACzB,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;IACP,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IAM9B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,wBAAwB,CAAC,EAAE,KAAK,CAAA;CACjC,GACA,IAAI,CAo4BN;AAyOD,OAAO,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,CAAA;AAE1D,eAAe,kBAAkB,CAAA"}
@@ -71,14 +71,17 @@ export function resetScrollDrainNode() {
71
71
  export function getScrollDrainNode() {
72
72
  return scrollDrainNode;
73
73
  }
74
- let followScroll = null;
74
+ let followScrolls = [];
75
75
  /**
76
- * Read and clear the follow-scroll event recorded this frame.
77
- * @returns the follow-scroll delta and viewport bounds, or null.
76
+ * Read and clear the follow-scroll events recorded this frame. At most one
77
+ * per ScrollBox (a box that follows AND drains reports only the follow —
78
+ * the follow branch clears pendingScrollDelta before the drain runs);
79
+ * several boxes may each report when they scroll in the same frame.
80
+ * @returns this frame's follow-scroll events; empty when none.
78
81
  */
79
82
  export function consumeFollowScroll() {
80
- const f = followScroll;
81
- followScroll = null;
83
+ const f = followScrolls;
84
+ followScrolls = [];
82
85
  return f;
83
86
  }
84
87
  // ── Native terminal drain (iTerm2/Ghostty/etc. — proportional events) ──
@@ -684,11 +687,11 @@ function renderNodeToOutput(node, output, options) {
684
687
  const followDelta = (node.scrollTop ?? 0) - scrollTopBeforeFollow;
685
688
  if (followDelta > 0) {
686
689
  const vpTop = node.scrollViewportTop ?? 0;
687
- followScroll = {
690
+ followScrolls.push({
688
691
  delta: followDelta,
689
692
  viewportTop: vpTop,
690
693
  viewportBottom: vpTop + innerHeight - 1,
691
- };
694
+ });
692
695
  }
693
696
  // Drain pendingScrollDelta. Native terminals (proportional burst
694
697
  // events) use proportional drain; xterm.js (VS Code, sparse events +
@@ -749,6 +752,29 @@ function renderNodeToOutput(node, output, options) {
749
752
  node.pendingScrollDelta = undefined;
750
753
  if (node.pendingScrollDelta !== undefined)
751
754
  scrollDrainNode = node;
755
+ // Wheel-drain selection translate (#438): the drain moved content
756
+ // by (scrollTop - scrollTopBeforeFollow) rows this frame, minus
757
+ // what at-bottom follow already reported above (followDelta is 0
758
+ // unless the follow branch fired — and when it did, it cleared
759
+ // pendingScrollDelta, so the drain contributed nothing). Record
760
+ // the remainder as a follow-scroll event with a SIGNED delta so
761
+ // ink.tsx re-anchors any active selection to the text. Without
762
+ // this, wheel scrolling leaves the highlight pinned to screen
763
+ // rows and copy-on-select grabs whatever scrolled under it.
764
+ // scrollTo/scrollToElement jumps never contribute: they write
765
+ // scrollTop before scrollTopBeforeFollow is captured. Multi-frame
766
+ // drains record per-frame portions; the selection's virtual-row
767
+ // tracking accumulates the clamp overshoot across frames. Multiple
768
+ // boxes may each record; ink.tsx attributes by viewport containment.
769
+ const wheelDelta = scrollTop - scrollTopBeforeFollow - followDelta;
770
+ if (wheelDelta !== 0) {
771
+ const wheelVpTop = node.scrollViewportTop ?? 0;
772
+ followScrolls.push({
773
+ delta: wheelDelta,
774
+ viewportTop: wheelVpTop,
775
+ viewportBottom: wheelVpTop + innerHeight - 1
776
+ });
777
+ }
752
778
  // A manual scroll that lands exactly on the bottom re-pins sticky
753
779
  // IMMEDIATELY on this frame — the follow-block restore above only
754
780
  // fires when a later frame happens, but an idle stream (turn done,
@@ -213,10 +213,12 @@ export declare function shiftAnchor(s: SelectionState, dRow: number, minRow: num
213
213
  * the selection is text-anchored at both ends — both must move. The
214
214
  * isDragging check in ink.tsx picks which shift to apply.
215
215
  *
216
- * If both ends would shift strictly BELOW minRow (unclamped), the selected
217
- * text has scrolled entirely off the top. Clear it — otherwise a single
218
- * inverted cell lingers at the viewport top as a ghost (native terminals
219
- * drop the selection when it leaves scrollback). Landing AT minRow is
216
+ * If both ends would shift strictly BELOW minRow or strictly ABOVE maxRow
217
+ * (unclamped), the selected text has scrolled entirely off the viewport —
218
+ * off the top via streaming follow / wheel-down, off the bottom via
219
+ * wheel-up. Clear it — otherwise a single inverted cell lingers at the
220
+ * edge as a ghost (native terminals drop the selection when it leaves
221
+ * scrollback). Landing AT the edge row is
220
222
  * still valid: that cell holds the correct text. Returns true if the
221
223
  * selection was cleared so the caller can notify React-land subscribers
222
224
  * (useHasSelection) — the caller is inside onRender so it can't use
@@ -229,6 +231,29 @@ export declare function shiftAnchor(s: SelectionState, dRow: number, minRow: num
229
231
  * entirely off the top, false otherwise.
230
232
  */
231
233
  export declare function shiftSelectionForFollow(s: SelectionState, dRow: number, minRow: number, maxRow: number): boolean;
234
+ /** A scroll event reported by a ScrollBox this frame (follow or wheel
235
+ * drain): signed delta plus the box's viewport bounds. Structurally
236
+ * identical to FollowScroll in render-node-to-output. */
237
+ export type ScrollEvent = {
238
+ delta: number;
239
+ viewportTop: number;
240
+ viewportBottom: number;
241
+ };
242
+ /**
243
+ * Pick which scroll event a selection belongs to when several ScrollBoxes
244
+ * scrolled in the same frame (e.g., the transcript still draining a wheel
245
+ * burst while an overlay panel's box scrolls). The selection follows the
246
+ * INNERMOST viewport containing the anchor: overlay panels render on top
247
+ * of the transcript, so a selection inside the overlap rows belongs to
248
+ * the panel, not the covered transcript beneath it. A selection outside
249
+ * every viewport (footer/prompt, static text) follows nothing — scrolling
250
+ * doesn't move the content under it.
251
+ * @param events - this frame's scroll events (signed deltas).
252
+ * @param anchorRow - the selection anchor's screen row, or null when no
253
+ * selection is active.
254
+ * @returns the event the selection should be translated by, or null.
255
+ */
256
+ export declare function pickFollowForSelection(events: ScrollEvent[], anchorRow: number | null): ScrollEvent | null;
232
257
  /**
233
258
  * True when a selection is active, meaning both anchor and focus are set.
234
259
  * @param s - the selection state to inspect.
@@ -1 +1 @@
1
- {"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../../../src/ink/selection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../../../src/ink/selection.ts"],"names":[],"mappings":"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"}
@@ -579,10 +579,12 @@ export function shiftAnchor(s, dRow, minRow, maxRow) {
579
579
  * the selection is text-anchored at both ends — both must move. The
580
580
  * isDragging check in ink.tsx picks which shift to apply.
581
581
  *
582
- * If both ends would shift strictly BELOW minRow (unclamped), the selected
583
- * text has scrolled entirely off the top. Clear it — otherwise a single
584
- * inverted cell lingers at the viewport top as a ghost (native terminals
585
- * drop the selection when it leaves scrollback). Landing AT minRow is
582
+ * If both ends would shift strictly BELOW minRow or strictly ABOVE maxRow
583
+ * (unclamped), the selected text has scrolled entirely off the viewport —
584
+ * off the top via streaming follow / wheel-down, off the bottom via
585
+ * wheel-up. Clear it — otherwise a single inverted cell lingers at the
586
+ * edge as a ghost (native terminals drop the selection when it leaves
587
+ * scrollback). Landing AT the edge row is
586
588
  * still valid: that cell holds the correct text. Returns true if the
587
589
  * selection was cleared so the caller can notify React-land subscribers
588
590
  * (useHasSelection) — the caller is inside onRender so it can't use
@@ -609,7 +611,11 @@ export function shiftSelectionForFollow(s, dRow, minRow, maxRow) {
609
611
  const rawFocus = s.focus
610
612
  ? (s.virtualFocusRow ?? s.focus.row) + dRow
611
613
  : undefined;
612
- if (rawAnchor < minRow && rawFocus !== undefined && rawFocus < minRow) {
614
+ // Both ends strictly past the same edge = selection fully scrolled off
615
+ // (top: follow/wheel-down; bottom: wheel-up). Symmetric clear — a
616
+ // clamped-to-edge pair would render as a 1-row ghost highlight.
617
+ if ((rawAnchor < minRow && rawFocus !== undefined && rawFocus < minRow) ||
618
+ (rawAnchor > maxRow && rawFocus !== undefined && rawFocus > maxRow)) {
613
619
  clearSelection(s);
614
620
  return true;
615
621
  }
@@ -640,6 +646,36 @@ export function shiftSelectionForFollow(s, dRow, minRow, maxRow) {
640
646
  }
641
647
  return false;
642
648
  }
649
+ /**
650
+ * Pick which scroll event a selection belongs to when several ScrollBoxes
651
+ * scrolled in the same frame (e.g., the transcript still draining a wheel
652
+ * burst while an overlay panel's box scrolls). The selection follows the
653
+ * INNERMOST viewport containing the anchor: overlay panels render on top
654
+ * of the transcript, so a selection inside the overlap rows belongs to
655
+ * the panel, not the covered transcript beneath it. A selection outside
656
+ * every viewport (footer/prompt, static text) follows nothing — scrolling
657
+ * doesn't move the content under it.
658
+ * @param events - this frame's scroll events (signed deltas).
659
+ * @param anchorRow - the selection anchor's screen row, or null when no
660
+ * selection is active.
661
+ * @returns the event the selection should be translated by, or null.
662
+ */
663
+ export function pickFollowForSelection(events, anchorRow) {
664
+ if (anchorRow === null)
665
+ return null;
666
+ let best = null;
667
+ let bestHeight = Infinity;
668
+ for (const e of events) {
669
+ if (anchorRow < e.viewportTop || anchorRow > e.viewportBottom)
670
+ continue;
671
+ const height = e.viewportBottom - e.viewportTop;
672
+ if (best === null || height < bestHeight) {
673
+ best = e;
674
+ bestHeight = height;
675
+ }
676
+ }
677
+ return best;
678
+ }
643
679
  /**
644
680
  * True when a selection is active, meaning both anchor and focus are set.
645
681
  * @param s - the selection state to inspect.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Minimal mode: one process-wide flag gating decorative UI (header splash,
3
+ * emoji status glyphs, non-tool text colors, status-bar extras). The flag is
4
+ * owned by the channel (settings `dsh-tui.minimal`, applied live through
5
+ * scope.watch); leaf components read it here instead of threading a prop
6
+ * through four render layers. Code highlighting and tool-name colors are
7
+ * deliberately NOT gated — minimal keeps those.
8
+ */
9
+ /** True when minimal mode is active (settings `dsh-tui.minimal`). */
10
+ export declare function isMinimalMode(): boolean;
11
+ /** @internal channel-owned setter; also called once at boot. */
12
+ export declare function setMinimalMode(enabled: boolean): void;
13
+ //# sourceMappingURL=minimalMode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"minimalMode.d.ts","sourceRoot":"","sources":["../../src/minimalMode.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,qEAAqE;AACrE,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAErD"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Minimal mode: one process-wide flag gating decorative UI (header splash,
3
+ * emoji status glyphs, non-tool text colors, status-bar extras). The flag is
4
+ * owned by the channel (settings `dsh-tui.minimal`, applied live through
5
+ * scope.watch); leaf components read it here instead of threading a prop
6
+ * through four render layers. Code highlighting and tool-name colors are
7
+ * deliberately NOT gated — minimal keeps those.
8
+ */
9
+ let minimal = false;
10
+ /** True when minimal mode is active (settings `dsh-tui.minimal`). */
11
+ export function isMinimalMode() {
12
+ return minimal;
13
+ }
14
+ /** @internal channel-owned setter; also called once at boot. */
15
+ export function setMinimalMode(enabled) {
16
+ minimal = enabled;
17
+ }
@@ -44,5 +44,5 @@ export declare function Chat({ channel, questionStore, approvalStore, extensionD
44
44
  * persisted flag when the host does not supply one.
45
45
  */
46
46
  trajectorySeen?: boolean;
47
- }): React.JSX.Element;
47
+ }): React.JSX.Element | null;
48
48
  //# sourceMappingURL=Chat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../src/screens/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,EAAqB,KAAK,OAAO,EAAsE,MAAM,2BAA2B,CAAA;AAC/I,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAGlE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAyI3D,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,MAAM,EACN,QAAQ,EACR,UAAkB,EAClB,cAAc,EAAE,kBAAkB,GACnC,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,aAAa,CAAA;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAA;IACjC,oEAAoE;IACpE,eAAe,CAAC,EAAE,cAAc,CAAA;IAChC,iDAAiD;IACjD,kBAAkB,CAAC,EAAE,eAAe,CAAA;IACpC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,qBAioEA"}
1
+ {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../src/screens/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,EAAqB,KAAK,OAAO,EAAsE,MAAM,2BAA2B,CAAA;AAC/I,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAGlE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AA4I3D,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,MAAM,EACN,QAAQ,EACR,UAAkB,EAClB,cAAc,EAAE,kBAAkB,GACnC,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,aAAa,CAAA;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAA;IACjC,oEAAoE;IACpE,eAAe,CAAC,EAAE,cAAc,CAAA;IAChC,iDAAiD;IACjD,kBAAkB,CAAC,EAAE,eAAe,CAAA;IACpC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,4BAisEA"}