@austinthesing/magic-shell 0.1.2 → 0.2.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 (4) hide show
  1. package/README.md +15 -25
  2. package/dist/cli.js +1724 -194
  3. package/dist/index.js +2154 -400
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -831,6 +831,13 @@ function t(strings, ...values) {
831
831
  }
832
832
  return new StyledText(chunks);
833
833
  }
834
+
835
+ class LinearScrollAccel {
836
+ tick(_now) {
837
+ return 1;
838
+ }
839
+ reset() {}
840
+ }
834
841
  function isCompleteSequence(data) {
835
842
  if (!data.startsWith(ESC)) {
836
843
  return "not-escape";
@@ -16650,7 +16657,7 @@ function canonicalize(obj, stack, replacementStack, replacer, key) {
16650
16657
  }
16651
16658
  return canonicalizedObj;
16652
16659
  }
16653
- var EditBuffer, engine, BoxRenderable, TextBufferRenderable, BrandedTextNodeRenderable, TextNodeRenderable, RootTextNodeRenderable, CharacterDiff, characterDiff, extendedWordChars = "a-zA-Z0-9_\\u{C0}-\\u{FF}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}", tokenizeIncludingWhitespace, WordDiff, wordDiff, WordsWithSpaceDiff, wordsWithSpaceDiff, LineDiff, lineDiff, SentenceDiff, sentenceDiff, CssDiff, cssDiff, JsonDiff, jsonDiff, ArrayDiff, arrayDiff, TextRenderable, defaultInputKeybindings, InputRenderableEvents, InputRenderable, defaultThumbBackgroundColor, defaultTrackBackgroundColor, defaultSelectKeybindings, SelectRenderableEvents, SelectRenderable, TabSelectRenderableEvents, EditBufferRenderable;
16660
+ var EditBuffer, engine, BoxRenderable, TextBufferRenderable, BrandedTextNodeRenderable, TextNodeRenderable, RootTextNodeRenderable, CharacterDiff, characterDiff, extendedWordChars = "a-zA-Z0-9_\\u{C0}-\\u{FF}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}", tokenizeIncludingWhitespace, WordDiff, wordDiff, WordsWithSpaceDiff, wordsWithSpaceDiff, LineDiff, lineDiff, SentenceDiff, sentenceDiff, CssDiff, cssDiff, JsonDiff, jsonDiff, ArrayDiff, arrayDiff, TextRenderable, defaultInputKeybindings, InputRenderableEvents, InputRenderable, defaultThumbBackgroundColor, defaultTrackBackgroundColor, SliderRenderable, ScrollBarRenderable, ArrowRenderable, ContentRenderable, ScrollBoxRenderable, defaultSelectKeybindings, SelectRenderableEvents, SelectRenderable, TabSelectRenderableEvents, EditBufferRenderable;
16654
16661
  var init_core = __esm(async () => {
16655
16662
  await init_index_93qf6w1k();
16656
16663
  EditBuffer = class EditBuffer extends EventEmitter10 {
@@ -18369,6 +18376,1146 @@ var init_core = __esm(async () => {
18369
18376
  };
18370
18377
  defaultThumbBackgroundColor = RGBA.fromHex("#9a9ea3");
18371
18378
  defaultTrackBackgroundColor = RGBA.fromHex("#252527");
18379
+ SliderRenderable = class SliderRenderable extends Renderable {
18380
+ orientation;
18381
+ _value;
18382
+ _min;
18383
+ _max;
18384
+ _viewPortSize;
18385
+ _backgroundColor;
18386
+ _foregroundColor;
18387
+ _onChange;
18388
+ constructor(ctx, options) {
18389
+ super(ctx, { flexShrink: 0, ...options });
18390
+ this.orientation = options.orientation;
18391
+ this._min = options.min ?? 0;
18392
+ this._max = options.max ?? 100;
18393
+ this._value = options.value ?? this._min;
18394
+ this._viewPortSize = options.viewPortSize ?? Math.max(1, (this._max - this._min) * 0.1);
18395
+ this._onChange = options.onChange;
18396
+ this._backgroundColor = options.backgroundColor ? parseColor(options.backgroundColor) : defaultTrackBackgroundColor;
18397
+ this._foregroundColor = options.foregroundColor ? parseColor(options.foregroundColor) : defaultThumbBackgroundColor;
18398
+ this.setupMouseHandling();
18399
+ }
18400
+ get value() {
18401
+ return this._value;
18402
+ }
18403
+ set value(newValue) {
18404
+ const clamped = Math.max(this._min, Math.min(this._max, newValue));
18405
+ if (clamped !== this._value) {
18406
+ this._value = clamped;
18407
+ this._onChange?.(clamped);
18408
+ this.emit("change", { value: clamped });
18409
+ this.requestRender();
18410
+ }
18411
+ }
18412
+ get min() {
18413
+ return this._min;
18414
+ }
18415
+ set min(newMin) {
18416
+ if (newMin !== this._min) {
18417
+ this._min = newMin;
18418
+ if (this._value < newMin) {
18419
+ this.value = newMin;
18420
+ }
18421
+ this.requestRender();
18422
+ }
18423
+ }
18424
+ get max() {
18425
+ return this._max;
18426
+ }
18427
+ set max(newMax) {
18428
+ if (newMax !== this._max) {
18429
+ this._max = newMax;
18430
+ if (this._value > newMax) {
18431
+ this.value = newMax;
18432
+ }
18433
+ this.requestRender();
18434
+ }
18435
+ }
18436
+ set viewPortSize(size) {
18437
+ const clampedSize = Math.max(0.01, Math.min(size, this._max - this._min));
18438
+ if (clampedSize !== this._viewPortSize) {
18439
+ this._viewPortSize = clampedSize;
18440
+ this.requestRender();
18441
+ }
18442
+ }
18443
+ get viewPortSize() {
18444
+ return this._viewPortSize;
18445
+ }
18446
+ get backgroundColor() {
18447
+ return this._backgroundColor;
18448
+ }
18449
+ set backgroundColor(value) {
18450
+ this._backgroundColor = parseColor(value);
18451
+ this.requestRender();
18452
+ }
18453
+ get foregroundColor() {
18454
+ return this._foregroundColor;
18455
+ }
18456
+ set foregroundColor(value) {
18457
+ this._foregroundColor = parseColor(value);
18458
+ this.requestRender();
18459
+ }
18460
+ calculateDragOffsetVirtual(event) {
18461
+ const trackStart = this.orientation === "vertical" ? this.y : this.x;
18462
+ const mousePos = (this.orientation === "vertical" ? event.y : event.x) - trackStart;
18463
+ const virtualMousePos = Math.max(0, Math.min((this.orientation === "vertical" ? this.height : this.width) * 2, mousePos * 2));
18464
+ const virtualThumbStart = this.getVirtualThumbStart();
18465
+ const virtualThumbSize = this.getVirtualThumbSize();
18466
+ return Math.max(0, Math.min(virtualThumbSize, virtualMousePos - virtualThumbStart));
18467
+ }
18468
+ setupMouseHandling() {
18469
+ let isDragging = false;
18470
+ let dragOffsetVirtual = 0;
18471
+ this.onMouseDown = (event) => {
18472
+ event.stopPropagation();
18473
+ event.preventDefault();
18474
+ const thumb = this.getThumbRect();
18475
+ const inThumb = event.x >= thumb.x && event.x < thumb.x + thumb.width && event.y >= thumb.y && event.y < thumb.y + thumb.height;
18476
+ if (inThumb) {
18477
+ isDragging = true;
18478
+ dragOffsetVirtual = this.calculateDragOffsetVirtual(event);
18479
+ } else {
18480
+ this.updateValueFromMouseDirect(event);
18481
+ isDragging = true;
18482
+ dragOffsetVirtual = this.calculateDragOffsetVirtual(event);
18483
+ }
18484
+ };
18485
+ this.onMouseDrag = (event) => {
18486
+ if (!isDragging)
18487
+ return;
18488
+ event.stopPropagation();
18489
+ this.updateValueFromMouseWithOffset(event, dragOffsetVirtual);
18490
+ };
18491
+ this.onMouseUp = (event) => {
18492
+ if (isDragging) {
18493
+ this.updateValueFromMouseWithOffset(event, dragOffsetVirtual);
18494
+ }
18495
+ isDragging = false;
18496
+ };
18497
+ }
18498
+ updateValueFromMouseDirect(event) {
18499
+ const trackStart = this.orientation === "vertical" ? this.y : this.x;
18500
+ const trackSize = this.orientation === "vertical" ? this.height : this.width;
18501
+ const mousePos = this.orientation === "vertical" ? event.y : event.x;
18502
+ const relativeMousePos = mousePos - trackStart;
18503
+ const clampedMousePos = Math.max(0, Math.min(trackSize, relativeMousePos));
18504
+ const ratio = trackSize === 0 ? 0 : clampedMousePos / trackSize;
18505
+ const range = this._max - this._min;
18506
+ const newValue = this._min + ratio * range;
18507
+ this.value = newValue;
18508
+ }
18509
+ updateValueFromMouseWithOffset(event, offsetVirtual) {
18510
+ const trackStart = this.orientation === "vertical" ? this.y : this.x;
18511
+ const trackSize = this.orientation === "vertical" ? this.height : this.width;
18512
+ const mousePos = this.orientation === "vertical" ? event.y : event.x;
18513
+ const virtualTrackSize = trackSize * 2;
18514
+ const relativeMousePos = mousePos - trackStart;
18515
+ const clampedMousePos = Math.max(0, Math.min(trackSize, relativeMousePos));
18516
+ const virtualMousePos = clampedMousePos * 2;
18517
+ const virtualThumbSize = this.getVirtualThumbSize();
18518
+ const maxThumbStart = Math.max(0, virtualTrackSize - virtualThumbSize);
18519
+ let desiredThumbStart = virtualMousePos - offsetVirtual;
18520
+ desiredThumbStart = Math.max(0, Math.min(maxThumbStart, desiredThumbStart));
18521
+ const ratio = maxThumbStart === 0 ? 0 : desiredThumbStart / maxThumbStart;
18522
+ const range = this._max - this._min;
18523
+ const newValue = this._min + ratio * range;
18524
+ this.value = newValue;
18525
+ }
18526
+ getThumbRect() {
18527
+ const virtualThumbSize = this.getVirtualThumbSize();
18528
+ const virtualThumbStart = this.getVirtualThumbStart();
18529
+ const realThumbStart = Math.floor(virtualThumbStart / 2);
18530
+ const realThumbSize = Math.ceil((virtualThumbStart + virtualThumbSize) / 2) - realThumbStart;
18531
+ if (this.orientation === "vertical") {
18532
+ return {
18533
+ x: this.x,
18534
+ y: this.y + realThumbStart,
18535
+ width: this.width,
18536
+ height: Math.max(1, realThumbSize)
18537
+ };
18538
+ } else {
18539
+ return {
18540
+ x: this.x + realThumbStart,
18541
+ y: this.y,
18542
+ width: Math.max(1, realThumbSize),
18543
+ height: this.height
18544
+ };
18545
+ }
18546
+ }
18547
+ renderSelf(buffer) {
18548
+ if (this.orientation === "horizontal") {
18549
+ this.renderHorizontal(buffer);
18550
+ } else {
18551
+ this.renderVertical(buffer);
18552
+ }
18553
+ }
18554
+ renderHorizontal(buffer) {
18555
+ const virtualThumbSize = this.getVirtualThumbSize();
18556
+ const virtualThumbStart = this.getVirtualThumbStart();
18557
+ const virtualThumbEnd = virtualThumbStart + virtualThumbSize;
18558
+ buffer.fillRect(this.x, this.y, this.width, this.height, this._backgroundColor);
18559
+ const realStartCell = Math.floor(virtualThumbStart / 2);
18560
+ const realEndCell = Math.ceil(virtualThumbEnd / 2) - 1;
18561
+ const startX = Math.max(0, realStartCell);
18562
+ const endX = Math.min(this.width - 1, realEndCell);
18563
+ for (let realX = startX;realX <= endX; realX++) {
18564
+ const virtualCellStart = realX * 2;
18565
+ const virtualCellEnd = virtualCellStart + 2;
18566
+ const thumbStartInCell = Math.max(virtualThumbStart, virtualCellStart);
18567
+ const thumbEndInCell = Math.min(virtualThumbEnd, virtualCellEnd);
18568
+ const coverage = thumbEndInCell - thumbStartInCell;
18569
+ let char = " ";
18570
+ if (coverage >= 2) {
18571
+ char = "█";
18572
+ } else {
18573
+ const isLeftHalf = thumbStartInCell === virtualCellStart;
18574
+ if (isLeftHalf) {
18575
+ char = "▌";
18576
+ } else {
18577
+ char = "▐";
18578
+ }
18579
+ }
18580
+ for (let y = 0;y < this.height; y++) {
18581
+ buffer.setCellWithAlphaBlending(this.x + realX, this.y + y, char, this._foregroundColor, this._backgroundColor);
18582
+ }
18583
+ }
18584
+ }
18585
+ renderVertical(buffer) {
18586
+ const virtualThumbSize = this.getVirtualThumbSize();
18587
+ const virtualThumbStart = this.getVirtualThumbStart();
18588
+ const virtualThumbEnd = virtualThumbStart + virtualThumbSize;
18589
+ buffer.fillRect(this.x, this.y, this.width, this.height, this._backgroundColor);
18590
+ const realStartCell = Math.floor(virtualThumbStart / 2);
18591
+ const realEndCell = Math.ceil(virtualThumbEnd / 2) - 1;
18592
+ const startY = Math.max(0, realStartCell);
18593
+ const endY = Math.min(this.height - 1, realEndCell);
18594
+ for (let realY = startY;realY <= endY; realY++) {
18595
+ const virtualCellStart = realY * 2;
18596
+ const virtualCellEnd = virtualCellStart + 2;
18597
+ const thumbStartInCell = Math.max(virtualThumbStart, virtualCellStart);
18598
+ const thumbEndInCell = Math.min(virtualThumbEnd, virtualCellEnd);
18599
+ const coverage = thumbEndInCell - thumbStartInCell;
18600
+ let char = " ";
18601
+ if (coverage >= 2) {
18602
+ char = "█";
18603
+ } else if (coverage > 0) {
18604
+ const virtualPositionInCell = thumbStartInCell - virtualCellStart;
18605
+ if (virtualPositionInCell === 0) {
18606
+ char = "▀";
18607
+ } else {
18608
+ char = "▄";
18609
+ }
18610
+ }
18611
+ for (let x = 0;x < this.width; x++) {
18612
+ buffer.setCellWithAlphaBlending(this.x + x, this.y + realY, char, this._foregroundColor, this._backgroundColor);
18613
+ }
18614
+ }
18615
+ }
18616
+ getVirtualThumbSize() {
18617
+ const virtualTrackSize = this.orientation === "vertical" ? this.height * 2 : this.width * 2;
18618
+ const range = this._max - this._min;
18619
+ if (range === 0)
18620
+ return virtualTrackSize;
18621
+ const viewportSize = Math.max(1, this._viewPortSize);
18622
+ const contentSize = range + viewportSize;
18623
+ if (contentSize <= viewportSize)
18624
+ return virtualTrackSize;
18625
+ const thumbRatio = viewportSize / contentSize;
18626
+ const calculatedSize = Math.floor(virtualTrackSize * thumbRatio);
18627
+ return Math.max(1, Math.min(calculatedSize, virtualTrackSize));
18628
+ }
18629
+ getVirtualThumbStart() {
18630
+ const virtualTrackSize = this.orientation === "vertical" ? this.height * 2 : this.width * 2;
18631
+ const range = this._max - this._min;
18632
+ if (range === 0)
18633
+ return 0;
18634
+ const valueRatio = (this._value - this._min) / range;
18635
+ const virtualThumbSize = this.getVirtualThumbSize();
18636
+ return Math.round(valueRatio * (virtualTrackSize - virtualThumbSize));
18637
+ }
18638
+ };
18639
+ ScrollBarRenderable = class ScrollBarRenderable extends Renderable {
18640
+ slider;
18641
+ startArrow;
18642
+ endArrow;
18643
+ orientation;
18644
+ _focusable = true;
18645
+ _scrollSize = 0;
18646
+ _scrollPosition = 0;
18647
+ _viewportSize = 0;
18648
+ _showArrows = false;
18649
+ _manualVisibility = false;
18650
+ _onChange;
18651
+ scrollStep = null;
18652
+ get visible() {
18653
+ return super.visible;
18654
+ }
18655
+ set visible(value) {
18656
+ this._manualVisibility = true;
18657
+ super.visible = value;
18658
+ }
18659
+ resetVisibilityControl() {
18660
+ this._manualVisibility = false;
18661
+ this.recalculateVisibility();
18662
+ }
18663
+ get scrollSize() {
18664
+ return this._scrollSize;
18665
+ }
18666
+ get scrollPosition() {
18667
+ return this._scrollPosition;
18668
+ }
18669
+ get viewportSize() {
18670
+ return this._viewportSize;
18671
+ }
18672
+ set scrollSize(value) {
18673
+ if (value === this.scrollSize)
18674
+ return;
18675
+ this._scrollSize = value;
18676
+ this.recalculateVisibility();
18677
+ this.updateSliderFromScrollState();
18678
+ this.scrollPosition = this.scrollPosition;
18679
+ }
18680
+ set scrollPosition(value) {
18681
+ const newPosition = Math.round(Math.min(Math.max(0, value), this.scrollSize - this.viewportSize));
18682
+ if (newPosition !== this._scrollPosition) {
18683
+ this._scrollPosition = newPosition;
18684
+ this.updateSliderFromScrollState();
18685
+ }
18686
+ }
18687
+ set viewportSize(value) {
18688
+ if (value === this.viewportSize)
18689
+ return;
18690
+ this._viewportSize = value;
18691
+ this.slider.viewPortSize = Math.max(1, this._viewportSize);
18692
+ this.recalculateVisibility();
18693
+ this.updateSliderFromScrollState();
18694
+ this.scrollPosition = this.scrollPosition;
18695
+ }
18696
+ get showArrows() {
18697
+ return this._showArrows;
18698
+ }
18699
+ set showArrows(value) {
18700
+ if (value === this._showArrows)
18701
+ return;
18702
+ this._showArrows = value;
18703
+ this.startArrow.visible = value;
18704
+ this.endArrow.visible = value;
18705
+ }
18706
+ constructor(ctx, { trackOptions, arrowOptions, orientation, showArrows = false, ...options }) {
18707
+ super(ctx, {
18708
+ flexDirection: orientation === "vertical" ? "column" : "row",
18709
+ alignSelf: "stretch",
18710
+ alignItems: "stretch",
18711
+ ...options
18712
+ });
18713
+ this._onChange = options.onChange;
18714
+ this.orientation = orientation;
18715
+ this._showArrows = showArrows;
18716
+ const scrollRange = Math.max(0, this._scrollSize - this._viewportSize);
18717
+ const defaultStepSize = Math.max(1, this._viewportSize);
18718
+ const stepSize = trackOptions?.viewPortSize ?? defaultStepSize;
18719
+ this.slider = new SliderRenderable(ctx, {
18720
+ orientation,
18721
+ min: 0,
18722
+ max: scrollRange,
18723
+ value: this._scrollPosition,
18724
+ viewPortSize: stepSize,
18725
+ onChange: (value) => {
18726
+ this._scrollPosition = Math.round(value);
18727
+ this._onChange?.(this._scrollPosition);
18728
+ this.emit("change", { position: this._scrollPosition });
18729
+ },
18730
+ ...orientation === "vertical" ? {
18731
+ width: Math.max(1, Math.min(2, this.width)),
18732
+ height: "100%",
18733
+ marginLeft: "auto"
18734
+ } : {
18735
+ width: "100%",
18736
+ height: 1,
18737
+ marginTop: "auto"
18738
+ },
18739
+ flexGrow: 1,
18740
+ flexShrink: 1,
18741
+ ...trackOptions
18742
+ });
18743
+ this.updateSliderFromScrollState();
18744
+ const arrowOpts = arrowOptions ? {
18745
+ foregroundColor: arrowOptions.backgroundColor,
18746
+ backgroundColor: arrowOptions.backgroundColor,
18747
+ attributes: arrowOptions.attributes,
18748
+ ...arrowOptions
18749
+ } : {};
18750
+ this.startArrow = new ArrowRenderable(ctx, {
18751
+ alignSelf: "center",
18752
+ visible: this.showArrows,
18753
+ direction: this.orientation === "vertical" ? "up" : "left",
18754
+ height: this.orientation === "vertical" ? 1 : 1,
18755
+ ...arrowOpts
18756
+ });
18757
+ this.endArrow = new ArrowRenderable(ctx, {
18758
+ alignSelf: "center",
18759
+ visible: this.showArrows,
18760
+ direction: this.orientation === "vertical" ? "down" : "right",
18761
+ height: this.orientation === "vertical" ? 1 : 1,
18762
+ ...arrowOpts
18763
+ });
18764
+ this.add(this.startArrow);
18765
+ this.add(this.slider);
18766
+ this.add(this.endArrow);
18767
+ let startArrowMouseTimeout = undefined;
18768
+ let endArrowMouseTimeout = undefined;
18769
+ this.startArrow.onMouseDown = (event) => {
18770
+ event.stopPropagation();
18771
+ event.preventDefault();
18772
+ this.scrollBy(-0.5, "viewport");
18773
+ startArrowMouseTimeout = setTimeout(() => {
18774
+ this.scrollBy(-0.5, "viewport");
18775
+ startArrowMouseTimeout = setInterval(() => {
18776
+ this.scrollBy(-0.2, "viewport");
18777
+ }, 200);
18778
+ }, 500);
18779
+ };
18780
+ this.startArrow.onMouseUp = (event) => {
18781
+ event.stopPropagation();
18782
+ clearInterval(startArrowMouseTimeout);
18783
+ };
18784
+ this.endArrow.onMouseDown = (event) => {
18785
+ event.stopPropagation();
18786
+ event.preventDefault();
18787
+ this.scrollBy(0.5, "viewport");
18788
+ endArrowMouseTimeout = setTimeout(() => {
18789
+ this.scrollBy(0.5, "viewport");
18790
+ endArrowMouseTimeout = setInterval(() => {
18791
+ this.scrollBy(0.2, "viewport");
18792
+ }, 200);
18793
+ }, 500);
18794
+ };
18795
+ this.endArrow.onMouseUp = (event) => {
18796
+ event.stopPropagation();
18797
+ clearInterval(endArrowMouseTimeout);
18798
+ };
18799
+ }
18800
+ set arrowOptions(options) {
18801
+ Object.assign(this.startArrow, options);
18802
+ Object.assign(this.endArrow, options);
18803
+ this.requestRender();
18804
+ }
18805
+ set trackOptions(options) {
18806
+ Object.assign(this.slider, options);
18807
+ this.requestRender();
18808
+ }
18809
+ updateSliderFromScrollState() {
18810
+ const scrollRange = Math.max(0, this._scrollSize - this._viewportSize);
18811
+ this.slider.min = 0;
18812
+ this.slider.max = scrollRange;
18813
+ this.slider.value = Math.min(this._scrollPosition, scrollRange);
18814
+ }
18815
+ scrollBy(delta, unit = "absolute") {
18816
+ const multiplier = unit === "viewport" ? this.viewportSize : unit === "content" ? this.scrollSize : unit === "step" ? this.scrollStep ?? 1 : 1;
18817
+ const resolvedDelta = multiplier * delta;
18818
+ this.scrollPosition += resolvedDelta;
18819
+ }
18820
+ recalculateVisibility() {
18821
+ if (!this._manualVisibility) {
18822
+ const sizeRatio = this.scrollSize <= this.viewportSize ? 1 : this.viewportSize / this.scrollSize;
18823
+ super.visible = sizeRatio < 1;
18824
+ }
18825
+ }
18826
+ handleKeyPress(key) {
18827
+ switch (key.name) {
18828
+ case "left":
18829
+ case "h":
18830
+ if (this.orientation !== "horizontal")
18831
+ return false;
18832
+ this.scrollBy(-1 / 5, "viewport");
18833
+ return true;
18834
+ case "right":
18835
+ case "l":
18836
+ if (this.orientation !== "horizontal")
18837
+ return false;
18838
+ this.scrollBy(1 / 5, "viewport");
18839
+ return true;
18840
+ case "up":
18841
+ case "k":
18842
+ if (this.orientation !== "vertical")
18843
+ return false;
18844
+ this.scrollBy(-1 / 5, "viewport");
18845
+ return true;
18846
+ case "down":
18847
+ case "j":
18848
+ if (this.orientation !== "vertical")
18849
+ return false;
18850
+ this.scrollBy(1 / 5, "viewport");
18851
+ return true;
18852
+ case "pageup":
18853
+ this.scrollBy(-1 / 2, "viewport");
18854
+ return true;
18855
+ case "pagedown":
18856
+ this.scrollBy(1 / 2, "viewport");
18857
+ return true;
18858
+ case "home":
18859
+ this.scrollBy(-1, "content");
18860
+ return true;
18861
+ case "end":
18862
+ this.scrollBy(1, "content");
18863
+ return true;
18864
+ }
18865
+ return false;
18866
+ }
18867
+ };
18868
+ ArrowRenderable = class ArrowRenderable extends Renderable {
18869
+ _direction;
18870
+ _foregroundColor;
18871
+ _backgroundColor;
18872
+ _attributes;
18873
+ _arrowChars;
18874
+ constructor(ctx, options) {
18875
+ super(ctx, options);
18876
+ this._direction = options.direction;
18877
+ this._foregroundColor = options.foregroundColor ? parseColor(options.foregroundColor) : RGBA.fromValues(1, 1, 1, 1);
18878
+ this._backgroundColor = options.backgroundColor ? parseColor(options.backgroundColor) : RGBA.fromValues(0, 0, 0, 0);
18879
+ this._attributes = options.attributes ?? 0;
18880
+ this._arrowChars = {
18881
+ up: "▲",
18882
+ down: "▼",
18883
+ left: "◀",
18884
+ right: "▶",
18885
+ ...options.arrowChars
18886
+ };
18887
+ if (!options.width) {
18888
+ this.width = Bun.stringWidth(this.getArrowChar());
18889
+ }
18890
+ }
18891
+ get direction() {
18892
+ return this._direction;
18893
+ }
18894
+ set direction(value) {
18895
+ if (this._direction !== value) {
18896
+ this._direction = value;
18897
+ this.requestRender();
18898
+ }
18899
+ }
18900
+ get foregroundColor() {
18901
+ return this._foregroundColor;
18902
+ }
18903
+ set foregroundColor(value) {
18904
+ if (this._foregroundColor !== value) {
18905
+ this._foregroundColor = parseColor(value);
18906
+ this.requestRender();
18907
+ }
18908
+ }
18909
+ get backgroundColor() {
18910
+ return this._backgroundColor;
18911
+ }
18912
+ set backgroundColor(value) {
18913
+ if (this._backgroundColor !== value) {
18914
+ this._backgroundColor = parseColor(value);
18915
+ this.requestRender();
18916
+ }
18917
+ }
18918
+ get attributes() {
18919
+ return this._attributes;
18920
+ }
18921
+ set attributes(value) {
18922
+ if (this._attributes !== value) {
18923
+ this._attributes = value;
18924
+ this.requestRender();
18925
+ }
18926
+ }
18927
+ set arrowChars(value) {
18928
+ this._arrowChars = {
18929
+ ...this._arrowChars,
18930
+ ...value
18931
+ };
18932
+ this.requestRender();
18933
+ }
18934
+ renderSelf(buffer) {
18935
+ const char = this.getArrowChar();
18936
+ buffer.drawText(char, this.x, this.y, this._foregroundColor, this._backgroundColor, this._attributes);
18937
+ }
18938
+ getArrowChar() {
18939
+ switch (this._direction) {
18940
+ case "up":
18941
+ return this._arrowChars.up;
18942
+ case "down":
18943
+ return this._arrowChars.down;
18944
+ case "left":
18945
+ return this._arrowChars.left;
18946
+ case "right":
18947
+ return this._arrowChars.right;
18948
+ default:
18949
+ return "?";
18950
+ }
18951
+ }
18952
+ };
18953
+ ContentRenderable = class ContentRenderable extends BoxRenderable {
18954
+ viewport;
18955
+ _viewportCulling;
18956
+ constructor(ctx, viewport, viewportCulling, options) {
18957
+ super(ctx, options);
18958
+ this.viewport = viewport;
18959
+ this._viewportCulling = viewportCulling;
18960
+ }
18961
+ get viewportCulling() {
18962
+ return this._viewportCulling;
18963
+ }
18964
+ set viewportCulling(value) {
18965
+ this._viewportCulling = value;
18966
+ }
18967
+ _getVisibleChildren() {
18968
+ if (this._viewportCulling) {
18969
+ return getObjectsInViewport(this.viewport, this.getChildrenSortedByPrimaryAxis(), this.primaryAxis, 0).map((child) => child.num);
18970
+ }
18971
+ return this.getChildrenSortedByPrimaryAxis().map((child) => child.num);
18972
+ }
18973
+ };
18974
+ ScrollBoxRenderable = class ScrollBoxRenderable extends BoxRenderable {
18975
+ static idCounter = 0;
18976
+ internalId = 0;
18977
+ wrapper;
18978
+ viewport;
18979
+ content;
18980
+ horizontalScrollBar;
18981
+ verticalScrollBar;
18982
+ _focusable = true;
18983
+ selectionListener;
18984
+ autoScrollMouseX = 0;
18985
+ autoScrollMouseY = 0;
18986
+ autoScrollThresholdVertical = 3;
18987
+ autoScrollThresholdHorizontal = 3;
18988
+ autoScrollSpeedSlow = 6;
18989
+ autoScrollSpeedMedium = 36;
18990
+ autoScrollSpeedFast = 72;
18991
+ isAutoScrolling = false;
18992
+ cachedAutoScrollSpeed = 3;
18993
+ autoScrollAccumulatorX = 0;
18994
+ autoScrollAccumulatorY = 0;
18995
+ scrollAccumulatorX = 0;
18996
+ scrollAccumulatorY = 0;
18997
+ _stickyScroll;
18998
+ _stickyScrollTop = false;
18999
+ _stickyScrollBottom = false;
19000
+ _stickyScrollLeft = false;
19001
+ _stickyScrollRight = false;
19002
+ _stickyStart;
19003
+ _hasManualScroll = false;
19004
+ _isApplyingStickyScroll = false;
19005
+ scrollAccel;
19006
+ get stickyScroll() {
19007
+ return this._stickyScroll;
19008
+ }
19009
+ set stickyScroll(value) {
19010
+ this._stickyScroll = value;
19011
+ this.updateStickyState();
19012
+ }
19013
+ get stickyStart() {
19014
+ return this._stickyStart;
19015
+ }
19016
+ set stickyStart(value) {
19017
+ this._stickyStart = value;
19018
+ this.updateStickyState();
19019
+ }
19020
+ get scrollTop() {
19021
+ return this.verticalScrollBar.scrollPosition;
19022
+ }
19023
+ set scrollTop(value) {
19024
+ this.verticalScrollBar.scrollPosition = value;
19025
+ if (!this._isApplyingStickyScroll) {
19026
+ const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
19027
+ if (!this.isAtStickyPosition() && maxScrollTop > 1) {
19028
+ this._hasManualScroll = true;
19029
+ }
19030
+ }
19031
+ this.updateStickyState();
19032
+ }
19033
+ get scrollLeft() {
19034
+ return this.horizontalScrollBar.scrollPosition;
19035
+ }
19036
+ set scrollLeft(value) {
19037
+ this.horizontalScrollBar.scrollPosition = value;
19038
+ if (!this._isApplyingStickyScroll) {
19039
+ const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
19040
+ if (!this.isAtStickyPosition() && maxScrollLeft > 1) {
19041
+ this._hasManualScroll = true;
19042
+ }
19043
+ }
19044
+ this.updateStickyState();
19045
+ }
19046
+ get scrollWidth() {
19047
+ return this.horizontalScrollBar.scrollSize;
19048
+ }
19049
+ get scrollHeight() {
19050
+ return this.verticalScrollBar.scrollSize;
19051
+ }
19052
+ updateStickyState() {
19053
+ if (!this._stickyScroll)
19054
+ return;
19055
+ const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
19056
+ const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
19057
+ if (this.scrollTop <= 0) {
19058
+ this._stickyScrollTop = true;
19059
+ this._stickyScrollBottom = false;
19060
+ } else if (this.scrollTop >= maxScrollTop) {
19061
+ this._stickyScrollTop = false;
19062
+ this._stickyScrollBottom = true;
19063
+ } else {
19064
+ this._stickyScrollTop = false;
19065
+ this._stickyScrollBottom = false;
19066
+ }
19067
+ if (this.scrollLeft <= 0) {
19068
+ this._stickyScrollLeft = true;
19069
+ this._stickyScrollRight = false;
19070
+ } else if (this.scrollLeft >= maxScrollLeft) {
19071
+ this._stickyScrollLeft = false;
19072
+ this._stickyScrollRight = true;
19073
+ } else {
19074
+ this._stickyScrollLeft = false;
19075
+ this._stickyScrollRight = false;
19076
+ }
19077
+ }
19078
+ applyStickyStart(stickyStart) {
19079
+ this._isApplyingStickyScroll = true;
19080
+ switch (stickyStart) {
19081
+ case "top":
19082
+ this._stickyScrollTop = true;
19083
+ this._stickyScrollBottom = false;
19084
+ this.verticalScrollBar.scrollPosition = 0;
19085
+ break;
19086
+ case "bottom":
19087
+ this._stickyScrollTop = false;
19088
+ this._stickyScrollBottom = true;
19089
+ this.verticalScrollBar.scrollPosition = Math.max(0, this.scrollHeight - this.viewport.height);
19090
+ break;
19091
+ case "left":
19092
+ this._stickyScrollLeft = true;
19093
+ this._stickyScrollRight = false;
19094
+ this.horizontalScrollBar.scrollPosition = 0;
19095
+ break;
19096
+ case "right":
19097
+ this._stickyScrollLeft = false;
19098
+ this._stickyScrollRight = true;
19099
+ this.horizontalScrollBar.scrollPosition = Math.max(0, this.scrollWidth - this.viewport.width);
19100
+ break;
19101
+ }
19102
+ this._isApplyingStickyScroll = false;
19103
+ }
19104
+ constructor(ctx, {
19105
+ wrapperOptions,
19106
+ viewportOptions,
19107
+ contentOptions,
19108
+ rootOptions,
19109
+ scrollbarOptions,
19110
+ verticalScrollbarOptions,
19111
+ horizontalScrollbarOptions,
19112
+ stickyScroll = false,
19113
+ stickyStart,
19114
+ scrollX = false,
19115
+ scrollY = true,
19116
+ scrollAcceleration,
19117
+ viewportCulling = true,
19118
+ ...options
19119
+ }) {
19120
+ super(ctx, {
19121
+ flexDirection: "row",
19122
+ alignItems: "stretch",
19123
+ ...options,
19124
+ ...rootOptions
19125
+ });
19126
+ this.internalId = ScrollBoxRenderable.idCounter++;
19127
+ this._stickyScroll = stickyScroll;
19128
+ this._stickyStart = stickyStart;
19129
+ this.scrollAccel = scrollAcceleration ?? new LinearScrollAccel;
19130
+ this.wrapper = new BoxRenderable(ctx, {
19131
+ flexDirection: "column",
19132
+ flexGrow: 1,
19133
+ ...wrapperOptions,
19134
+ id: `scroll-box-wrapper-${this.internalId}`
19135
+ });
19136
+ super.add(this.wrapper);
19137
+ this.viewport = new BoxRenderable(ctx, {
19138
+ flexDirection: "column",
19139
+ flexGrow: 1,
19140
+ overflow: "hidden",
19141
+ onSizeChange: () => {
19142
+ this.recalculateBarProps();
19143
+ },
19144
+ ...viewportOptions,
19145
+ id: `scroll-box-viewport-${this.internalId}`
19146
+ });
19147
+ this.wrapper.add(this.viewport);
19148
+ this.content = new ContentRenderable(ctx, this.viewport, viewportCulling, {
19149
+ alignSelf: "flex-start",
19150
+ flexShrink: 0,
19151
+ ...scrollX ? { minWidth: "100%" } : { minWidth: "100%", maxWidth: "100%" },
19152
+ ...scrollY ? { minHeight: "100%" } : { minHeight: "100%", maxHeight: "100%" },
19153
+ onSizeChange: () => {
19154
+ this.recalculateBarProps();
19155
+ },
19156
+ ...contentOptions,
19157
+ id: `scroll-box-content-${this.internalId}`
19158
+ });
19159
+ this.viewport.add(this.content);
19160
+ this.verticalScrollBar = new ScrollBarRenderable(ctx, {
19161
+ ...scrollbarOptions,
19162
+ ...verticalScrollbarOptions,
19163
+ arrowOptions: {
19164
+ ...scrollbarOptions?.arrowOptions,
19165
+ ...verticalScrollbarOptions?.arrowOptions
19166
+ },
19167
+ id: `scroll-box-vertical-scrollbar-${this.internalId}`,
19168
+ orientation: "vertical",
19169
+ onChange: (position) => {
19170
+ this.content.translateY = -position;
19171
+ if (!this._isApplyingStickyScroll) {
19172
+ const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
19173
+ if (!this.isAtStickyPosition() && maxScrollTop > 1) {
19174
+ this._hasManualScroll = true;
19175
+ }
19176
+ }
19177
+ this.updateStickyState();
19178
+ }
19179
+ });
19180
+ super.add(this.verticalScrollBar);
19181
+ this.horizontalScrollBar = new ScrollBarRenderable(ctx, {
19182
+ ...scrollbarOptions,
19183
+ ...horizontalScrollbarOptions,
19184
+ arrowOptions: {
19185
+ ...scrollbarOptions?.arrowOptions,
19186
+ ...horizontalScrollbarOptions?.arrowOptions
19187
+ },
19188
+ id: `scroll-box-horizontal-scrollbar-${this.internalId}`,
19189
+ orientation: "horizontal",
19190
+ onChange: (position) => {
19191
+ this.content.translateX = -position;
19192
+ if (!this._isApplyingStickyScroll) {
19193
+ const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
19194
+ if (!this.isAtStickyPosition() && maxScrollLeft > 1) {
19195
+ this._hasManualScroll = true;
19196
+ }
19197
+ }
19198
+ this.updateStickyState();
19199
+ }
19200
+ });
19201
+ this.wrapper.add(this.horizontalScrollBar);
19202
+ this.recalculateBarProps();
19203
+ if (stickyStart && stickyScroll) {
19204
+ this.applyStickyStart(stickyStart);
19205
+ }
19206
+ this.selectionListener = () => {
19207
+ const selection = this._ctx.getSelection();
19208
+ if (!selection || !selection.isSelecting) {
19209
+ this.stopAutoScroll();
19210
+ }
19211
+ };
19212
+ this._ctx.on("selection", this.selectionListener);
19213
+ }
19214
+ onUpdate(deltaTime) {
19215
+ this.handleAutoScroll(deltaTime);
19216
+ }
19217
+ scrollBy(delta, unit = "absolute") {
19218
+ if (typeof delta === "number") {
19219
+ this.verticalScrollBar.scrollBy(delta, unit);
19220
+ } else {
19221
+ this.verticalScrollBar.scrollBy(delta.y, unit);
19222
+ this.horizontalScrollBar.scrollBy(delta.x, unit);
19223
+ }
19224
+ }
19225
+ scrollTo(position) {
19226
+ if (typeof position === "number") {
19227
+ this.scrollTop = position;
19228
+ } else {
19229
+ this.scrollTop = position.y;
19230
+ this.scrollLeft = position.x;
19231
+ }
19232
+ }
19233
+ isAtStickyPosition() {
19234
+ if (!this._stickyScroll || !this._stickyStart) {
19235
+ return false;
19236
+ }
19237
+ const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
19238
+ const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
19239
+ switch (this._stickyStart) {
19240
+ case "top":
19241
+ return this.scrollTop === 0;
19242
+ case "bottom":
19243
+ return this.scrollTop >= maxScrollTop;
19244
+ case "left":
19245
+ return this.scrollLeft === 0;
19246
+ case "right":
19247
+ return this.scrollLeft >= maxScrollLeft;
19248
+ default:
19249
+ return false;
19250
+ }
19251
+ }
19252
+ add(obj, index) {
19253
+ return this.content.add(obj, index);
19254
+ }
19255
+ insertBefore(obj, anchor) {
19256
+ return this.content.insertBefore(obj, anchor);
19257
+ }
19258
+ remove(id) {
19259
+ this.content.remove(id);
19260
+ }
19261
+ getChildren() {
19262
+ return this.content.getChildren();
19263
+ }
19264
+ onMouseEvent(event) {
19265
+ if (event.type === "scroll") {
19266
+ let dir = event.scroll?.direction;
19267
+ if (event.modifiers.shift)
19268
+ dir = dir === "up" ? "left" : dir === "down" ? "right" : dir === "right" ? "down" : "up";
19269
+ const baseDelta = event.scroll?.delta ?? 0;
19270
+ const now = Date.now();
19271
+ const multiplier = this.scrollAccel.tick(now);
19272
+ const scrollAmount = baseDelta * multiplier;
19273
+ if (dir === "up") {
19274
+ this.scrollAccumulatorY -= scrollAmount;
19275
+ const integerScroll = Math.trunc(this.scrollAccumulatorY);
19276
+ if (integerScroll !== 0) {
19277
+ this.scrollTop += integerScroll;
19278
+ this.scrollAccumulatorY -= integerScroll;
19279
+ }
19280
+ } else if (dir === "down") {
19281
+ this.scrollAccumulatorY += scrollAmount;
19282
+ const integerScroll = Math.trunc(this.scrollAccumulatorY);
19283
+ if (integerScroll !== 0) {
19284
+ this.scrollTop += integerScroll;
19285
+ this.scrollAccumulatorY -= integerScroll;
19286
+ }
19287
+ } else if (dir === "left") {
19288
+ this.scrollAccumulatorX -= scrollAmount;
19289
+ const integerScroll = Math.trunc(this.scrollAccumulatorX);
19290
+ if (integerScroll !== 0) {
19291
+ this.scrollLeft += integerScroll;
19292
+ this.scrollAccumulatorX -= integerScroll;
19293
+ }
19294
+ } else if (dir === "right") {
19295
+ this.scrollAccumulatorX += scrollAmount;
19296
+ const integerScroll = Math.trunc(this.scrollAccumulatorX);
19297
+ if (integerScroll !== 0) {
19298
+ this.scrollLeft += integerScroll;
19299
+ this.scrollAccumulatorX -= integerScroll;
19300
+ }
19301
+ }
19302
+ const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
19303
+ const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
19304
+ if (maxScrollTop > 1 || maxScrollLeft > 1) {
19305
+ this._hasManualScroll = true;
19306
+ }
19307
+ }
19308
+ if (event.type === "drag" && event.isSelecting) {
19309
+ this.updateAutoScroll(event.x, event.y);
19310
+ } else if (event.type === "up") {
19311
+ this.stopAutoScroll();
19312
+ }
19313
+ }
19314
+ handleKeyPress(key) {
19315
+ if (this.verticalScrollBar.handleKeyPress(key)) {
19316
+ this._hasManualScroll = true;
19317
+ this.scrollAccel.reset();
19318
+ this.resetScrollAccumulators();
19319
+ return true;
19320
+ }
19321
+ if (this.horizontalScrollBar.handleKeyPress(key)) {
19322
+ this._hasManualScroll = true;
19323
+ this.scrollAccel.reset();
19324
+ this.resetScrollAccumulators();
19325
+ return true;
19326
+ }
19327
+ return false;
19328
+ }
19329
+ resetScrollAccumulators() {
19330
+ this.scrollAccumulatorX = 0;
19331
+ this.scrollAccumulatorY = 0;
19332
+ }
19333
+ startAutoScroll(mouseX, mouseY) {
19334
+ this.stopAutoScroll();
19335
+ this.autoScrollMouseX = mouseX;
19336
+ this.autoScrollMouseY = mouseY;
19337
+ this.cachedAutoScrollSpeed = this.getAutoScrollSpeed(mouseX, mouseY);
19338
+ this.isAutoScrolling = true;
19339
+ if (!this.live) {
19340
+ this.live = true;
19341
+ }
19342
+ }
19343
+ updateAutoScroll(mouseX, mouseY) {
19344
+ this.autoScrollMouseX = mouseX;
19345
+ this.autoScrollMouseY = mouseY;
19346
+ this.cachedAutoScrollSpeed = this.getAutoScrollSpeed(mouseX, mouseY);
19347
+ const scrollX = this.getAutoScrollDirectionX(mouseX);
19348
+ const scrollY = this.getAutoScrollDirectionY(mouseY);
19349
+ if (scrollX === 0 && scrollY === 0) {
19350
+ this.stopAutoScroll();
19351
+ } else if (!this.isAutoScrolling) {
19352
+ this.startAutoScroll(mouseX, mouseY);
19353
+ }
19354
+ }
19355
+ stopAutoScroll() {
19356
+ const wasAutoScrolling = this.isAutoScrolling;
19357
+ this.isAutoScrolling = false;
19358
+ this.autoScrollAccumulatorX = 0;
19359
+ this.autoScrollAccumulatorY = 0;
19360
+ if (wasAutoScrolling && !this.hasOtherLiveReasons()) {
19361
+ this.live = false;
19362
+ }
19363
+ }
19364
+ hasOtherLiveReasons() {
19365
+ return false;
19366
+ }
19367
+ handleAutoScroll(deltaTime) {
19368
+ if (!this.isAutoScrolling)
19369
+ return;
19370
+ const scrollX = this.getAutoScrollDirectionX(this.autoScrollMouseX);
19371
+ const scrollY = this.getAutoScrollDirectionY(this.autoScrollMouseY);
19372
+ const scrollAmount = this.cachedAutoScrollSpeed * (deltaTime / 1000);
19373
+ let scrolled = false;
19374
+ if (scrollX !== 0) {
19375
+ this.autoScrollAccumulatorX += scrollX * scrollAmount;
19376
+ const integerScrollX = Math.trunc(this.autoScrollAccumulatorX);
19377
+ if (integerScrollX !== 0) {
19378
+ this.scrollLeft += integerScrollX;
19379
+ this.autoScrollAccumulatorX -= integerScrollX;
19380
+ scrolled = true;
19381
+ }
19382
+ }
19383
+ if (scrollY !== 0) {
19384
+ this.autoScrollAccumulatorY += scrollY * scrollAmount;
19385
+ const integerScrollY = Math.trunc(this.autoScrollAccumulatorY);
19386
+ if (integerScrollY !== 0) {
19387
+ this.scrollTop += integerScrollY;
19388
+ this.autoScrollAccumulatorY -= integerScrollY;
19389
+ scrolled = true;
19390
+ }
19391
+ }
19392
+ if (scrolled) {
19393
+ this._ctx.requestSelectionUpdate();
19394
+ }
19395
+ if (scrollX === 0 && scrollY === 0) {
19396
+ this.stopAutoScroll();
19397
+ }
19398
+ }
19399
+ getAutoScrollDirectionX(mouseX) {
19400
+ const relativeX = mouseX - this.x;
19401
+ const distToLeft = relativeX;
19402
+ const distToRight = this.width - relativeX;
19403
+ if (distToLeft <= this.autoScrollThresholdHorizontal) {
19404
+ return this.scrollLeft > 0 ? -1 : 0;
19405
+ } else if (distToRight <= this.autoScrollThresholdHorizontal) {
19406
+ const maxScrollLeft = this.scrollWidth - this.viewport.width;
19407
+ return this.scrollLeft < maxScrollLeft ? 1 : 0;
19408
+ }
19409
+ return 0;
19410
+ }
19411
+ getAutoScrollDirectionY(mouseY) {
19412
+ const relativeY = mouseY - this.y;
19413
+ const distToTop = relativeY;
19414
+ const distToBottom = this.height - relativeY;
19415
+ if (distToTop <= this.autoScrollThresholdVertical) {
19416
+ return this.scrollTop > 0 ? -1 : 0;
19417
+ } else if (distToBottom <= this.autoScrollThresholdVertical) {
19418
+ const maxScrollTop = this.scrollHeight - this.viewport.height;
19419
+ return this.scrollTop < maxScrollTop ? 1 : 0;
19420
+ }
19421
+ return 0;
19422
+ }
19423
+ getAutoScrollSpeed(mouseX, mouseY) {
19424
+ const relativeX = mouseX - this.x;
19425
+ const relativeY = mouseY - this.y;
19426
+ const distToLeft = relativeX;
19427
+ const distToRight = this.width - relativeX;
19428
+ const distToTop = relativeY;
19429
+ const distToBottom = this.height - relativeY;
19430
+ const minDistance = Math.min(distToLeft, distToRight, distToTop, distToBottom);
19431
+ if (minDistance <= 1) {
19432
+ return this.autoScrollSpeedFast;
19433
+ } else if (minDistance <= 2) {
19434
+ return this.autoScrollSpeedMedium;
19435
+ } else {
19436
+ return this.autoScrollSpeedSlow;
19437
+ }
19438
+ }
19439
+ recalculateBarProps() {
19440
+ const wasApplyingStickyScroll = this._isApplyingStickyScroll;
19441
+ this._isApplyingStickyScroll = true;
19442
+ this.verticalScrollBar.scrollSize = this.content.height;
19443
+ this.verticalScrollBar.viewportSize = this.viewport.height;
19444
+ this.horizontalScrollBar.scrollSize = this.content.width;
19445
+ this.horizontalScrollBar.viewportSize = this.viewport.width;
19446
+ if (this._stickyScroll) {
19447
+ const newMaxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
19448
+ const newMaxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
19449
+ if (this._stickyStart && !this._hasManualScroll) {
19450
+ this.applyStickyStart(this._stickyStart);
19451
+ } else {
19452
+ if (this._stickyScrollTop) {
19453
+ this.scrollTop = 0;
19454
+ } else if (this._stickyScrollBottom && newMaxScrollTop > 0) {
19455
+ this.scrollTop = newMaxScrollTop;
19456
+ }
19457
+ if (this._stickyScrollLeft) {
19458
+ this.scrollLeft = 0;
19459
+ } else if (this._stickyScrollRight && newMaxScrollLeft > 0) {
19460
+ this.scrollLeft = newMaxScrollLeft;
19461
+ }
19462
+ }
19463
+ }
19464
+ this._isApplyingStickyScroll = wasApplyingStickyScroll;
19465
+ process.nextTick(() => {
19466
+ this.requestRender();
19467
+ });
19468
+ }
19469
+ set rootOptions(options) {
19470
+ Object.assign(this, options);
19471
+ this.requestRender();
19472
+ }
19473
+ set wrapperOptions(options) {
19474
+ Object.assign(this.wrapper, options);
19475
+ this.requestRender();
19476
+ }
19477
+ set viewportOptions(options) {
19478
+ Object.assign(this.viewport, options);
19479
+ this.requestRender();
19480
+ }
19481
+ set contentOptions(options) {
19482
+ Object.assign(this.content, options);
19483
+ this.requestRender();
19484
+ }
19485
+ set scrollbarOptions(options) {
19486
+ Object.assign(this.verticalScrollBar, options);
19487
+ Object.assign(this.horizontalScrollBar, options);
19488
+ this.requestRender();
19489
+ }
19490
+ set verticalScrollbarOptions(options) {
19491
+ Object.assign(this.verticalScrollBar, options);
19492
+ this.requestRender();
19493
+ }
19494
+ set horizontalScrollbarOptions(options) {
19495
+ Object.assign(this.horizontalScrollBar, options);
19496
+ this.requestRender();
19497
+ }
19498
+ get scrollAcceleration() {
19499
+ return this.scrollAccel;
19500
+ }
19501
+ set scrollAcceleration(value) {
19502
+ this.scrollAccel = value;
19503
+ }
19504
+ get viewportCulling() {
19505
+ return this.content.viewportCulling;
19506
+ }
19507
+ set viewportCulling(value) {
19508
+ this.content.viewportCulling = value;
19509
+ this.requestRender();
19510
+ }
19511
+ destroySelf() {
19512
+ if (this.selectionListener) {
19513
+ this._ctx.off("selection", this.selectionListener);
19514
+ this.selectionListener = undefined;
19515
+ }
19516
+ super.destroySelf();
19517
+ }
19518
+ };
18372
19519
  defaultSelectKeybindings = [
18373
19520
  { name: "up", action: "move-up" },
18374
19521
  { name: "k", action: "move-up" },
@@ -19969,7 +21116,7 @@ var init_config = __esm(() => {
19969
21116
  provider: "opencode-zen",
19970
21117
  openrouterApiKey: "",
19971
21118
  opencodeZenApiKey: "",
19972
- defaultModel: "gpt-5-nano",
21119
+ defaultModel: "gemini-3-flash",
19973
21120
  safetyLevel: "moderate",
19974
21121
  dryRunByDefault: false,
19975
21122
  blockedCommands: [
@@ -19980,7 +21127,8 @@ var init_config = __esm(() => {
19980
21127
  "chmod -R 777 /",
19981
21128
  "chown -R"
19982
21129
  ],
19983
- confirmedDangerousPatterns: []
21130
+ confirmedDangerousPatterns: [],
21131
+ repoContext: false
19984
21132
  };
19985
21133
  });
19986
21134
 
@@ -20325,6 +21473,112 @@ function getPlatformPaths(platform) {
20325
21473
  }
20326
21474
  var init_shell = () => {};
20327
21475
 
21476
+ // src/lib/repo-context.ts
21477
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
21478
+ import { join as join3 } from "path";
21479
+ function detectRepoContext(cwd) {
21480
+ const context = {
21481
+ type: "unknown"
21482
+ };
21483
+ let detected = false;
21484
+ if (existsSync6(join3(cwd, ".git"))) {
21485
+ context.hasGit = true;
21486
+ detected = true;
21487
+ }
21488
+ if (existsSync6(join3(cwd, "Dockerfile")) || existsSync6(join3(cwd, "docker-compose.yml")) || existsSync6(join3(cwd, "docker-compose.yaml"))) {
21489
+ context.hasDocker = true;
21490
+ detected = true;
21491
+ }
21492
+ const packageJsonPath = join3(cwd, "package.json");
21493
+ if (existsSync6(packageJsonPath)) {
21494
+ detected = true;
21495
+ context.type = "node";
21496
+ try {
21497
+ const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
21498
+ if (existsSync6(join3(cwd, "bun.lockb")) || existsSync6(join3(cwd, "bun.lock"))) {
21499
+ context.packageManager = "bun";
21500
+ } else if (existsSync6(join3(cwd, "pnpm-lock.yaml"))) {
21501
+ context.packageManager = "pnpm";
21502
+ } else if (existsSync6(join3(cwd, "yarn.lock"))) {
21503
+ context.packageManager = "yarn";
21504
+ } else if (existsSync6(join3(cwd, "package-lock.json"))) {
21505
+ context.packageManager = "npm";
21506
+ } else if (packageJson.packageManager) {
21507
+ const pm = packageJson.packageManager.split("@")[0];
21508
+ context.packageManager = pm;
21509
+ }
21510
+ if (packageJson.scripts && typeof packageJson.scripts === "object") {
21511
+ context.scripts = Object.keys(packageJson.scripts);
21512
+ }
21513
+ } catch {}
21514
+ }
21515
+ const makefilePath = join3(cwd, "Makefile");
21516
+ if (existsSync6(makefilePath)) {
21517
+ detected = true;
21518
+ if (context.type === "unknown")
21519
+ context.type = "make";
21520
+ try {
21521
+ const makefile = readFileSync2(makefilePath, "utf-8");
21522
+ const targetRegex = /^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:/gm;
21523
+ const targets = [];
21524
+ let match;
21525
+ while ((match = targetRegex.exec(makefile)) !== null) {
21526
+ if (!match[1].startsWith(".") && !match[1].startsWith("_")) {
21527
+ targets.push(match[1]);
21528
+ }
21529
+ }
21530
+ if (targets.length > 0) {
21531
+ context.makeTargets = [...new Set(targets)];
21532
+ }
21533
+ } catch {}
21534
+ }
21535
+ if (existsSync6(join3(cwd, "Cargo.toml"))) {
21536
+ detected = true;
21537
+ context.type = "rust";
21538
+ context.cargoCommands = ["build", "run", "test", "check", "clippy", "fmt", "doc"];
21539
+ }
21540
+ if (existsSync6(join3(cwd, "pyproject.toml")) || existsSync6(join3(cwd, "setup.py")) || existsSync6(join3(cwd, "requirements.txt"))) {
21541
+ detected = true;
21542
+ if (context.type === "unknown")
21543
+ context.type = "python";
21544
+ }
21545
+ if (existsSync6(join3(cwd, "go.mod"))) {
21546
+ detected = true;
21547
+ if (context.type === "unknown")
21548
+ context.type = "go";
21549
+ }
21550
+ return detected ? context : null;
21551
+ }
21552
+ function formatRepoContext(context) {
21553
+ const lines = [];
21554
+ lines.push(`Project type: ${context.type}`);
21555
+ if (context.packageManager) {
21556
+ lines.push(`Package manager: ${context.packageManager}`);
21557
+ }
21558
+ if (context.scripts && context.scripts.length > 0) {
21559
+ const displayScripts = context.scripts.slice(0, 15);
21560
+ const suffix = context.scripts.length > 15 ? ` (+${context.scripts.length - 15} more)` : "";
21561
+ lines.push(`Available scripts: ${displayScripts.join(", ")}${suffix}`);
21562
+ }
21563
+ if (context.makeTargets && context.makeTargets.length > 0) {
21564
+ const displayTargets = context.makeTargets.slice(0, 15);
21565
+ const suffix = context.makeTargets.length > 15 ? ` (+${context.makeTargets.length - 15} more)` : "";
21566
+ lines.push(`Make targets: ${displayTargets.join(", ")}${suffix}`);
21567
+ }
21568
+ if (context.cargoCommands) {
21569
+ lines.push(`Cargo commands: ${context.cargoCommands.join(", ")}`);
21570
+ }
21571
+ if (context.hasDocker) {
21572
+ lines.push(`Docker: available`);
21573
+ }
21574
+ if (context.hasGit) {
21575
+ lines.push(`Git: initialized`);
21576
+ }
21577
+ return lines.join(`
21578
+ `);
21579
+ }
21580
+ var init_repo_context = () => {};
21581
+
20328
21582
  // src/lib/api.ts
20329
21583
  function getZenApiType(modelId) {
20330
21584
  if (modelId.startsWith("gpt-")) {
@@ -20338,11 +21592,21 @@ function getZenApiType(modelId) {
20338
21592
  }
20339
21593
  return "openai-compatible";
20340
21594
  }
20341
- function buildSystemPrompt(cwd, history, shellInfo) {
21595
+ function buildSystemPrompt(cwd, history, shellInfo, repoContextEnabled) {
20342
21596
  const historyContext = formatHistory(history);
20343
21597
  const platformPaths = getPlatformPaths(shellInfo.platform);
20344
21598
  const shellHints = getShellSyntaxHints(shellInfo.shell);
20345
21599
  const platformName = shellInfo.platform === "macos" ? "macOS" : shellInfo.platform === "windows" ? "Windows" : shellInfo.platform === "linux" ? shellInfo.isWSL ? "Linux (WSL)" : "Linux" : "Unknown";
21600
+ let projectContextSection = "";
21601
+ if (repoContextEnabled) {
21602
+ const repoContext = detectRepoContext(cwd);
21603
+ if (repoContext) {
21604
+ projectContextSection = `
21605
+ Project context:
21606
+ ${formatRepoContext(repoContext)}
21607
+ `;
21608
+ }
21609
+ }
20346
21610
  return `You are a shell command translator. Convert the user's natural language request into a shell command.
20347
21611
 
20348
21612
  Current environment:
@@ -20351,7 +21615,7 @@ Current environment:
20351
21615
  - Working directory: ${cwd}
20352
21616
  - Home directory: ${shellInfo.homeDir}
20353
21617
  ${shellInfo.terminalEmulator ? `- Terminal: ${shellInfo.terminalEmulator}` : ""}
20354
-
21618
+ ${projectContextSection}
20355
21619
  ${shellHints}
20356
21620
 
20357
21621
  Recent command history:
@@ -20362,7 +21626,8 @@ Rules:
20362
21626
  - No explanations, no markdown, no backticks, no code blocks
20363
21627
  - Use the correct syntax for the detected shell (${shellInfo.shell})
20364
21628
  - If the request is unclear, make a reasonable assumption
20365
- - Prefer simple, common commands over complex one-liners
21629
+ - Prefer simple, common commands over complex one-liners${repoContextEnabled ? `
21630
+ - Use project-specific commands when relevant (e.g., use the detected package manager and available scripts)` : ""}
20366
21631
  - Use the command history for context (e.g., "do that again", "undo", "delete the file I just created")
20367
21632
  - If the user asks something that can't be done with a shell command, output a command that prints a helpful message
20368
21633
  - For file operations, prefer safer alternatives when possible
@@ -20634,9 +21899,9 @@ function getShellInfo() {
20634
21899
  }
20635
21900
  return cachedShellInfo;
20636
21901
  }
20637
- async function translateToCommand(apiKey, model, userInput, cwd, history = []) {
21902
+ async function translateToCommand(apiKey, model, userInput, cwd, history = [], repoContextEnabled) {
20638
21903
  const shellInfo = getShellInfo();
20639
- const systemPrompt = buildSystemPrompt(cwd, history, shellInfo);
21904
+ const systemPrompt = buildSystemPrompt(cwd, history, shellInfo, repoContextEnabled);
20640
21905
  let rawCommand;
20641
21906
  if (model.provider === "openrouter") {
20642
21907
  rawCommand = await callOpenRouter(apiKey, model.id, systemPrompt, userInput);
@@ -20662,6 +21927,7 @@ async function translateToCommand(apiKey, model, userInput, cwd, history = []) {
20662
21927
  var DEBUG_API, cachedShellInfo = null;
20663
21928
  var init_api = __esm(() => {
20664
21929
  init_shell();
21930
+ init_repo_context();
20665
21931
  DEBUG_API = process.env.DEBUG_API === "1";
20666
21932
  });
20667
21933
 
@@ -20877,21 +22143,21 @@ var config;
20877
22143
  var history = [];
20878
22144
  var currentCwd = getCwd();
20879
22145
  var dryRunMode = false;
22146
+ var chatMessages = [];
22147
+ var messageIdCounter = 0;
20880
22148
  var mainContainer;
20881
22149
  var headerText;
20882
- var cwdText;
20883
- var modelText;
22150
+ var statusBarText;
22151
+ var chatScrollBox;
20884
22152
  var inputField;
20885
- var outputContainer;
20886
- var outputText;
20887
- var statusText;
20888
- var commandPreview;
20889
- var safetyWarning;
20890
- var confirmPrompt;
22153
+ var helpBarText;
20891
22154
  var modelSelector = null;
20892
22155
  var providerSelector = null;
20893
- var pendingCommand = null;
22156
+ var pendingMessageId = null;
20894
22157
  var awaitingConfirmation = false;
22158
+ function generateMessageId() {
22159
+ return `msg-${++messageIdCounter}`;
22160
+ }
20895
22161
  async function main2() {
20896
22162
  config = loadConfig();
20897
22163
  history = loadHistory();
@@ -21069,44 +22335,65 @@ function createMainUI() {
21069
22335
  id: "header-row",
21070
22336
  flexDirection: "row",
21071
22337
  width: "100%",
22338
+ alignItems: "center",
21072
22339
  marginBottom: 1
21073
22340
  });
21074
22341
  mainContainer.add(headerRow);
21075
22342
  headerText = new TextRenderable(renderer, {
21076
22343
  id: "header-text",
21077
- content: t`${bold(fg(theme.colors.primary)("magic-shell"))} ${fg(theme.colors.textMuted)("- natural language to terminal commands")}`,
22344
+ content: t`${bold(fg(theme.colors.primary)("magic-shell"))}`,
21078
22345
  flexGrow: 1
21079
22346
  });
21080
22347
  headerRow.add(headerText);
21081
- const statusRow = new BoxRenderable(renderer, {
21082
- id: "status-row",
21083
- flexDirection: "row",
21084
- width: "100%",
21085
- marginBottom: 1
22348
+ const modelBadge = new TextRenderable(renderer, {
22349
+ id: "model-badge",
22350
+ content: getModelDisplay()
21086
22351
  });
21087
- mainContainer.add(statusRow);
21088
- cwdText = new TextRenderable(renderer, {
21089
- id: "cwd-text",
21090
- content: t`${fg(theme.colors.textMuted)("cwd:")} ${fg(theme.colors.success)(currentCwd)}`,
21091
- flexGrow: 1
22352
+ headerRow.add(modelBadge);
22353
+ statusBarText = new TextRenderable(renderer, {
22354
+ id: "status-bar-text",
22355
+ content: getStatusBarContent(),
22356
+ marginBottom: 1
21092
22357
  });
21093
- statusRow.add(cwdText);
21094
- modelText = new TextRenderable(renderer, {
21095
- id: "model-text",
21096
- content: getModelDisplay()
22358
+ mainContainer.add(statusBarText);
22359
+ chatScrollBox = new ScrollBoxRenderable(renderer, {
22360
+ id: "chat-scroll-box",
22361
+ flexGrow: 1,
22362
+ width: "100%",
22363
+ scrollY: true,
22364
+ scrollX: false,
22365
+ stickyScroll: true,
22366
+ stickyStart: "bottom",
22367
+ rootOptions: {
22368
+ border: true,
22369
+ borderColor: theme.colors.border,
22370
+ borderStyle: "single"
22371
+ },
22372
+ viewportOptions: {
22373
+ backgroundColor: theme.colors.background,
22374
+ paddingLeft: 1,
22375
+ paddingRight: 1,
22376
+ paddingTop: 1
22377
+ },
22378
+ contentOptions: {
22379
+ flexDirection: "column",
22380
+ gap: 1
22381
+ }
21097
22382
  });
21098
- statusRow.add(modelText);
22383
+ mainContainer.add(chatScrollBox);
22384
+ addSystemMessage(getWelcomeMessage());
21099
22385
  const inputRow = new BoxRenderable(renderer, {
21100
22386
  id: "input-row",
21101
22387
  flexDirection: "row",
21102
22388
  width: "100%",
21103
- marginBottom: 1
22389
+ marginTop: 1,
22390
+ alignItems: "center"
21104
22391
  });
21105
22392
  mainContainer.add(inputRow);
21106
22393
  const promptText = new TextRenderable(renderer, {
21107
22394
  id: "prompt-text",
21108
- content: t`${fg(theme.colors.success)(">")} `,
21109
- width: 2
22395
+ content: t`${fg(theme.colors.primary)("~>")} `,
22396
+ width: 3
21110
22397
  });
21111
22398
  inputRow.add(promptText);
21112
22399
  inputField = new InputRenderable(renderer, {
@@ -21124,58 +22411,269 @@ function createMainUI() {
21124
22411
  }
21125
22412
  });
21126
22413
  inputRow.add(inputField);
21127
- commandPreview = new TextRenderable(renderer, {
21128
- id: "command-preview",
21129
- content: "",
21130
- marginBottom: 1
21131
- });
21132
- mainContainer.add(commandPreview);
21133
- safetyWarning = new TextRenderable(renderer, {
21134
- id: "safety-warning",
21135
- content: ""
22414
+ helpBarText = new TextRenderable(renderer, {
22415
+ id: "help-bar-text",
22416
+ content: getHelpBarContent(),
22417
+ marginTop: 1
21136
22418
  });
21137
- mainContainer.add(safetyWarning);
21138
- confirmPrompt = new BoxRenderable(renderer, {
21139
- id: "confirm-prompt",
22419
+ mainContainer.add(helpBarText);
22420
+ inputField.on(InputRenderableEvents.ENTER, handleInput);
22421
+ renderer.keyInput.on("keypress", handleKeypress);
22422
+ inputField.focus();
22423
+ }
22424
+ function getStatusBarContent() {
22425
+ const theme = getTheme();
22426
+ const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
22427
+ const safeModeIndicator = dryRunMode ? fg(theme.colors.warning)("[DRY RUN]") : fg(theme.colors.success)("Safe");
22428
+ const repoContextIndicator = config.repoContext ? fg(theme.colors.info)("[Repo]") : "";
22429
+ return t`${fg(theme.colors.textMuted)("Provider:")} ${fg(theme.colors.text)(providerName)} ${fg(theme.colors.textMuted)("Model:")} ${fg(theme.colors.text)(currentModel.name)} ${safeModeIndicator}${repoContextIndicator ? " " : ""}${repoContextIndicator}`;
22430
+ }
22431
+ function getHelpBarContent() {
22432
+ const theme = getTheme();
22433
+ if (awaitingConfirmation) {
22434
+ return t`${fg(theme.colors.warning)("[Enter] Run")} ${fg(theme.colors.textMuted)("|")} ${fg(theme.colors.error)("[Esc] Cancel")} ${fg(theme.colors.textMuted)("|")} ${fg(theme.colors.primary)("[e] Edit")}`;
22435
+ }
22436
+ return t`${fg(theme.colors.textMuted)("Ctrl+X")} ${fg(theme.colors.primary)("P")}${fg(theme.colors.textMuted)(" Palette")} ${fg(theme.colors.primary)("M")}${fg(theme.colors.textMuted)(" Model")} ${fg(theme.colors.primary)("T")}${fg(theme.colors.textMuted)(" Theme")} ${fg(theme.colors.primary)("D")}${fg(theme.colors.textMuted)(" Dry-run")} ${fg(theme.colors.primary)("?")}${fg(theme.colors.textMuted)(" Help")}`;
22437
+ }
22438
+ function getWelcomeMessage() {
22439
+ const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
22440
+ const freeNote = config.provider === "opencode-zen" ? `
22441
+ Free models: grok-code, glm-4.7-free` : "";
22442
+ return `Ready. Using ${providerName}.${freeNote}
22443
+ Type what you want to do, or press Ctrl+X P for command palette.`;
22444
+ }
22445
+ function addSystemMessage(content) {
22446
+ const msg = {
22447
+ id: generateMessageId(),
22448
+ type: "system",
22449
+ content,
22450
+ timestamp: Date.now()
22451
+ };
22452
+ chatMessages.push(msg);
22453
+ renderMessage(msg);
22454
+ return msg;
22455
+ }
22456
+ function addUserMessage(content) {
22457
+ const msg = {
22458
+ id: generateMessageId(),
22459
+ type: "user",
22460
+ content,
22461
+ timestamp: Date.now()
22462
+ };
22463
+ chatMessages.push(msg);
22464
+ renderMessage(msg);
22465
+ return msg;
22466
+ }
22467
+ function addAssistantMessage(content, command, safety) {
22468
+ const msg = {
22469
+ id: generateMessageId(),
22470
+ type: "assistant",
22471
+ content,
22472
+ command,
22473
+ safety,
22474
+ timestamp: Date.now(),
22475
+ executed: false
22476
+ };
22477
+ chatMessages.push(msg);
22478
+ renderMessage(msg);
22479
+ return msg;
22480
+ }
22481
+ function addResultMessage(content, exitCode) {
22482
+ const msg = {
22483
+ id: generateMessageId(),
22484
+ type: "result",
22485
+ content,
22486
+ timestamp: Date.now(),
22487
+ exitCode
22488
+ };
22489
+ chatMessages.push(msg);
22490
+ renderMessage(msg);
22491
+ return msg;
22492
+ }
22493
+ function renderMessage(msg) {
22494
+ const theme = getTheme();
22495
+ const msgBox = createMessageRenderable(msg, theme);
22496
+ chatScrollBox.add(msgBox);
22497
+ }
22498
+ function createMessageRenderable(msg, theme) {
22499
+ switch (msg.type) {
22500
+ case "user":
22501
+ return createUserMessageRenderable(msg, theme);
22502
+ case "assistant":
22503
+ return createAssistantMessageRenderable(msg, theme);
22504
+ case "result":
22505
+ return createResultMessageRenderable(msg, theme);
22506
+ case "system":
22507
+ default:
22508
+ return createSystemMessageRenderable(msg, theme);
22509
+ }
22510
+ }
22511
+ function createUserMessageRenderable(msg, theme) {
22512
+ const box = new BoxRenderable(renderer, {
22513
+ id: `msg-${msg.id}`,
21140
22514
  flexDirection: "row",
21141
- visible: false,
21142
- marginBottom: 1
22515
+ width: "100%"
21143
22516
  });
21144
- mainContainer.add(confirmPrompt);
21145
- const confirmText = new TextRenderable(renderer, {
21146
- id: "confirm-text",
21147
- content: t`${fg(theme.colors.warning)("[Enter] Execute")} ${fg(theme.colors.textMuted)("|")} ${fg(theme.colors.error)("[Esc] Cancel")} ${fg(theme.colors.textMuted)("|")} ${fg(theme.colors.primary)("[e] Edit")}`
22517
+ const text = new TextRenderable(renderer, {
22518
+ id: `msg-${msg.id}-text`,
22519
+ content: t`${fg(theme.colors.success)(">")} ${fg(theme.colors.text)(msg.content)}`
21148
22520
  });
21149
- confirmPrompt.add(confirmText);
21150
- outputContainer = new BoxRenderable(renderer, {
21151
- id: "output-container",
21152
- flexGrow: 1,
22521
+ box.add(text);
22522
+ return box;
22523
+ }
22524
+ function createAssistantMessageRenderable(msg, theme) {
22525
+ const isSelected = pendingMessageId === msg.id;
22526
+ const card = new BoxRenderable(renderer, {
22527
+ id: `msg-${msg.id}`,
22528
+ flexDirection: "column",
22529
+ width: "100%",
21153
22530
  border: true,
21154
- borderColor: theme.colors.border,
22531
+ borderColor: isSelected ? theme.colors.primary : theme.colors.border,
21155
22532
  borderStyle: "single",
21156
- title: "Output",
21157
- padding: 1
22533
+ paddingLeft: 1,
22534
+ paddingRight: 1,
22535
+ paddingTop: 0,
22536
+ paddingBottom: 0,
22537
+ backgroundColor: theme.colors.backgroundPanel
21158
22538
  });
21159
- mainContainer.add(outputContainer);
21160
- const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
21161
- const freeModelsNote = config.provider === "opencode-zen" ? `
21162
- ${fg(theme.colors.success)("Free models available!")} Try: grok-code, glm-4.7-free` : "";
21163
- outputText = new TextRenderable(renderer, {
21164
- id: "output-text",
21165
- content: t`${fg(theme.colors.textMuted)(`Ready. Using ${providerName}.`)}${freeModelsNote}
21166
-
21167
- ${fg(theme.colors.textMuted)("Type what you want to do, or press")} ${fg(theme.colors.primary)("Ctrl+X P")} ${fg(theme.colors.textMuted)("for command palette.")}`
22539
+ const commandText = new TextRenderable(renderer, {
22540
+ id: `msg-${msg.id}-cmd`,
22541
+ content: t`${fg(theme.colors.textMuted)("Command:")} ${fg(theme.colors.text)(msg.command || "")}`
21168
22542
  });
21169
- outputContainer.add(outputText);
21170
- statusText = new TextRenderable(renderer, {
21171
- id: "status-text",
21172
- content: getDryRunStatus(),
21173
- marginTop: 1
22543
+ card.add(commandText);
22544
+ if (msg.safety) {
22545
+ const severityColor = getSeverityColor(msg.safety.severity);
22546
+ const severityText = msg.safety.isDangerous ? `${msg.safety.severity.toUpperCase()} risk${msg.safety.reason ? ` - ${msg.safety.reason}` : ""}` : "Low risk";
22547
+ const safetyText = new TextRenderable(renderer, {
22548
+ id: `msg-${msg.id}-safety`,
22549
+ content: t`${fg(severityColor)("●")} ${fg(theme.colors.textMuted)(severityText)}`
22550
+ });
22551
+ card.add(safetyText);
22552
+ }
22553
+ if (isSelected && !msg.executed) {
22554
+ const actionsText = new TextRenderable(renderer, {
22555
+ id: `msg-${msg.id}-actions`,
22556
+ content: t`${fg(theme.colors.warning)("[Enter]")} ${fg(theme.colors.textMuted)("Run")} ${fg(theme.colors.primary)("[c]")} ${fg(theme.colors.textMuted)("Copy")} ${fg(theme.colors.primary)("[e]")} ${fg(theme.colors.textMuted)("Edit")}`
22557
+ });
22558
+ card.add(actionsText);
22559
+ }
22560
+ if (msg.executed) {
22561
+ const execText = new TextRenderable(renderer, {
22562
+ id: `msg-${msg.id}-exec`,
22563
+ content: t`${fg(theme.colors.success)("Executed")}`
22564
+ });
22565
+ card.add(execText);
22566
+ }
22567
+ return card;
22568
+ }
22569
+ function createResultMessageRenderable(msg, theme) {
22570
+ const isSuccess = msg.exitCode === undefined || msg.exitCode === 0;
22571
+ const isExpanded = msg.expanded ?? false;
22572
+ const hasOutput = msg.content && msg.content.trim().length > 0;
22573
+ const outputLines = hasOutput ? msg.content.trim().split(`
22574
+ `) : [];
22575
+ const isLongOutput = outputLines.length > 5;
22576
+ const PREVIEW_LINES = 3;
22577
+ const card = new BoxRenderable(renderer, {
22578
+ id: `msg-${msg.id}`,
22579
+ flexDirection: "column",
22580
+ width: "100%",
22581
+ border: true,
22582
+ borderColor: isSuccess ? theme.colors.success : theme.colors.error,
22583
+ borderStyle: "single",
22584
+ paddingLeft: 1,
22585
+ paddingRight: 1,
22586
+ backgroundColor: theme.colors.backgroundPanel,
22587
+ onMouseDown: isLongOutput ? () => {
22588
+ toggleResultExpand(msg.id);
22589
+ } : undefined
21174
22590
  });
21175
- mainContainer.add(statusText);
21176
- inputField.on(InputRenderableEvents.ENTER, handleInput);
21177
- renderer.keyInput.on("keypress", handleKeypress);
21178
- inputField.focus();
22591
+ const statusIcon = isSuccess ? "✓" : "✗";
22592
+ const statusColor = isSuccess ? theme.colors.success : theme.colors.error;
22593
+ const statusLabel = isSuccess ? "Executed successfully" : `Exit code: ${msg.exitCode}`;
22594
+ const expandIcon = isLongOutput ? isExpanded ? "▼" : "▶" : "";
22595
+ const lineCount = isLongOutput ? ` (${outputLines.length} lines)` : "";
22596
+ const statusText = new TextRenderable(renderer, {
22597
+ id: `msg-${msg.id}-status`,
22598
+ content: t`${fg(statusColor)(statusIcon)} ${fg(theme.colors.text)(statusLabel)}${fg(theme.colors.textMuted)(lineCount)} ${fg(theme.colors.primary)(expandIcon)}`
22599
+ });
22600
+ card.add(statusText);
22601
+ if (hasOutput) {
22602
+ let displayContent;
22603
+ if (isExpanded || !isLongOutput) {
22604
+ displayContent = msg.content.trim();
22605
+ } else {
22606
+ const previewLines = outputLines.slice(0, PREVIEW_LINES);
22607
+ displayContent = previewLines.join(`
22608
+ `) + `
22609
+ ... ${outputLines.length - PREVIEW_LINES} more lines`;
22610
+ }
22611
+ const outputText = new TextRenderable(renderer, {
22612
+ id: `msg-${msg.id}-output`,
22613
+ content: t`${fg(theme.colors.textMuted)(displayContent)}`
22614
+ });
22615
+ card.add(outputText);
22616
+ if (isLongOutput) {
22617
+ const hintText = new TextRenderable(renderer, {
22618
+ id: `msg-${msg.id}-hint`,
22619
+ content: t`${fg(theme.colors.primary)("[o]")} ${fg(theme.colors.textMuted)(isExpanded ? "Collapse" : "Expand output")}`
22620
+ });
22621
+ card.add(hintText);
22622
+ }
22623
+ }
22624
+ return card;
22625
+ }
22626
+ function createSystemMessageRenderable(msg, theme) {
22627
+ const box = new BoxRenderable(renderer, {
22628
+ id: `msg-${msg.id}`,
22629
+ flexDirection: "column",
22630
+ width: "100%"
22631
+ });
22632
+ const text = new TextRenderable(renderer, {
22633
+ id: `msg-${msg.id}-text`,
22634
+ content: t`${fg(theme.colors.textMuted)(msg.content)}`
22635
+ });
22636
+ box.add(text);
22637
+ return box;
22638
+ }
22639
+ function updateAssistantMessage(msgId, updates) {
22640
+ const msgIndex = chatMessages.findIndex((m) => m.id === msgId);
22641
+ if (msgIndex === -1)
22642
+ return;
22643
+ const msg = chatMessages[msgIndex];
22644
+ Object.assign(msg, updates);
22645
+ chatScrollBox.remove(`msg-${msgId}`);
22646
+ const theme = getTheme();
22647
+ const newBox = createMessageRenderable(msg, theme);
22648
+ chatScrollBox.add(newBox);
22649
+ }
22650
+ function updateResultMessage(msgId, updates) {
22651
+ const msgIndex = chatMessages.findIndex((m) => m.id === msgId);
22652
+ if (msgIndex === -1)
22653
+ return;
22654
+ const msg = chatMessages[msgIndex];
22655
+ Object.assign(msg, updates);
22656
+ chatScrollBox.remove(`msg-${msgId}`);
22657
+ const theme = getTheme();
22658
+ const newBox = createMessageRenderable(msg, theme);
22659
+ chatScrollBox.add(newBox);
22660
+ }
22661
+ function toggleResultExpand(msgId) {
22662
+ const msg = chatMessages.find((m) => m.id === msgId);
22663
+ if (!msg || msg.type !== "result")
22664
+ return;
22665
+ const outputLines = msg.content?.trim().split(`
22666
+ `) || [];
22667
+ if (outputLines.length <= 5)
22668
+ return;
22669
+ updateResultMessage(msgId, { expanded: !msg.expanded });
22670
+ }
22671
+ function toggleLastResultExpand() {
22672
+ const resultMessages = chatMessages.filter((m) => m.type === "result");
22673
+ if (resultMessages.length === 0)
22674
+ return;
22675
+ const lastResult = resultMessages[resultMessages.length - 1];
22676
+ toggleResultExpand(lastResult.id);
21179
22677
  }
21180
22678
  function getModelDisplay() {
21181
22679
  const theme = getTheme();
@@ -21184,30 +22682,22 @@ function getModelDisplay() {
21184
22682
  const freeBadge = currentModel.free ? fg(theme.colors.success)(" FREE") : "";
21185
22683
  return t`${providerBadge} ${fg(categoryColor)(currentModel.name)}${freeBadge}`;
21186
22684
  }
21187
- function getDryRunStatus() {
21188
- const theme = getTheme();
21189
- if (dryRunMode) {
21190
- return t`${fg(theme.colors.warning)("[DRY RUN]")} ${fg(theme.colors.textMuted)("Ctrl+X P palette | Ctrl+X M model | Ctrl+X D dry-run")}`;
21191
- }
21192
- return t`${fg(theme.colors.textMuted)("Ctrl+X P palette | Ctrl+X M model | Ctrl+X ? help")}`;
21193
- }
21194
22685
  function refreshThemeColors() {
21195
22686
  const theme = getTheme();
21196
22687
  renderer.setBackgroundColor(theme.colors.background);
21197
22688
  if (headerText) {
21198
- headerText.content = t`${bold(fg(theme.colors.primary)("magic-shell"))} ${fg(theme.colors.textMuted)("- natural language to terminal commands")}`;
21199
- }
21200
- if (cwdText) {
21201
- cwdText.content = t`${fg(theme.colors.textMuted)("cwd:")} ${fg(theme.colors.success)(currentCwd)}`;
22689
+ headerText.content = t`${bold(fg(theme.colors.primary)("magic-shell"))}`;
21202
22690
  }
21203
- if (modelText) {
21204
- modelText.content = getModelDisplay();
22691
+ if (statusBarText) {
22692
+ statusBarText.content = getStatusBarContent();
21205
22693
  }
21206
- if (statusText) {
21207
- statusText.content = getDryRunStatus();
22694
+ if (helpBarText) {
22695
+ helpBarText.content = getHelpBarContent();
21208
22696
  }
21209
- if (outputContainer) {
21210
- outputContainer.borderColor = theme.colors.border;
22697
+ if (chatScrollBox) {
22698
+ chatScrollBox.rootOptions = {
22699
+ borderColor: theme.colors.border
22700
+ };
21211
22701
  }
21212
22702
  if (inputField) {
21213
22703
  inputField.focusedBackgroundColor = theme.colors.backgroundPanel;
@@ -21215,14 +22705,6 @@ function refreshThemeColors() {
21215
22705
  inputField.placeholderColor = theme.colors.textMuted;
21216
22706
  inputField.cursorColor = theme.colors.primary;
21217
22707
  }
21218
- const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
21219
- const freeModelsNote = config.provider === "opencode-zen" ? `
21220
- ${fg(theme.colors.success)("Free models available!")} Try: grok-code, glm-4.7-free` : "";
21221
- if (outputText) {
21222
- outputText.content = t`${fg(theme.colors.textMuted)(`Ready. Using ${providerName}.`)}${freeModelsNote}
21223
-
21224
- ${fg(theme.colors.textMuted)("Type what you want to do, or press")} ${fg(theme.colors.primary)("Ctrl+X P")} ${fg(theme.colors.textMuted)("for command palette.")}`;
21225
- }
21226
22708
  }
21227
22709
  async function handleInput(value) {
21228
22710
  const input = value.trim();
@@ -21233,8 +22715,9 @@ async function handleInput(value) {
21233
22715
  await handleSpecialCommand(input);
21234
22716
  return;
21235
22717
  }
22718
+ addUserMessage(input);
21236
22719
  if (isDirectCommand(input)) {
21237
- await processCommand(input, input);
22720
+ await processDirectCommand(input, input);
21238
22721
  return;
21239
22722
  }
21240
22723
  await translateAndProcess(input);
@@ -21268,38 +22751,50 @@ function isDirectCommand(input) {
21268
22751
  async function translateAndProcess(input) {
21269
22752
  const apiKey = await getApiKey(config.provider);
21270
22753
  if (!apiKey) {
21271
- setOutput(t`${fg("#ef4444")("Error: No API key configured. Run !provider to set up.")}`);
22754
+ addSystemMessage("Error: No API key configured. Run !provider to set up.");
21272
22755
  return;
21273
22756
  }
21274
- setOutput(t`${fg("#64748b")("Translating...")}`);
22757
+ const loadingMsg = addSystemMessage("Translating...");
21275
22758
  try {
21276
- const command = await translateToCommand(apiKey, currentModel, input, currentCwd, history);
21277
- commandPreview.content = t`${fg("#64748b")("Command:")} ${fg("#f8fafc")(command)}`;
22759
+ const command = await translateToCommand(apiKey, currentModel, input, currentCwd, history, config.repoContext);
22760
+ chatScrollBox.remove(`msg-${loadingMsg.id}`);
22761
+ chatMessages = chatMessages.filter((m) => m.id !== loadingMsg.id);
21278
22762
  const safety = analyzeCommand(command, config);
22763
+ const assistantMsg = addAssistantMessage(input, command, safety);
21279
22764
  if (safety.isDangerous) {
21280
- safetyWarning.content = t`${fg(getSeverityColor(safety.severity))(`[${safety.severity.toUpperCase()}] ${safety.reason}`)}`;
21281
- pendingCommand = command;
22765
+ pendingMessageId = assistantMsg.id;
21282
22766
  awaitingConfirmation = true;
21283
- confirmPrompt.visible = true;
21284
- setOutput(t`${fg("#fbbf24")("Command requires confirmation. Press Enter to execute or Esc to cancel.")}`);
22767
+ helpBarText.content = getHelpBarContent();
21285
22768
  } else {
21286
- safetyWarning.content = "";
21287
- await processCommand(input, command);
22769
+ await executeAndShowResult(input, command, assistantMsg.id);
21288
22770
  }
21289
22771
  } catch (error) {
22772
+ chatScrollBox.remove(`msg-${loadingMsg.id}`);
22773
+ chatMessages = chatMessages.filter((m) => m.id !== loadingMsg.id);
21290
22774
  const message = error instanceof Error ? error.message : String(error);
21291
- setOutput(t`${fg("#ef4444")(`Error: ${message}`)}`);
22775
+ addSystemMessage(`Error: ${message}`);
21292
22776
  }
21293
22777
  }
21294
- async function processCommand(input, command) {
22778
+ async function processDirectCommand(input, command) {
22779
+ const safety = analyzeCommand(command, config);
22780
+ const assistantMsg = addAssistantMessage(input, command, safety);
22781
+ if (safety.isDangerous) {
22782
+ pendingMessageId = assistantMsg.id;
22783
+ awaitingConfirmation = true;
22784
+ helpBarText.content = getHelpBarContent();
22785
+ } else {
22786
+ await executeAndShowResult(input, command, assistantMsg.id);
22787
+ }
22788
+ }
22789
+ async function executeAndShowResult(input, command, assistantMsgId) {
21295
22790
  if (command.startsWith("cd ")) {
21296
22791
  const path2 = command.slice(3).trim().replace(/^["']|["']$/g, "");
21297
22792
  try {
21298
22793
  const expandedPath = path2.startsWith("~") ? path2.replace("~", process.env.HOME || "") : path2;
21299
22794
  process.chdir(expandedPath);
21300
22795
  currentCwd = getCwd();
21301
- cwdText.content = t`${fg("#64748b")("cwd:")} ${fg("#22c55e")(currentCwd)}`;
21302
- setOutput(t`${fg("#22c55e")(`Changed directory to ${currentCwd}`)}`);
22796
+ statusBarText.content = getStatusBarContent();
22797
+ addResultMessage(`Changed directory to ${currentCwd}`, 0);
21303
22798
  addToHistory({
21304
22799
  input,
21305
22800
  command,
@@ -21307,35 +22802,37 @@ async function processCommand(input, command) {
21307
22802
  timestamp: Date.now()
21308
22803
  });
21309
22804
  history = loadHistory();
22805
+ updateAssistantMessage(assistantMsgId, { executed: true });
21310
22806
  } catch (err) {
21311
- setOutput(t`${fg("#ef4444")(`cd: ${err instanceof Error ? err.message : String(err)}`)}`);
22807
+ addResultMessage(`cd: ${err instanceof Error ? err.message : String(err)}`, 1);
21312
22808
  }
21313
22809
  clearCommandState();
21314
22810
  return;
21315
22811
  }
21316
22812
  if (dryRunMode) {
21317
- setOutput(t`${fg("#fbbf24")("[DRY RUN]")} Would execute: ${fg("#f8fafc")(command)}`);
22813
+ addResultMessage(`[DRY RUN] Would execute: ${command}`, 0);
22814
+ updateAssistantMessage(assistantMsgId, { executed: true });
21318
22815
  clearCommandState();
21319
22816
  return;
21320
22817
  }
21321
- setOutput(t`${fg("#64748b")("Executing...")}`);
21322
22818
  try {
21323
- const result = await executeCommand(command);
21324
- setOutput(result || t`${fg("#22c55e")("Command completed successfully")}`);
22819
+ const { output, exitCode } = await executeCommandWithCode(command);
22820
+ addResultMessage(output || "Command completed successfully", exitCode);
21325
22821
  addToHistory({
21326
22822
  input,
21327
22823
  command,
21328
- output: result.slice(0, 500),
22824
+ output: output.slice(0, 500),
21329
22825
  timestamp: Date.now()
21330
22826
  });
21331
22827
  history = loadHistory();
22828
+ updateAssistantMessage(assistantMsgId, { executed: true });
21332
22829
  } catch (error) {
21333
22830
  const message = error instanceof Error ? error.message : String(error);
21334
- setOutput(t`${fg("#ef4444")(`Error: ${message}`)}`);
22831
+ addResultMessage(`Error: ${message}`, 1);
21335
22832
  }
21336
22833
  clearCommandState();
21337
22834
  }
21338
- function executeCommand(command) {
22835
+ function executeCommandWithCode(command) {
21339
22836
  return new Promise((resolve3, reject) => {
21340
22837
  const child = spawn(command, {
21341
22838
  shell: true,
@@ -21354,23 +22851,16 @@ function executeCommand(command) {
21354
22851
  reject(error);
21355
22852
  });
21356
22853
  child.on("close", (code) => {
21357
- if (code === 0) {
21358
- resolve3(stdout || stderr);
21359
- } else {
21360
- resolve3(stderr || stdout || `Command exited with code ${code}`);
21361
- }
22854
+ const exitCode = code ?? 0;
22855
+ const output = stdout || stderr || (exitCode === 0 ? "" : `Command exited with code ${exitCode}`);
22856
+ resolve3({ output, exitCode });
21362
22857
  });
21363
22858
  });
21364
22859
  }
21365
22860
  function clearCommandState() {
21366
- pendingCommand = null;
22861
+ pendingMessageId = null;
21367
22862
  awaitingConfirmation = false;
21368
- confirmPrompt.visible = false;
21369
- commandPreview.content = "";
21370
- safetyWarning.content = "";
21371
- }
21372
- function setOutput(content) {
21373
- outputText.content = content;
22863
+ helpBarText.content = getHelpBarContent();
21374
22864
  }
21375
22865
  async function handleSpecialCommand(input) {
21376
22866
  const cmd = input.slice(1).toLowerCase().trim();
@@ -21386,8 +22876,8 @@ async function handleSpecialCommand(input) {
21386
22876
  break;
21387
22877
  case "dry":
21388
22878
  dryRunMode = !dryRunMode;
21389
- statusText.content = getDryRunStatus();
21390
- setOutput(t`${fg("#22c55e")(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`)}`);
22879
+ statusBarText.content = getStatusBarContent();
22880
+ addSystemMessage(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`);
21391
22881
  break;
21392
22882
  case "config":
21393
22883
  await showConfig();
@@ -21396,65 +22886,74 @@ async function handleSpecialCommand(input) {
21396
22886
  showHistory();
21397
22887
  break;
21398
22888
  case "clear":
21399
- setOutput("");
22889
+ clearChat();
21400
22890
  break;
21401
22891
  default:
21402
22892
  if (cmd) {
21403
- await processCommand(input, cmd);
22893
+ addUserMessage(cmd);
22894
+ await processDirectCommand(input, cmd);
21404
22895
  }
21405
22896
  }
21406
22897
  }
22898
+ function clearChat() {
22899
+ for (const msg of chatMessages) {
22900
+ chatScrollBox.remove(`msg-${msg.id}`);
22901
+ }
22902
+ chatMessages = [];
22903
+ addSystemMessage(getWelcomeMessage());
22904
+ }
21407
22905
  function showHelp() {
21408
- const theme = getTheme();
21409
- setOutput(t`${bold(fg(theme.colors.primary)("Magic Shell"))}
21410
-
21411
- ${bold(fg(theme.colors.textMuted)("Keyboard Shortcuts (Ctrl+X then...):"))}
21412
- ${fg(theme.colors.primary)("P")} ${fg(theme.colors.textMuted)("Command palette")} ${fg(theme.colors.primary)("M")} ${fg(theme.colors.textMuted)("Change model")}
21413
- ${fg(theme.colors.primary)("S")} ${fg(theme.colors.textMuted)("Switch provider")} ${fg(theme.colors.primary)("D")} ${fg(theme.colors.textMuted)("Toggle dry-run")}
21414
- ${fg(theme.colors.primary)("T")} ${fg(theme.colors.textMuted)("Change theme")} ${fg(theme.colors.primary)("H")} ${fg(theme.colors.textMuted)("Show history")}
21415
- ${fg(theme.colors.primary)("C")} ${fg(theme.colors.textMuted)("Show config")} ${fg(theme.colors.primary)("L")} ${fg(theme.colors.textMuted)("Clear output")}
21416
- ${fg(theme.colors.primary)("?")} ${fg(theme.colors.textMuted)("This help")} ${fg(theme.colors.primary)("Q")} ${fg(theme.colors.textMuted)("Exit")}
22906
+ const helpText = `Keyboard Shortcuts (Ctrl+X then...):
22907
+ P Command palette M Change model
22908
+ S Switch provider D Toggle dry-run
22909
+ T Change theme R Toggle repo context
22910
+ H Show history L Clear chat
22911
+ C Show config ? This help
22912
+ Q Exit
21417
22913
 
21418
- ${bold(fg(theme.colors.textMuted)("Other:"))}
21419
- ${fg(theme.colors.primary)("Ctrl+C")} ${fg(theme.colors.textMuted)("Exit / Cancel")} ${fg(theme.colors.primary)("Esc")} ${fg(theme.colors.textMuted)("Close palette")}
22914
+ Other:
22915
+ Ctrl+C Exit / Cancel Esc Close palette
21420
22916
 
21421
- ${bold(fg(theme.colors.textMuted)("Tips:"))}
22917
+ Tips:
21422
22918
  - Type naturally: "list all files" -> ls -la
21423
22919
  - Reference history: "do that again", "undo"
21424
- - ${fg(theme.colors.success)("Free models:")} gpt-5-nano, grok-code, glm-4.7-free`);
22920
+ - Enable repo context to use project scripts (Ctrl+X R)`;
22921
+ addSystemMessage(helpText);
21425
22922
  }
21426
22923
  async function showConfig() {
21427
22924
  const theme = getTheme();
21428
22925
  const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
21429
22926
  const apiKey = await getApiKey(config.provider);
21430
- const apiKeyStatus = apiKey ? fg(theme.colors.success)("configured") : fg(theme.colors.error)("not set");
21431
- const freeBadge = currentModel.free ? fg(theme.colors.success)(" (FREE)") : "";
22927
+ const apiKeyStatus = apiKey ? "configured" : "not set";
22928
+ const freeBadge = currentModel.free ? " (FREE)" : "";
21432
22929
  const shellInfo = getShellInfo();
21433
- setOutput(t`${bold(fg(theme.colors.primary)("Current Configuration"))}
22930
+ const configText = `Current Configuration
21434
22931
 
21435
- ${fg(theme.colors.textMuted)("Provider:")} ${fg(theme.colors.text)(providerName)}
21436
- ${fg(theme.colors.textMuted)("Model:")} ${fg(theme.colors.text)(currentModel.name)}${freeBadge}
21437
- ${fg(theme.colors.textMuted)("Model ID:")} ${fg(theme.colors.textMuted)(currentModel.id)}
21438
- ${fg(theme.colors.textMuted)("Category:")} ${fg(theme.colors.text)(currentModel.category)}
21439
- ${fg(theme.colors.textMuted)("Theme:")} ${fg(theme.colors.text)(theme.name)}
21440
- ${fg(theme.colors.textMuted)("Shell:")} ${fg(theme.colors.text)(shellInfo.shell)} ${fg(theme.colors.textMuted)(`(${shellInfo.shellPath})`)}
21441
- ${fg(theme.colors.textMuted)("Platform:")} ${fg(theme.colors.text)(shellInfo.platform)}${shellInfo.isWSL ? fg(theme.colors.textMuted)(" (WSL)") : ""}
21442
- ${fg(theme.colors.textMuted)("Safety:")} ${fg(theme.colors.text)(config.safetyLevel)}
21443
- ${fg(theme.colors.textMuted)("Dry-run:")} ${fg(theme.colors.text)(dryRunMode ? "ON" : "OFF")}
21444
- ${fg(theme.colors.textMuted)("API Key:")} ${apiKeyStatus}
21445
- ${fg(theme.colors.textMuted)("History:")} ${fg(theme.colors.text)(`${history.length} commands`)}`);
22932
+ Provider: ${providerName}
22933
+ Model: ${currentModel.name}${freeBadge}
22934
+ Model ID: ${currentModel.id}
22935
+ Category: ${currentModel.category}
22936
+ Theme: ${theme.name}
22937
+ Shell: ${shellInfo.shell} (${shellInfo.shellPath})
22938
+ Platform: ${shellInfo.platform}${shellInfo.isWSL ? " (WSL)" : ""}
22939
+ Safety: ${config.safetyLevel}
22940
+ Dry-run: ${dryRunMode ? "ON" : "OFF"}
22941
+ Repo context: ${config.repoContext ? "ON" : "OFF"}
22942
+ API Key: ${apiKeyStatus}
22943
+ History: ${history.length} commands`;
22944
+ addSystemMessage(configText);
21446
22945
  }
21447
22946
  function showHistory() {
21448
22947
  if (history.length === 0) {
21449
- setOutput(t`${fg("#64748b")("No command history yet.")}`);
22948
+ addSystemMessage("No command history yet.");
21450
22949
  return;
21451
22950
  }
21452
22951
  const recent = history.slice(-10);
21453
22952
  const lines = recent.map((entry, i) => {
21454
22953
  const date = new Date(entry.timestamp).toLocaleTimeString();
21455
- return t`${fg("#64748b")(`${i + 1}.`)} ${fg("#94a3b8")(`[${date}]`)} ${fg("#f8fafc")(entry.command)}`;
22954
+ return `${i + 1}. [${date}] ${entry.command}`;
21456
22955
  });
21457
- setOutput(t`${bold(fg("#60a5fa")("Recent Command History"))}
22956
+ addSystemMessage(`Recent Command History
21458
22957
 
21459
22958
  ${lines.join(`
21460
22959
  `)}`);
@@ -21520,10 +23019,10 @@ async function switchProvider() {
21520
23019
  currentModel = models.find((m) => m.id === config.defaultModel) || models[0];
21521
23020
  config.defaultModel = currentModel.id;
21522
23021
  saveConfig(config);
21523
- modelText.content = getModelDisplay();
23022
+ statusBarText.content = getStatusBarContent();
21524
23023
  closeSelector();
21525
23024
  const providerName = newProvider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
21526
- setOutput(t`${fg("#22c55e")(`Switched to ${providerName}. Model: ${currentModel.name}`)}`);
23025
+ addSystemMessage(`Switched to ${providerName}. Model: ${currentModel.name}`);
21527
23026
  } else {
21528
23027
  closeSelector();
21529
23028
  renderer.root.remove("main-container");
@@ -21589,12 +23088,12 @@ function showModelSelector() {
21589
23088
  currentModel = option.value;
21590
23089
  config.defaultModel = currentModel.id;
21591
23090
  saveConfig(config);
21592
- modelText.content = getModelDisplay();
23091
+ statusBarText.content = getStatusBarContent();
21593
23092
  renderer.root.remove("model-selector-container");
21594
23093
  modelSelector = null;
21595
23094
  inputField.focus();
21596
23095
  const freeBadge = currentModel.free ? " (FREE)" : "";
21597
- setOutput(t`${fg("#22c55e")(`Model changed to ${currentModel.name}${freeBadge}`)}`);
23096
+ addSystemMessage(`Model changed to ${currentModel.name}${freeBadge}`);
21598
23097
  });
21599
23098
  modelSelector.focus();
21600
23099
  }
@@ -21652,8 +23151,7 @@ function showThemeSelector() {
21652
23151
  renderer.root.remove("theme-selector-container");
21653
23152
  themeSelector = null;
21654
23153
  refreshThemeColors();
21655
- const newTheme = getTheme();
21656
- setOutput(t`${fg(newTheme.colors.success)(`Theme changed to ${themeName}`)}`);
23154
+ addSystemMessage(`Theme changed to ${themeName}`);
21657
23155
  inputField.focus();
21658
23156
  });
21659
23157
  const escHandler = (key) => {
@@ -21699,8 +23197,20 @@ function getCommandPaletteOptions() {
21699
23197
  chord: "d",
21700
23198
  action: () => {
21701
23199
  dryRunMode = !dryRunMode;
21702
- statusText.content = getDryRunStatus();
21703
- setOutput(t`${fg("#22c55e")(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`)}`);
23200
+ statusBarText.content = getStatusBarContent();
23201
+ addSystemMessage(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`);
23202
+ }
23203
+ },
23204
+ {
23205
+ name: "Toggle Project Context",
23206
+ description: config.repoContext ? "Currently ON (sends script names to AI)" : "Currently OFF",
23207
+ key: "r",
23208
+ chord: "r",
23209
+ action: () => {
23210
+ config.repoContext = !config.repoContext;
23211
+ saveConfig(config);
23212
+ statusBarText.content = getStatusBarContent();
23213
+ addSystemMessage(`Project context: ${config.repoContext ? "ON - AI can see your package.json scripts, Makefile targets, etc." : "OFF"}`);
21704
23214
  }
21705
23215
  },
21706
23216
  {
@@ -21725,11 +23235,11 @@ function getCommandPaletteOptions() {
21725
23235
  action: () => showThemeSelector()
21726
23236
  },
21727
23237
  {
21728
- name: "Clear Output",
21729
- description: "Clear the output area",
23238
+ name: "Clear Chat",
23239
+ description: "Clear the chat history",
21730
23240
  key: "l",
21731
23241
  chord: "l",
21732
- action: () => setOutput("")
23242
+ action: () => clearChat()
21733
23243
  },
21734
23244
  {
21735
23245
  name: "Show Help",
@@ -21870,21 +23380,41 @@ function handleKeypress(key) {
21870
23380
  inputField.focus();
21871
23381
  return;
21872
23382
  }
21873
- if (awaitingConfirmation) {
23383
+ if (awaitingConfirmation && pendingMessageId) {
21874
23384
  clearCommandState();
21875
- setOutput(t`${fg("#64748b")("Command cancelled.")}`);
23385
+ addSystemMessage("Command cancelled.");
21876
23386
  inputField.focus();
21877
23387
  }
21878
23388
  }
21879
- if (key.name === "return" && awaitingConfirmation && pendingCommand) {
21880
- const cmd = pendingCommand;
21881
- clearCommandState();
21882
- processCommand("", cmd);
23389
+ if (key.name === "return" && awaitingConfirmation && pendingMessageId) {
23390
+ const msg = chatMessages.find((m) => m.id === pendingMessageId);
23391
+ if (msg && msg.command) {
23392
+ const command = msg.command;
23393
+ const msgId = pendingMessageId;
23394
+ clearCommandState();
23395
+ executeAndShowResult(msg.content, command, msgId);
23396
+ }
21883
23397
  }
21884
- if (key.name === "e" && awaitingConfirmation && pendingCommand) {
21885
- inputField.value = pendingCommand;
21886
- clearCommandState();
21887
- inputField.focus();
23398
+ if (key.name === "e" && awaitingConfirmation && pendingMessageId) {
23399
+ const msg = chatMessages.find((m) => m.id === pendingMessageId);
23400
+ if (msg && msg.command) {
23401
+ inputField.value = msg.command;
23402
+ clearCommandState();
23403
+ inputField.focus();
23404
+ }
23405
+ }
23406
+ if (key.name === "c" && awaitingConfirmation && pendingMessageId) {
23407
+ const msg = chatMessages.find((m) => m.id === pendingMessageId);
23408
+ if (msg && msg.command) {
23409
+ const copyCmd = process.platform === "darwin" ? "pbcopy" : "xclip -selection clipboard";
23410
+ const child = spawn(copyCmd, { shell: true });
23411
+ child.stdin?.write(msg.command);
23412
+ child.stdin?.end();
23413
+ addSystemMessage(`Copied to clipboard: ${msg.command}`);
23414
+ }
23415
+ }
23416
+ if (key.name === "o" && !awaitingConfirmation && !commandPalette && !modelSelector) {
23417
+ toggleLastResultExpand();
21888
23418
  }
21889
23419
  }
21890
23420
  if (__require.main == __require.module) {