@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/index.js CHANGED
@@ -518,22 +518,128 @@ function getPlatformPaths(platform) {
518
518
  }
519
519
  var init_shell = () => {};
520
520
 
521
+ // src/lib/repo-context.ts
522
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
523
+ import { join as join2 } from "path";
524
+ function detectRepoContext(cwd) {
525
+ const context = {
526
+ type: "unknown"
527
+ };
528
+ let detected = false;
529
+ if (existsSync3(join2(cwd, ".git"))) {
530
+ context.hasGit = true;
531
+ detected = true;
532
+ }
533
+ if (existsSync3(join2(cwd, "Dockerfile")) || existsSync3(join2(cwd, "docker-compose.yml")) || existsSync3(join2(cwd, "docker-compose.yaml"))) {
534
+ context.hasDocker = true;
535
+ detected = true;
536
+ }
537
+ const packageJsonPath = join2(cwd, "package.json");
538
+ if (existsSync3(packageJsonPath)) {
539
+ detected = true;
540
+ context.type = "node";
541
+ try {
542
+ const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
543
+ if (existsSync3(join2(cwd, "bun.lockb")) || existsSync3(join2(cwd, "bun.lock"))) {
544
+ context.packageManager = "bun";
545
+ } else if (existsSync3(join2(cwd, "pnpm-lock.yaml"))) {
546
+ context.packageManager = "pnpm";
547
+ } else if (existsSync3(join2(cwd, "yarn.lock"))) {
548
+ context.packageManager = "yarn";
549
+ } else if (existsSync3(join2(cwd, "package-lock.json"))) {
550
+ context.packageManager = "npm";
551
+ } else if (packageJson.packageManager) {
552
+ const pm = packageJson.packageManager.split("@")[0];
553
+ context.packageManager = pm;
554
+ }
555
+ if (packageJson.scripts && typeof packageJson.scripts === "object") {
556
+ context.scripts = Object.keys(packageJson.scripts);
557
+ }
558
+ } catch {}
559
+ }
560
+ const makefilePath = join2(cwd, "Makefile");
561
+ if (existsSync3(makefilePath)) {
562
+ detected = true;
563
+ if (context.type === "unknown")
564
+ context.type = "make";
565
+ try {
566
+ const makefile = readFileSync2(makefilePath, "utf-8");
567
+ const targetRegex = /^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:/gm;
568
+ const targets = [];
569
+ let match;
570
+ while ((match = targetRegex.exec(makefile)) !== null) {
571
+ if (!match[1].startsWith(".") && !match[1].startsWith("_")) {
572
+ targets.push(match[1]);
573
+ }
574
+ }
575
+ if (targets.length > 0) {
576
+ context.makeTargets = [...new Set(targets)];
577
+ }
578
+ } catch {}
579
+ }
580
+ if (existsSync3(join2(cwd, "Cargo.toml"))) {
581
+ detected = true;
582
+ context.type = "rust";
583
+ context.cargoCommands = ["build", "run", "test", "check", "clippy", "fmt", "doc"];
584
+ }
585
+ if (existsSync3(join2(cwd, "pyproject.toml")) || existsSync3(join2(cwd, "setup.py")) || existsSync3(join2(cwd, "requirements.txt"))) {
586
+ detected = true;
587
+ if (context.type === "unknown")
588
+ context.type = "python";
589
+ }
590
+ if (existsSync3(join2(cwd, "go.mod"))) {
591
+ detected = true;
592
+ if (context.type === "unknown")
593
+ context.type = "go";
594
+ }
595
+ return detected ? context : null;
596
+ }
597
+ function formatRepoContext(context) {
598
+ const lines = [];
599
+ lines.push(`Project type: ${context.type}`);
600
+ if (context.packageManager) {
601
+ lines.push(`Package manager: ${context.packageManager}`);
602
+ }
603
+ if (context.scripts && context.scripts.length > 0) {
604
+ const displayScripts = context.scripts.slice(0, 15);
605
+ const suffix = context.scripts.length > 15 ? ` (+${context.scripts.length - 15} more)` : "";
606
+ lines.push(`Available scripts: ${displayScripts.join(", ")}${suffix}`);
607
+ }
608
+ if (context.makeTargets && context.makeTargets.length > 0) {
609
+ const displayTargets = context.makeTargets.slice(0, 15);
610
+ const suffix = context.makeTargets.length > 15 ? ` (+${context.makeTargets.length - 15} more)` : "";
611
+ lines.push(`Make targets: ${displayTargets.join(", ")}${suffix}`);
612
+ }
613
+ if (context.cargoCommands) {
614
+ lines.push(`Cargo commands: ${context.cargoCommands.join(", ")}`);
615
+ }
616
+ if (context.hasDocker) {
617
+ lines.push(`Docker: available`);
618
+ }
619
+ if (context.hasGit) {
620
+ lines.push(`Git: initialized`);
621
+ }
622
+ return lines.join(`
623
+ `);
624
+ }
625
+ var init_repo_context = () => {};
626
+
521
627
  // src/lib/config.ts
522
628
  import { homedir as homedir3 } from "os";
523
- import { join as join2 } from "path";
524
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
629
+ import { join as join3 } from "path";
630
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
525
631
  function ensureConfigDir2() {
526
- if (!existsSync3(CONFIG_DIR2)) {
632
+ if (!existsSync4(CONFIG_DIR2)) {
527
633
  mkdirSync2(CONFIG_DIR2, { recursive: true });
528
634
  }
529
635
  }
530
636
  function loadConfig2() {
531
637
  ensureConfigDir2();
532
- if (!existsSync3(CONFIG_FILE2)) {
638
+ if (!existsSync4(CONFIG_FILE2)) {
533
639
  return { ...DEFAULT_CONFIG2 };
534
640
  }
535
641
  try {
536
- const data = readFileSync2(CONFIG_FILE2, "utf-8");
642
+ const data = readFileSync3(CONFIG_FILE2, "utf-8");
537
643
  const loaded = JSON.parse(data);
538
644
  return { ...DEFAULT_CONFIG2, ...loaded };
539
645
  } catch {
@@ -582,11 +688,11 @@ async function setApiKey2(provider, key) {
582
688
  }
583
689
  function loadHistory2() {
584
690
  ensureConfigDir2();
585
- if (!existsSync3(HISTORY_FILE2)) {
691
+ if (!existsSync4(HISTORY_FILE2)) {
586
692
  return [];
587
693
  }
588
694
  try {
589
- const data = readFileSync2(HISTORY_FILE2, "utf-8");
695
+ const data = readFileSync3(HISTORY_FILE2, "utf-8");
590
696
  return JSON.parse(data);
591
697
  } catch {
592
698
  return [];
@@ -605,14 +711,14 @@ function addToHistory(entry) {
605
711
  var CONFIG_DIR2, CONFIG_FILE2, HISTORY_FILE2, KEYCHAIN_OPENROUTER2 = "openrouter-api-key", KEYCHAIN_OPENCODE_ZEN2 = "opencode-zen-api-key", DEFAULT_CONFIG2;
606
712
  var init_config = __esm(() => {
607
713
  init_keychain();
608
- CONFIG_DIR2 = join2(homedir3(), ".magic-shell");
609
- CONFIG_FILE2 = join2(CONFIG_DIR2, "config.json");
610
- HISTORY_FILE2 = join2(CONFIG_DIR2, "history.json");
714
+ CONFIG_DIR2 = join3(homedir3(), ".magic-shell");
715
+ CONFIG_FILE2 = join3(CONFIG_DIR2, "config.json");
716
+ HISTORY_FILE2 = join3(CONFIG_DIR2, "history.json");
611
717
  DEFAULT_CONFIG2 = {
612
718
  provider: "opencode-zen",
613
719
  openrouterApiKey: "",
614
720
  opencodeZenApiKey: "",
615
- defaultModel: "gpt-5-nano",
721
+ defaultModel: "gemini-3-flash",
616
722
  safetyLevel: "moderate",
617
723
  dryRunByDefault: false,
618
724
  blockedCommands: [
@@ -623,7 +729,8 @@ var init_config = __esm(() => {
623
729
  "chmod -R 777 /",
624
730
  "chown -R"
625
731
  ],
626
- confirmedDangerousPatterns: []
732
+ confirmedDangerousPatterns: [],
733
+ repoContext: false
627
734
  };
628
735
  });
629
736
 
@@ -679,8 +786,8 @@ import { EventEmitter as EventEmitter3 } from "events";
679
786
  import { resolve, dirname } from "path";
680
787
  import { fileURLToPath } from "url";
681
788
  import { resolve as resolve2, isAbsolute, parse } from "path";
682
- import { existsSync as existsSync4 } from "fs";
683
- import { basename, join as join3 } from "path";
789
+ import { existsSync as existsSync6 } from "fs";
790
+ import { basename, join as join5 } from "path";
684
791
  import os from "os";
685
792
  import path from "path";
686
793
  import { EventEmitter as EventEmitter4 } from "events";
@@ -1430,6 +1537,13 @@ function t(strings, ...values) {
1430
1537
  }
1431
1538
  return new StyledText(chunks);
1432
1539
  }
1540
+
1541
+ class LinearScrollAccel {
1542
+ tick(_now) {
1543
+ return 1;
1544
+ }
1545
+ reset() {}
1546
+ }
1433
1547
  function isCompleteSequence(data) {
1434
1548
  if (!data.startsWith(ESC)) {
1435
1549
  return "not-escape";
@@ -2035,7 +2149,7 @@ function getBunfsRootPath() {
2035
2149
  return process.platform === "win32" ? "B:\\~BUN\\root" : "/$bunfs/root";
2036
2150
  }
2037
2151
  function normalizeBunfsPath(fileName) {
2038
- return join3(getBunfsRootPath(), basename(fileName));
2152
+ return join5(getBunfsRootPath(), basename(fileName));
2039
2153
  }
2040
2154
  function isValidDirectoryName(name) {
2041
2155
  if (!name || typeof name !== "string") {
@@ -11894,7 +12008,7 @@ var init_index_93qf6w1k = __esm(async () => {
11894
12008
  worker_path = this.options.workerPath;
11895
12009
  } else {
11896
12010
  worker_path = new URL("./parser.worker.js", import.meta.url).href;
11897
- if (!existsSync4(resolve2(import.meta.dirname, "parser.worker.js"))) {
12011
+ if (!existsSync6(resolve2(import.meta.dirname, "parser.worker.js"))) {
11898
12012
  worker_path = new URL("./parser.worker.ts", import.meta.url).href;
11899
12013
  }
11900
12014
  }
@@ -17249,7 +17363,7 @@ function canonicalize(obj, stack, replacementStack, replacer, key) {
17249
17363
  }
17250
17364
  return canonicalizedObj;
17251
17365
  }
17252
- 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;
17366
+ 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;
17253
17367
  var init_core = __esm(async () => {
17254
17368
  await init_index_93qf6w1k();
17255
17369
  EditBuffer = class EditBuffer extends EventEmitter10 {
@@ -18968,220 +19082,1360 @@ var init_core = __esm(async () => {
18968
19082
  };
18969
19083
  defaultThumbBackgroundColor = RGBA.fromHex("#9a9ea3");
18970
19084
  defaultTrackBackgroundColor = RGBA.fromHex("#252527");
18971
- defaultSelectKeybindings = [
18972
- { name: "up", action: "move-up" },
18973
- { name: "k", action: "move-up" },
18974
- { name: "down", action: "move-down" },
18975
- { name: "j", action: "move-down" },
18976
- { name: "up", shift: true, action: "move-up-fast" },
18977
- { name: "down", shift: true, action: "move-down-fast" },
18978
- { name: "return", action: "select-current" },
18979
- { name: "linefeed", action: "select-current" }
18980
- ];
18981
- ((SelectRenderableEvents2) => {
18982
- SelectRenderableEvents2["SELECTION_CHANGED"] = "selectionChanged";
18983
- SelectRenderableEvents2["ITEM_SELECTED"] = "itemSelected";
18984
- })(SelectRenderableEvents ||= {});
18985
- SelectRenderable = class SelectRenderable extends Renderable {
18986
- _focusable = true;
18987
- _options = [];
18988
- _selectedIndex = 0;
18989
- scrollOffset = 0;
18990
- maxVisibleItems;
19085
+ SliderRenderable = class SliderRenderable extends Renderable {
19086
+ orientation;
19087
+ _value;
19088
+ _min;
19089
+ _max;
19090
+ _viewPortSize;
18991
19091
  _backgroundColor;
18992
- _textColor;
18993
- _focusedBackgroundColor;
18994
- _focusedTextColor;
18995
- _selectedBackgroundColor;
18996
- _selectedTextColor;
18997
- _descriptionColor;
18998
- _selectedDescriptionColor;
18999
- _showScrollIndicator;
19000
- _wrapSelection;
19001
- _showDescription;
19002
- _font;
19003
- _itemSpacing;
19004
- linesPerItem;
19005
- fontHeight;
19006
- _fastScrollStep;
19007
- _keyBindingsMap;
19008
- _keyAliasMap;
19009
- _keyBindings;
19010
- _defaultOptions = {
19011
- backgroundColor: "transparent",
19012
- textColor: "#FFFFFF",
19013
- focusedBackgroundColor: "#1a1a1a",
19014
- focusedTextColor: "#FFFFFF",
19015
- selectedBackgroundColor: "#334455",
19016
- selectedTextColor: "#FFFF00",
19017
- selectedIndex: 0,
19018
- descriptionColor: "#888888",
19019
- selectedDescriptionColor: "#CCCCCC",
19020
- showScrollIndicator: false,
19021
- wrapSelection: false,
19022
- showDescription: true,
19023
- itemSpacing: 0,
19024
- fastScrollStep: 5
19025
- };
19092
+ _foregroundColor;
19093
+ _onChange;
19026
19094
  constructor(ctx, options) {
19027
- super(ctx, { ...options, buffered: true });
19028
- this._options = options.options || [];
19029
- const requestedIndex = options.selectedIndex ?? this._defaultOptions.selectedIndex;
19030
- this._selectedIndex = this._options.length > 0 ? Math.min(requestedIndex, this._options.length - 1) : 0;
19031
- this._backgroundColor = parseColor(options.backgroundColor || this._defaultOptions.backgroundColor);
19032
- this._textColor = parseColor(options.textColor || this._defaultOptions.textColor);
19033
- this._focusedBackgroundColor = parseColor(options.focusedBackgroundColor || this._defaultOptions.focusedBackgroundColor);
19034
- this._focusedTextColor = parseColor(options.focusedTextColor || this._defaultOptions.focusedTextColor);
19035
- this._showScrollIndicator = options.showScrollIndicator ?? this._defaultOptions.showScrollIndicator;
19036
- this._wrapSelection = options.wrapSelection ?? this._defaultOptions.wrapSelection;
19037
- this._showDescription = options.showDescription ?? this._defaultOptions.showDescription;
19038
- this._font = options.font;
19039
- this._itemSpacing = options.itemSpacing || this._defaultOptions.itemSpacing;
19040
- this.fontHeight = this._font ? measureText({ text: "A", font: this._font }).height : 1;
19041
- this.linesPerItem = this._showDescription ? this._font ? this.fontHeight + 1 : 2 : this._font ? this.fontHeight : 1;
19042
- this.linesPerItem += this._itemSpacing;
19043
- this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem));
19044
- this._selectedBackgroundColor = parseColor(options.selectedBackgroundColor || this._defaultOptions.selectedBackgroundColor);
19045
- this._selectedTextColor = parseColor(options.selectedTextColor || this._defaultOptions.selectedTextColor);
19046
- this._descriptionColor = parseColor(options.descriptionColor || this._defaultOptions.descriptionColor);
19047
- this._selectedDescriptionColor = parseColor(options.selectedDescriptionColor || this._defaultOptions.selectedDescriptionColor);
19048
- this._fastScrollStep = options.fastScrollStep || this._defaultOptions.fastScrollStep;
19049
- this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {});
19050
- this._keyBindings = options.keyBindings || [];
19051
- const mergedBindings = mergeKeyBindings(defaultSelectKeybindings, this._keyBindings);
19052
- this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
19053
- this.requestRender();
19095
+ super(ctx, { flexShrink: 0, ...options });
19096
+ this.orientation = options.orientation;
19097
+ this._min = options.min ?? 0;
19098
+ this._max = options.max ?? 100;
19099
+ this._value = options.value ?? this._min;
19100
+ this._viewPortSize = options.viewPortSize ?? Math.max(1, (this._max - this._min) * 0.1);
19101
+ this._onChange = options.onChange;
19102
+ this._backgroundColor = options.backgroundColor ? parseColor(options.backgroundColor) : defaultTrackBackgroundColor;
19103
+ this._foregroundColor = options.foregroundColor ? parseColor(options.foregroundColor) : defaultThumbBackgroundColor;
19104
+ this.setupMouseHandling();
19054
19105
  }
19055
- renderSelf(buffer, deltaTime) {
19056
- if (!this.visible || !this.frameBuffer)
19057
- return;
19058
- if (this.isDirty) {
19059
- this.refreshFrameBuffer();
19106
+ get value() {
19107
+ return this._value;
19108
+ }
19109
+ set value(newValue) {
19110
+ const clamped = Math.max(this._min, Math.min(this._max, newValue));
19111
+ if (clamped !== this._value) {
19112
+ this._value = clamped;
19113
+ this._onChange?.(clamped);
19114
+ this.emit("change", { value: clamped });
19115
+ this.requestRender();
19060
19116
  }
19061
19117
  }
19062
- refreshFrameBuffer() {
19063
- if (!this.frameBuffer || this._options.length === 0)
19064
- return;
19065
- const bgColor = this._focused ? this._focusedBackgroundColor : this._backgroundColor;
19066
- this.frameBuffer.clear(bgColor);
19067
- const contentX = 0;
19068
- const contentY = 0;
19069
- const contentWidth = this.width;
19070
- const contentHeight = this.height;
19071
- const visibleOptions = this._options.slice(this.scrollOffset, this.scrollOffset + this.maxVisibleItems);
19072
- for (let i = 0;i < visibleOptions.length; i++) {
19073
- const actualIndex = this.scrollOffset + i;
19074
- const option = visibleOptions[i];
19075
- const isSelected = actualIndex === this._selectedIndex;
19076
- const itemY = contentY + i * this.linesPerItem;
19077
- if (itemY + this.linesPerItem - 1 >= contentY + contentHeight)
19078
- break;
19079
- if (isSelected) {
19080
- const contentHeight2 = this.linesPerItem - this._itemSpacing;
19081
- this.frameBuffer.fillRect(contentX, itemY, contentWidth, contentHeight2, this._selectedBackgroundColor);
19082
- }
19083
- const nameContent = `${isSelected ? "▶ " : " "}${option.name}`;
19084
- const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
19085
- const nameColor = isSelected ? this._selectedTextColor : baseTextColor;
19086
- let descX = contentX + 3;
19087
- if (this._font) {
19088
- const indicator = isSelected ? "▶ " : " ";
19089
- this.frameBuffer.drawText(indicator, contentX + 1, itemY, nameColor);
19090
- const indicatorWidth = 2;
19091
- renderFontToFrameBuffer(this.frameBuffer, {
19092
- text: option.name,
19093
- x: contentX + 1 + indicatorWidth,
19094
- y: itemY,
19095
- color: nameColor,
19096
- backgroundColor: isSelected ? this._selectedBackgroundColor : bgColor,
19097
- font: this._font
19098
- });
19099
- descX = contentX + 1 + indicatorWidth;
19100
- } else {
19101
- this.frameBuffer.drawText(nameContent, contentX + 1, itemY, nameColor);
19118
+ get min() {
19119
+ return this._min;
19120
+ }
19121
+ set min(newMin) {
19122
+ if (newMin !== this._min) {
19123
+ this._min = newMin;
19124
+ if (this._value < newMin) {
19125
+ this.value = newMin;
19102
19126
  }
19103
- if (this._showDescription && itemY + this.fontHeight < contentY + contentHeight) {
19104
- const descColor = isSelected ? this._selectedDescriptionColor : this._descriptionColor;
19105
- this.frameBuffer.drawText(option.description, descX, itemY + this.fontHeight, descColor);
19127
+ this.requestRender();
19128
+ }
19129
+ }
19130
+ get max() {
19131
+ return this._max;
19132
+ }
19133
+ set max(newMax) {
19134
+ if (newMax !== this._max) {
19135
+ this._max = newMax;
19136
+ if (this._value > newMax) {
19137
+ this.value = newMax;
19106
19138
  }
19139
+ this.requestRender();
19107
19140
  }
19108
- if (this._showScrollIndicator && this._options.length > this.maxVisibleItems) {
19109
- this.renderScrollIndicatorToFrameBuffer(contentX, contentY, contentWidth, contentHeight);
19141
+ }
19142
+ set viewPortSize(size) {
19143
+ const clampedSize = Math.max(0.01, Math.min(size, this._max - this._min));
19144
+ if (clampedSize !== this._viewPortSize) {
19145
+ this._viewPortSize = clampedSize;
19146
+ this.requestRender();
19110
19147
  }
19111
19148
  }
19112
- renderScrollIndicatorToFrameBuffer(contentX, contentY, contentWidth, contentHeight) {
19113
- if (!this.frameBuffer)
19114
- return;
19115
- const scrollPercent = this._selectedIndex / Math.max(1, this._options.length - 1);
19116
- const indicatorHeight = Math.max(1, contentHeight - 2);
19117
- const indicatorY = contentY + 1 + Math.floor(scrollPercent * indicatorHeight);
19118
- const indicatorX = contentX + contentWidth - 1;
19119
- this.frameBuffer.drawText("█", indicatorX, indicatorY, parseColor("#666666"));
19149
+ get viewPortSize() {
19150
+ return this._viewPortSize;
19120
19151
  }
19121
- get options() {
19122
- return this._options;
19152
+ get backgroundColor() {
19153
+ return this._backgroundColor;
19123
19154
  }
19124
- set options(options) {
19125
- this._options = options;
19126
- this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, options.length - 1));
19127
- this.updateScrollOffset();
19155
+ set backgroundColor(value) {
19156
+ this._backgroundColor = parseColor(value);
19128
19157
  this.requestRender();
19129
19158
  }
19130
- getSelectedOption() {
19131
- return this._options[this._selectedIndex] || null;
19159
+ get foregroundColor() {
19160
+ return this._foregroundColor;
19132
19161
  }
19133
- getSelectedIndex() {
19134
- return this._selectedIndex;
19162
+ set foregroundColor(value) {
19163
+ this._foregroundColor = parseColor(value);
19164
+ this.requestRender();
19135
19165
  }
19136
- moveUp(steps = 1) {
19137
- const newIndex = this._selectedIndex - steps;
19138
- if (newIndex >= 0) {
19139
- this._selectedIndex = newIndex;
19140
- } else if (this._wrapSelection && this._options.length > 0) {
19141
- this._selectedIndex = this._options.length - 1;
19166
+ calculateDragOffsetVirtual(event) {
19167
+ const trackStart = this.orientation === "vertical" ? this.y : this.x;
19168
+ const mousePos = (this.orientation === "vertical" ? event.y : event.x) - trackStart;
19169
+ const virtualMousePos = Math.max(0, Math.min((this.orientation === "vertical" ? this.height : this.width) * 2, mousePos * 2));
19170
+ const virtualThumbStart = this.getVirtualThumbStart();
19171
+ const virtualThumbSize = this.getVirtualThumbSize();
19172
+ return Math.max(0, Math.min(virtualThumbSize, virtualMousePos - virtualThumbStart));
19173
+ }
19174
+ setupMouseHandling() {
19175
+ let isDragging = false;
19176
+ let dragOffsetVirtual = 0;
19177
+ this.onMouseDown = (event) => {
19178
+ event.stopPropagation();
19179
+ event.preventDefault();
19180
+ const thumb = this.getThumbRect();
19181
+ const inThumb = event.x >= thumb.x && event.x < thumb.x + thumb.width && event.y >= thumb.y && event.y < thumb.y + thumb.height;
19182
+ if (inThumb) {
19183
+ isDragging = true;
19184
+ dragOffsetVirtual = this.calculateDragOffsetVirtual(event);
19185
+ } else {
19186
+ this.updateValueFromMouseDirect(event);
19187
+ isDragging = true;
19188
+ dragOffsetVirtual = this.calculateDragOffsetVirtual(event);
19189
+ }
19190
+ };
19191
+ this.onMouseDrag = (event) => {
19192
+ if (!isDragging)
19193
+ return;
19194
+ event.stopPropagation();
19195
+ this.updateValueFromMouseWithOffset(event, dragOffsetVirtual);
19196
+ };
19197
+ this.onMouseUp = (event) => {
19198
+ if (isDragging) {
19199
+ this.updateValueFromMouseWithOffset(event, dragOffsetVirtual);
19200
+ }
19201
+ isDragging = false;
19202
+ };
19203
+ }
19204
+ updateValueFromMouseDirect(event) {
19205
+ const trackStart = this.orientation === "vertical" ? this.y : this.x;
19206
+ const trackSize = this.orientation === "vertical" ? this.height : this.width;
19207
+ const mousePos = this.orientation === "vertical" ? event.y : event.x;
19208
+ const relativeMousePos = mousePos - trackStart;
19209
+ const clampedMousePos = Math.max(0, Math.min(trackSize, relativeMousePos));
19210
+ const ratio = trackSize === 0 ? 0 : clampedMousePos / trackSize;
19211
+ const range = this._max - this._min;
19212
+ const newValue = this._min + ratio * range;
19213
+ this.value = newValue;
19214
+ }
19215
+ updateValueFromMouseWithOffset(event, offsetVirtual) {
19216
+ const trackStart = this.orientation === "vertical" ? this.y : this.x;
19217
+ const trackSize = this.orientation === "vertical" ? this.height : this.width;
19218
+ const mousePos = this.orientation === "vertical" ? event.y : event.x;
19219
+ const virtualTrackSize = trackSize * 2;
19220
+ const relativeMousePos = mousePos - trackStart;
19221
+ const clampedMousePos = Math.max(0, Math.min(trackSize, relativeMousePos));
19222
+ const virtualMousePos = clampedMousePos * 2;
19223
+ const virtualThumbSize = this.getVirtualThumbSize();
19224
+ const maxThumbStart = Math.max(0, virtualTrackSize - virtualThumbSize);
19225
+ let desiredThumbStart = virtualMousePos - offsetVirtual;
19226
+ desiredThumbStart = Math.max(0, Math.min(maxThumbStart, desiredThumbStart));
19227
+ const ratio = maxThumbStart === 0 ? 0 : desiredThumbStart / maxThumbStart;
19228
+ const range = this._max - this._min;
19229
+ const newValue = this._min + ratio * range;
19230
+ this.value = newValue;
19231
+ }
19232
+ getThumbRect() {
19233
+ const virtualThumbSize = this.getVirtualThumbSize();
19234
+ const virtualThumbStart = this.getVirtualThumbStart();
19235
+ const realThumbStart = Math.floor(virtualThumbStart / 2);
19236
+ const realThumbSize = Math.ceil((virtualThumbStart + virtualThumbSize) / 2) - realThumbStart;
19237
+ if (this.orientation === "vertical") {
19238
+ return {
19239
+ x: this.x,
19240
+ y: this.y + realThumbStart,
19241
+ width: this.width,
19242
+ height: Math.max(1, realThumbSize)
19243
+ };
19142
19244
  } else {
19143
- this._selectedIndex = 0;
19245
+ return {
19246
+ x: this.x + realThumbStart,
19247
+ y: this.y,
19248
+ width: Math.max(1, realThumbSize),
19249
+ height: this.height
19250
+ };
19144
19251
  }
19145
- this.updateScrollOffset();
19146
- this.requestRender();
19147
- this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
19148
19252
  }
19149
- moveDown(steps = 1) {
19150
- const newIndex = this._selectedIndex + steps;
19151
- if (newIndex < this._options.length) {
19152
- this._selectedIndex = newIndex;
19153
- } else if (this._wrapSelection && this._options.length > 0) {
19154
- this._selectedIndex = 0;
19253
+ renderSelf(buffer) {
19254
+ if (this.orientation === "horizontal") {
19255
+ this.renderHorizontal(buffer);
19155
19256
  } else {
19156
- this._selectedIndex = this._options.length - 1;
19257
+ this.renderVertical(buffer);
19258
+ }
19259
+ }
19260
+ renderHorizontal(buffer) {
19261
+ const virtualThumbSize = this.getVirtualThumbSize();
19262
+ const virtualThumbStart = this.getVirtualThumbStart();
19263
+ const virtualThumbEnd = virtualThumbStart + virtualThumbSize;
19264
+ buffer.fillRect(this.x, this.y, this.width, this.height, this._backgroundColor);
19265
+ const realStartCell = Math.floor(virtualThumbStart / 2);
19266
+ const realEndCell = Math.ceil(virtualThumbEnd / 2) - 1;
19267
+ const startX = Math.max(0, realStartCell);
19268
+ const endX = Math.min(this.width - 1, realEndCell);
19269
+ for (let realX = startX;realX <= endX; realX++) {
19270
+ const virtualCellStart = realX * 2;
19271
+ const virtualCellEnd = virtualCellStart + 2;
19272
+ const thumbStartInCell = Math.max(virtualThumbStart, virtualCellStart);
19273
+ const thumbEndInCell = Math.min(virtualThumbEnd, virtualCellEnd);
19274
+ const coverage = thumbEndInCell - thumbStartInCell;
19275
+ let char = " ";
19276
+ if (coverage >= 2) {
19277
+ char = "█";
19278
+ } else {
19279
+ const isLeftHalf = thumbStartInCell === virtualCellStart;
19280
+ if (isLeftHalf) {
19281
+ char = "▌";
19282
+ } else {
19283
+ char = "▐";
19284
+ }
19285
+ }
19286
+ for (let y = 0;y < this.height; y++) {
19287
+ buffer.setCellWithAlphaBlending(this.x + realX, this.y + y, char, this._foregroundColor, this._backgroundColor);
19288
+ }
19157
19289
  }
19158
- this.updateScrollOffset();
19159
- this.requestRender();
19160
- this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
19161
19290
  }
19162
- selectCurrent() {
19163
- const selected = this.getSelectedOption();
19164
- if (selected) {
19165
- this.emit("itemSelected", this._selectedIndex, selected);
19291
+ renderVertical(buffer) {
19292
+ const virtualThumbSize = this.getVirtualThumbSize();
19293
+ const virtualThumbStart = this.getVirtualThumbStart();
19294
+ const virtualThumbEnd = virtualThumbStart + virtualThumbSize;
19295
+ buffer.fillRect(this.x, this.y, this.width, this.height, this._backgroundColor);
19296
+ const realStartCell = Math.floor(virtualThumbStart / 2);
19297
+ const realEndCell = Math.ceil(virtualThumbEnd / 2) - 1;
19298
+ const startY = Math.max(0, realStartCell);
19299
+ const endY = Math.min(this.height - 1, realEndCell);
19300
+ for (let realY = startY;realY <= endY; realY++) {
19301
+ const virtualCellStart = realY * 2;
19302
+ const virtualCellEnd = virtualCellStart + 2;
19303
+ const thumbStartInCell = Math.max(virtualThumbStart, virtualCellStart);
19304
+ const thumbEndInCell = Math.min(virtualThumbEnd, virtualCellEnd);
19305
+ const coverage = thumbEndInCell - thumbStartInCell;
19306
+ let char = " ";
19307
+ if (coverage >= 2) {
19308
+ char = "█";
19309
+ } else if (coverage > 0) {
19310
+ const virtualPositionInCell = thumbStartInCell - virtualCellStart;
19311
+ if (virtualPositionInCell === 0) {
19312
+ char = "▀";
19313
+ } else {
19314
+ char = "▄";
19315
+ }
19316
+ }
19317
+ for (let x = 0;x < this.width; x++) {
19318
+ buffer.setCellWithAlphaBlending(this.x + x, this.y + realY, char, this._foregroundColor, this._backgroundColor);
19319
+ }
19166
19320
  }
19167
19321
  }
19168
- setSelectedIndex(index) {
19169
- if (index >= 0 && index < this._options.length) {
19170
- this._selectedIndex = index;
19171
- this.updateScrollOffset();
19172
- this.requestRender();
19173
- this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
19174
- }
19322
+ getVirtualThumbSize() {
19323
+ const virtualTrackSize = this.orientation === "vertical" ? this.height * 2 : this.width * 2;
19324
+ const range = this._max - this._min;
19325
+ if (range === 0)
19326
+ return virtualTrackSize;
19327
+ const viewportSize = Math.max(1, this._viewPortSize);
19328
+ const contentSize = range + viewportSize;
19329
+ if (contentSize <= viewportSize)
19330
+ return virtualTrackSize;
19331
+ const thumbRatio = viewportSize / contentSize;
19332
+ const calculatedSize = Math.floor(virtualTrackSize * thumbRatio);
19333
+ return Math.max(1, Math.min(calculatedSize, virtualTrackSize));
19334
+ }
19335
+ getVirtualThumbStart() {
19336
+ const virtualTrackSize = this.orientation === "vertical" ? this.height * 2 : this.width * 2;
19337
+ const range = this._max - this._min;
19338
+ if (range === 0)
19339
+ return 0;
19340
+ const valueRatio = (this._value - this._min) / range;
19341
+ const virtualThumbSize = this.getVirtualThumbSize();
19342
+ return Math.round(valueRatio * (virtualTrackSize - virtualThumbSize));
19175
19343
  }
19176
- updateScrollOffset() {
19177
- if (!this._options)
19178
- return;
19179
- const halfVisible = Math.floor(this.maxVisibleItems / 2);
19180
- const newScrollOffset = Math.max(0, Math.min(this._selectedIndex - halfVisible, this._options.length - this.maxVisibleItems));
19181
- if (newScrollOffset !== this.scrollOffset) {
19182
- this.scrollOffset = newScrollOffset;
19183
- this.requestRender();
19184
- }
19344
+ };
19345
+ ScrollBarRenderable = class ScrollBarRenderable extends Renderable {
19346
+ slider;
19347
+ startArrow;
19348
+ endArrow;
19349
+ orientation;
19350
+ _focusable = true;
19351
+ _scrollSize = 0;
19352
+ _scrollPosition = 0;
19353
+ _viewportSize = 0;
19354
+ _showArrows = false;
19355
+ _manualVisibility = false;
19356
+ _onChange;
19357
+ scrollStep = null;
19358
+ get visible() {
19359
+ return super.visible;
19360
+ }
19361
+ set visible(value) {
19362
+ this._manualVisibility = true;
19363
+ super.visible = value;
19364
+ }
19365
+ resetVisibilityControl() {
19366
+ this._manualVisibility = false;
19367
+ this.recalculateVisibility();
19368
+ }
19369
+ get scrollSize() {
19370
+ return this._scrollSize;
19371
+ }
19372
+ get scrollPosition() {
19373
+ return this._scrollPosition;
19374
+ }
19375
+ get viewportSize() {
19376
+ return this._viewportSize;
19377
+ }
19378
+ set scrollSize(value) {
19379
+ if (value === this.scrollSize)
19380
+ return;
19381
+ this._scrollSize = value;
19382
+ this.recalculateVisibility();
19383
+ this.updateSliderFromScrollState();
19384
+ this.scrollPosition = this.scrollPosition;
19385
+ }
19386
+ set scrollPosition(value) {
19387
+ const newPosition = Math.round(Math.min(Math.max(0, value), this.scrollSize - this.viewportSize));
19388
+ if (newPosition !== this._scrollPosition) {
19389
+ this._scrollPosition = newPosition;
19390
+ this.updateSliderFromScrollState();
19391
+ }
19392
+ }
19393
+ set viewportSize(value) {
19394
+ if (value === this.viewportSize)
19395
+ return;
19396
+ this._viewportSize = value;
19397
+ this.slider.viewPortSize = Math.max(1, this._viewportSize);
19398
+ this.recalculateVisibility();
19399
+ this.updateSliderFromScrollState();
19400
+ this.scrollPosition = this.scrollPosition;
19401
+ }
19402
+ get showArrows() {
19403
+ return this._showArrows;
19404
+ }
19405
+ set showArrows(value) {
19406
+ if (value === this._showArrows)
19407
+ return;
19408
+ this._showArrows = value;
19409
+ this.startArrow.visible = value;
19410
+ this.endArrow.visible = value;
19411
+ }
19412
+ constructor(ctx, { trackOptions, arrowOptions, orientation, showArrows = false, ...options }) {
19413
+ super(ctx, {
19414
+ flexDirection: orientation === "vertical" ? "column" : "row",
19415
+ alignSelf: "stretch",
19416
+ alignItems: "stretch",
19417
+ ...options
19418
+ });
19419
+ this._onChange = options.onChange;
19420
+ this.orientation = orientation;
19421
+ this._showArrows = showArrows;
19422
+ const scrollRange = Math.max(0, this._scrollSize - this._viewportSize);
19423
+ const defaultStepSize = Math.max(1, this._viewportSize);
19424
+ const stepSize = trackOptions?.viewPortSize ?? defaultStepSize;
19425
+ this.slider = new SliderRenderable(ctx, {
19426
+ orientation,
19427
+ min: 0,
19428
+ max: scrollRange,
19429
+ value: this._scrollPosition,
19430
+ viewPortSize: stepSize,
19431
+ onChange: (value) => {
19432
+ this._scrollPosition = Math.round(value);
19433
+ this._onChange?.(this._scrollPosition);
19434
+ this.emit("change", { position: this._scrollPosition });
19435
+ },
19436
+ ...orientation === "vertical" ? {
19437
+ width: Math.max(1, Math.min(2, this.width)),
19438
+ height: "100%",
19439
+ marginLeft: "auto"
19440
+ } : {
19441
+ width: "100%",
19442
+ height: 1,
19443
+ marginTop: "auto"
19444
+ },
19445
+ flexGrow: 1,
19446
+ flexShrink: 1,
19447
+ ...trackOptions
19448
+ });
19449
+ this.updateSliderFromScrollState();
19450
+ const arrowOpts = arrowOptions ? {
19451
+ foregroundColor: arrowOptions.backgroundColor,
19452
+ backgroundColor: arrowOptions.backgroundColor,
19453
+ attributes: arrowOptions.attributes,
19454
+ ...arrowOptions
19455
+ } : {};
19456
+ this.startArrow = new ArrowRenderable(ctx, {
19457
+ alignSelf: "center",
19458
+ visible: this.showArrows,
19459
+ direction: this.orientation === "vertical" ? "up" : "left",
19460
+ height: this.orientation === "vertical" ? 1 : 1,
19461
+ ...arrowOpts
19462
+ });
19463
+ this.endArrow = new ArrowRenderable(ctx, {
19464
+ alignSelf: "center",
19465
+ visible: this.showArrows,
19466
+ direction: this.orientation === "vertical" ? "down" : "right",
19467
+ height: this.orientation === "vertical" ? 1 : 1,
19468
+ ...arrowOpts
19469
+ });
19470
+ this.add(this.startArrow);
19471
+ this.add(this.slider);
19472
+ this.add(this.endArrow);
19473
+ let startArrowMouseTimeout = undefined;
19474
+ let endArrowMouseTimeout = undefined;
19475
+ this.startArrow.onMouseDown = (event) => {
19476
+ event.stopPropagation();
19477
+ event.preventDefault();
19478
+ this.scrollBy(-0.5, "viewport");
19479
+ startArrowMouseTimeout = setTimeout(() => {
19480
+ this.scrollBy(-0.5, "viewport");
19481
+ startArrowMouseTimeout = setInterval(() => {
19482
+ this.scrollBy(-0.2, "viewport");
19483
+ }, 200);
19484
+ }, 500);
19485
+ };
19486
+ this.startArrow.onMouseUp = (event) => {
19487
+ event.stopPropagation();
19488
+ clearInterval(startArrowMouseTimeout);
19489
+ };
19490
+ this.endArrow.onMouseDown = (event) => {
19491
+ event.stopPropagation();
19492
+ event.preventDefault();
19493
+ this.scrollBy(0.5, "viewport");
19494
+ endArrowMouseTimeout = setTimeout(() => {
19495
+ this.scrollBy(0.5, "viewport");
19496
+ endArrowMouseTimeout = setInterval(() => {
19497
+ this.scrollBy(0.2, "viewport");
19498
+ }, 200);
19499
+ }, 500);
19500
+ };
19501
+ this.endArrow.onMouseUp = (event) => {
19502
+ event.stopPropagation();
19503
+ clearInterval(endArrowMouseTimeout);
19504
+ };
19505
+ }
19506
+ set arrowOptions(options) {
19507
+ Object.assign(this.startArrow, options);
19508
+ Object.assign(this.endArrow, options);
19509
+ this.requestRender();
19510
+ }
19511
+ set trackOptions(options) {
19512
+ Object.assign(this.slider, options);
19513
+ this.requestRender();
19514
+ }
19515
+ updateSliderFromScrollState() {
19516
+ const scrollRange = Math.max(0, this._scrollSize - this._viewportSize);
19517
+ this.slider.min = 0;
19518
+ this.slider.max = scrollRange;
19519
+ this.slider.value = Math.min(this._scrollPosition, scrollRange);
19520
+ }
19521
+ scrollBy(delta, unit = "absolute") {
19522
+ const multiplier = unit === "viewport" ? this.viewportSize : unit === "content" ? this.scrollSize : unit === "step" ? this.scrollStep ?? 1 : 1;
19523
+ const resolvedDelta = multiplier * delta;
19524
+ this.scrollPosition += resolvedDelta;
19525
+ }
19526
+ recalculateVisibility() {
19527
+ if (!this._manualVisibility) {
19528
+ const sizeRatio = this.scrollSize <= this.viewportSize ? 1 : this.viewportSize / this.scrollSize;
19529
+ super.visible = sizeRatio < 1;
19530
+ }
19531
+ }
19532
+ handleKeyPress(key) {
19533
+ switch (key.name) {
19534
+ case "left":
19535
+ case "h":
19536
+ if (this.orientation !== "horizontal")
19537
+ return false;
19538
+ this.scrollBy(-1 / 5, "viewport");
19539
+ return true;
19540
+ case "right":
19541
+ case "l":
19542
+ if (this.orientation !== "horizontal")
19543
+ return false;
19544
+ this.scrollBy(1 / 5, "viewport");
19545
+ return true;
19546
+ case "up":
19547
+ case "k":
19548
+ if (this.orientation !== "vertical")
19549
+ return false;
19550
+ this.scrollBy(-1 / 5, "viewport");
19551
+ return true;
19552
+ case "down":
19553
+ case "j":
19554
+ if (this.orientation !== "vertical")
19555
+ return false;
19556
+ this.scrollBy(1 / 5, "viewport");
19557
+ return true;
19558
+ case "pageup":
19559
+ this.scrollBy(-1 / 2, "viewport");
19560
+ return true;
19561
+ case "pagedown":
19562
+ this.scrollBy(1 / 2, "viewport");
19563
+ return true;
19564
+ case "home":
19565
+ this.scrollBy(-1, "content");
19566
+ return true;
19567
+ case "end":
19568
+ this.scrollBy(1, "content");
19569
+ return true;
19570
+ }
19571
+ return false;
19572
+ }
19573
+ };
19574
+ ArrowRenderable = class ArrowRenderable extends Renderable {
19575
+ _direction;
19576
+ _foregroundColor;
19577
+ _backgroundColor;
19578
+ _attributes;
19579
+ _arrowChars;
19580
+ constructor(ctx, options) {
19581
+ super(ctx, options);
19582
+ this._direction = options.direction;
19583
+ this._foregroundColor = options.foregroundColor ? parseColor(options.foregroundColor) : RGBA.fromValues(1, 1, 1, 1);
19584
+ this._backgroundColor = options.backgroundColor ? parseColor(options.backgroundColor) : RGBA.fromValues(0, 0, 0, 0);
19585
+ this._attributes = options.attributes ?? 0;
19586
+ this._arrowChars = {
19587
+ up: "▲",
19588
+ down: "▼",
19589
+ left: "◀",
19590
+ right: "▶",
19591
+ ...options.arrowChars
19592
+ };
19593
+ if (!options.width) {
19594
+ this.width = Bun.stringWidth(this.getArrowChar());
19595
+ }
19596
+ }
19597
+ get direction() {
19598
+ return this._direction;
19599
+ }
19600
+ set direction(value) {
19601
+ if (this._direction !== value) {
19602
+ this._direction = value;
19603
+ this.requestRender();
19604
+ }
19605
+ }
19606
+ get foregroundColor() {
19607
+ return this._foregroundColor;
19608
+ }
19609
+ set foregroundColor(value) {
19610
+ if (this._foregroundColor !== value) {
19611
+ this._foregroundColor = parseColor(value);
19612
+ this.requestRender();
19613
+ }
19614
+ }
19615
+ get backgroundColor() {
19616
+ return this._backgroundColor;
19617
+ }
19618
+ set backgroundColor(value) {
19619
+ if (this._backgroundColor !== value) {
19620
+ this._backgroundColor = parseColor(value);
19621
+ this.requestRender();
19622
+ }
19623
+ }
19624
+ get attributes() {
19625
+ return this._attributes;
19626
+ }
19627
+ set attributes(value) {
19628
+ if (this._attributes !== value) {
19629
+ this._attributes = value;
19630
+ this.requestRender();
19631
+ }
19632
+ }
19633
+ set arrowChars(value) {
19634
+ this._arrowChars = {
19635
+ ...this._arrowChars,
19636
+ ...value
19637
+ };
19638
+ this.requestRender();
19639
+ }
19640
+ renderSelf(buffer) {
19641
+ const char = this.getArrowChar();
19642
+ buffer.drawText(char, this.x, this.y, this._foregroundColor, this._backgroundColor, this._attributes);
19643
+ }
19644
+ getArrowChar() {
19645
+ switch (this._direction) {
19646
+ case "up":
19647
+ return this._arrowChars.up;
19648
+ case "down":
19649
+ return this._arrowChars.down;
19650
+ case "left":
19651
+ return this._arrowChars.left;
19652
+ case "right":
19653
+ return this._arrowChars.right;
19654
+ default:
19655
+ return "?";
19656
+ }
19657
+ }
19658
+ };
19659
+ ContentRenderable = class ContentRenderable extends BoxRenderable {
19660
+ viewport;
19661
+ _viewportCulling;
19662
+ constructor(ctx, viewport, viewportCulling, options) {
19663
+ super(ctx, options);
19664
+ this.viewport = viewport;
19665
+ this._viewportCulling = viewportCulling;
19666
+ }
19667
+ get viewportCulling() {
19668
+ return this._viewportCulling;
19669
+ }
19670
+ set viewportCulling(value) {
19671
+ this._viewportCulling = value;
19672
+ }
19673
+ _getVisibleChildren() {
19674
+ if (this._viewportCulling) {
19675
+ return getObjectsInViewport(this.viewport, this.getChildrenSortedByPrimaryAxis(), this.primaryAxis, 0).map((child) => child.num);
19676
+ }
19677
+ return this.getChildrenSortedByPrimaryAxis().map((child) => child.num);
19678
+ }
19679
+ };
19680
+ ScrollBoxRenderable = class ScrollBoxRenderable extends BoxRenderable {
19681
+ static idCounter = 0;
19682
+ internalId = 0;
19683
+ wrapper;
19684
+ viewport;
19685
+ content;
19686
+ horizontalScrollBar;
19687
+ verticalScrollBar;
19688
+ _focusable = true;
19689
+ selectionListener;
19690
+ autoScrollMouseX = 0;
19691
+ autoScrollMouseY = 0;
19692
+ autoScrollThresholdVertical = 3;
19693
+ autoScrollThresholdHorizontal = 3;
19694
+ autoScrollSpeedSlow = 6;
19695
+ autoScrollSpeedMedium = 36;
19696
+ autoScrollSpeedFast = 72;
19697
+ isAutoScrolling = false;
19698
+ cachedAutoScrollSpeed = 3;
19699
+ autoScrollAccumulatorX = 0;
19700
+ autoScrollAccumulatorY = 0;
19701
+ scrollAccumulatorX = 0;
19702
+ scrollAccumulatorY = 0;
19703
+ _stickyScroll;
19704
+ _stickyScrollTop = false;
19705
+ _stickyScrollBottom = false;
19706
+ _stickyScrollLeft = false;
19707
+ _stickyScrollRight = false;
19708
+ _stickyStart;
19709
+ _hasManualScroll = false;
19710
+ _isApplyingStickyScroll = false;
19711
+ scrollAccel;
19712
+ get stickyScroll() {
19713
+ return this._stickyScroll;
19714
+ }
19715
+ set stickyScroll(value) {
19716
+ this._stickyScroll = value;
19717
+ this.updateStickyState();
19718
+ }
19719
+ get stickyStart() {
19720
+ return this._stickyStart;
19721
+ }
19722
+ set stickyStart(value) {
19723
+ this._stickyStart = value;
19724
+ this.updateStickyState();
19725
+ }
19726
+ get scrollTop() {
19727
+ return this.verticalScrollBar.scrollPosition;
19728
+ }
19729
+ set scrollTop(value) {
19730
+ this.verticalScrollBar.scrollPosition = value;
19731
+ if (!this._isApplyingStickyScroll) {
19732
+ const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
19733
+ if (!this.isAtStickyPosition() && maxScrollTop > 1) {
19734
+ this._hasManualScroll = true;
19735
+ }
19736
+ }
19737
+ this.updateStickyState();
19738
+ }
19739
+ get scrollLeft() {
19740
+ return this.horizontalScrollBar.scrollPosition;
19741
+ }
19742
+ set scrollLeft(value) {
19743
+ this.horizontalScrollBar.scrollPosition = value;
19744
+ if (!this._isApplyingStickyScroll) {
19745
+ const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
19746
+ if (!this.isAtStickyPosition() && maxScrollLeft > 1) {
19747
+ this._hasManualScroll = true;
19748
+ }
19749
+ }
19750
+ this.updateStickyState();
19751
+ }
19752
+ get scrollWidth() {
19753
+ return this.horizontalScrollBar.scrollSize;
19754
+ }
19755
+ get scrollHeight() {
19756
+ return this.verticalScrollBar.scrollSize;
19757
+ }
19758
+ updateStickyState() {
19759
+ if (!this._stickyScroll)
19760
+ return;
19761
+ const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
19762
+ const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
19763
+ if (this.scrollTop <= 0) {
19764
+ this._stickyScrollTop = true;
19765
+ this._stickyScrollBottom = false;
19766
+ } else if (this.scrollTop >= maxScrollTop) {
19767
+ this._stickyScrollTop = false;
19768
+ this._stickyScrollBottom = true;
19769
+ } else {
19770
+ this._stickyScrollTop = false;
19771
+ this._stickyScrollBottom = false;
19772
+ }
19773
+ if (this.scrollLeft <= 0) {
19774
+ this._stickyScrollLeft = true;
19775
+ this._stickyScrollRight = false;
19776
+ } else if (this.scrollLeft >= maxScrollLeft) {
19777
+ this._stickyScrollLeft = false;
19778
+ this._stickyScrollRight = true;
19779
+ } else {
19780
+ this._stickyScrollLeft = false;
19781
+ this._stickyScrollRight = false;
19782
+ }
19783
+ }
19784
+ applyStickyStart(stickyStart) {
19785
+ this._isApplyingStickyScroll = true;
19786
+ switch (stickyStart) {
19787
+ case "top":
19788
+ this._stickyScrollTop = true;
19789
+ this._stickyScrollBottom = false;
19790
+ this.verticalScrollBar.scrollPosition = 0;
19791
+ break;
19792
+ case "bottom":
19793
+ this._stickyScrollTop = false;
19794
+ this._stickyScrollBottom = true;
19795
+ this.verticalScrollBar.scrollPosition = Math.max(0, this.scrollHeight - this.viewport.height);
19796
+ break;
19797
+ case "left":
19798
+ this._stickyScrollLeft = true;
19799
+ this._stickyScrollRight = false;
19800
+ this.horizontalScrollBar.scrollPosition = 0;
19801
+ break;
19802
+ case "right":
19803
+ this._stickyScrollLeft = false;
19804
+ this._stickyScrollRight = true;
19805
+ this.horizontalScrollBar.scrollPosition = Math.max(0, this.scrollWidth - this.viewport.width);
19806
+ break;
19807
+ }
19808
+ this._isApplyingStickyScroll = false;
19809
+ }
19810
+ constructor(ctx, {
19811
+ wrapperOptions,
19812
+ viewportOptions,
19813
+ contentOptions,
19814
+ rootOptions,
19815
+ scrollbarOptions,
19816
+ verticalScrollbarOptions,
19817
+ horizontalScrollbarOptions,
19818
+ stickyScroll = false,
19819
+ stickyStart,
19820
+ scrollX = false,
19821
+ scrollY = true,
19822
+ scrollAcceleration,
19823
+ viewportCulling = true,
19824
+ ...options
19825
+ }) {
19826
+ super(ctx, {
19827
+ flexDirection: "row",
19828
+ alignItems: "stretch",
19829
+ ...options,
19830
+ ...rootOptions
19831
+ });
19832
+ this.internalId = ScrollBoxRenderable.idCounter++;
19833
+ this._stickyScroll = stickyScroll;
19834
+ this._stickyStart = stickyStart;
19835
+ this.scrollAccel = scrollAcceleration ?? new LinearScrollAccel;
19836
+ this.wrapper = new BoxRenderable(ctx, {
19837
+ flexDirection: "column",
19838
+ flexGrow: 1,
19839
+ ...wrapperOptions,
19840
+ id: `scroll-box-wrapper-${this.internalId}`
19841
+ });
19842
+ super.add(this.wrapper);
19843
+ this.viewport = new BoxRenderable(ctx, {
19844
+ flexDirection: "column",
19845
+ flexGrow: 1,
19846
+ overflow: "hidden",
19847
+ onSizeChange: () => {
19848
+ this.recalculateBarProps();
19849
+ },
19850
+ ...viewportOptions,
19851
+ id: `scroll-box-viewport-${this.internalId}`
19852
+ });
19853
+ this.wrapper.add(this.viewport);
19854
+ this.content = new ContentRenderable(ctx, this.viewport, viewportCulling, {
19855
+ alignSelf: "flex-start",
19856
+ flexShrink: 0,
19857
+ ...scrollX ? { minWidth: "100%" } : { minWidth: "100%", maxWidth: "100%" },
19858
+ ...scrollY ? { minHeight: "100%" } : { minHeight: "100%", maxHeight: "100%" },
19859
+ onSizeChange: () => {
19860
+ this.recalculateBarProps();
19861
+ },
19862
+ ...contentOptions,
19863
+ id: `scroll-box-content-${this.internalId}`
19864
+ });
19865
+ this.viewport.add(this.content);
19866
+ this.verticalScrollBar = new ScrollBarRenderable(ctx, {
19867
+ ...scrollbarOptions,
19868
+ ...verticalScrollbarOptions,
19869
+ arrowOptions: {
19870
+ ...scrollbarOptions?.arrowOptions,
19871
+ ...verticalScrollbarOptions?.arrowOptions
19872
+ },
19873
+ id: `scroll-box-vertical-scrollbar-${this.internalId}`,
19874
+ orientation: "vertical",
19875
+ onChange: (position) => {
19876
+ this.content.translateY = -position;
19877
+ if (!this._isApplyingStickyScroll) {
19878
+ const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
19879
+ if (!this.isAtStickyPosition() && maxScrollTop > 1) {
19880
+ this._hasManualScroll = true;
19881
+ }
19882
+ }
19883
+ this.updateStickyState();
19884
+ }
19885
+ });
19886
+ super.add(this.verticalScrollBar);
19887
+ this.horizontalScrollBar = new ScrollBarRenderable(ctx, {
19888
+ ...scrollbarOptions,
19889
+ ...horizontalScrollbarOptions,
19890
+ arrowOptions: {
19891
+ ...scrollbarOptions?.arrowOptions,
19892
+ ...horizontalScrollbarOptions?.arrowOptions
19893
+ },
19894
+ id: `scroll-box-horizontal-scrollbar-${this.internalId}`,
19895
+ orientation: "horizontal",
19896
+ onChange: (position) => {
19897
+ this.content.translateX = -position;
19898
+ if (!this._isApplyingStickyScroll) {
19899
+ const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
19900
+ if (!this.isAtStickyPosition() && maxScrollLeft > 1) {
19901
+ this._hasManualScroll = true;
19902
+ }
19903
+ }
19904
+ this.updateStickyState();
19905
+ }
19906
+ });
19907
+ this.wrapper.add(this.horizontalScrollBar);
19908
+ this.recalculateBarProps();
19909
+ if (stickyStart && stickyScroll) {
19910
+ this.applyStickyStart(stickyStart);
19911
+ }
19912
+ this.selectionListener = () => {
19913
+ const selection = this._ctx.getSelection();
19914
+ if (!selection || !selection.isSelecting) {
19915
+ this.stopAutoScroll();
19916
+ }
19917
+ };
19918
+ this._ctx.on("selection", this.selectionListener);
19919
+ }
19920
+ onUpdate(deltaTime) {
19921
+ this.handleAutoScroll(deltaTime);
19922
+ }
19923
+ scrollBy(delta, unit = "absolute") {
19924
+ if (typeof delta === "number") {
19925
+ this.verticalScrollBar.scrollBy(delta, unit);
19926
+ } else {
19927
+ this.verticalScrollBar.scrollBy(delta.y, unit);
19928
+ this.horizontalScrollBar.scrollBy(delta.x, unit);
19929
+ }
19930
+ }
19931
+ scrollTo(position) {
19932
+ if (typeof position === "number") {
19933
+ this.scrollTop = position;
19934
+ } else {
19935
+ this.scrollTop = position.y;
19936
+ this.scrollLeft = position.x;
19937
+ }
19938
+ }
19939
+ isAtStickyPosition() {
19940
+ if (!this._stickyScroll || !this._stickyStart) {
19941
+ return false;
19942
+ }
19943
+ const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
19944
+ const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
19945
+ switch (this._stickyStart) {
19946
+ case "top":
19947
+ return this.scrollTop === 0;
19948
+ case "bottom":
19949
+ return this.scrollTop >= maxScrollTop;
19950
+ case "left":
19951
+ return this.scrollLeft === 0;
19952
+ case "right":
19953
+ return this.scrollLeft >= maxScrollLeft;
19954
+ default:
19955
+ return false;
19956
+ }
19957
+ }
19958
+ add(obj, index) {
19959
+ return this.content.add(obj, index);
19960
+ }
19961
+ insertBefore(obj, anchor) {
19962
+ return this.content.insertBefore(obj, anchor);
19963
+ }
19964
+ remove(id) {
19965
+ this.content.remove(id);
19966
+ }
19967
+ getChildren() {
19968
+ return this.content.getChildren();
19969
+ }
19970
+ onMouseEvent(event) {
19971
+ if (event.type === "scroll") {
19972
+ let dir = event.scroll?.direction;
19973
+ if (event.modifiers.shift)
19974
+ dir = dir === "up" ? "left" : dir === "down" ? "right" : dir === "right" ? "down" : "up";
19975
+ const baseDelta = event.scroll?.delta ?? 0;
19976
+ const now = Date.now();
19977
+ const multiplier = this.scrollAccel.tick(now);
19978
+ const scrollAmount = baseDelta * multiplier;
19979
+ if (dir === "up") {
19980
+ this.scrollAccumulatorY -= scrollAmount;
19981
+ const integerScroll = Math.trunc(this.scrollAccumulatorY);
19982
+ if (integerScroll !== 0) {
19983
+ this.scrollTop += integerScroll;
19984
+ this.scrollAccumulatorY -= integerScroll;
19985
+ }
19986
+ } else if (dir === "down") {
19987
+ this.scrollAccumulatorY += scrollAmount;
19988
+ const integerScroll = Math.trunc(this.scrollAccumulatorY);
19989
+ if (integerScroll !== 0) {
19990
+ this.scrollTop += integerScroll;
19991
+ this.scrollAccumulatorY -= integerScroll;
19992
+ }
19993
+ } else if (dir === "left") {
19994
+ this.scrollAccumulatorX -= scrollAmount;
19995
+ const integerScroll = Math.trunc(this.scrollAccumulatorX);
19996
+ if (integerScroll !== 0) {
19997
+ this.scrollLeft += integerScroll;
19998
+ this.scrollAccumulatorX -= integerScroll;
19999
+ }
20000
+ } else if (dir === "right") {
20001
+ this.scrollAccumulatorX += scrollAmount;
20002
+ const integerScroll = Math.trunc(this.scrollAccumulatorX);
20003
+ if (integerScroll !== 0) {
20004
+ this.scrollLeft += integerScroll;
20005
+ this.scrollAccumulatorX -= integerScroll;
20006
+ }
20007
+ }
20008
+ const maxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
20009
+ const maxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
20010
+ if (maxScrollTop > 1 || maxScrollLeft > 1) {
20011
+ this._hasManualScroll = true;
20012
+ }
20013
+ }
20014
+ if (event.type === "drag" && event.isSelecting) {
20015
+ this.updateAutoScroll(event.x, event.y);
20016
+ } else if (event.type === "up") {
20017
+ this.stopAutoScroll();
20018
+ }
20019
+ }
20020
+ handleKeyPress(key) {
20021
+ if (this.verticalScrollBar.handleKeyPress(key)) {
20022
+ this._hasManualScroll = true;
20023
+ this.scrollAccel.reset();
20024
+ this.resetScrollAccumulators();
20025
+ return true;
20026
+ }
20027
+ if (this.horizontalScrollBar.handleKeyPress(key)) {
20028
+ this._hasManualScroll = true;
20029
+ this.scrollAccel.reset();
20030
+ this.resetScrollAccumulators();
20031
+ return true;
20032
+ }
20033
+ return false;
20034
+ }
20035
+ resetScrollAccumulators() {
20036
+ this.scrollAccumulatorX = 0;
20037
+ this.scrollAccumulatorY = 0;
20038
+ }
20039
+ startAutoScroll(mouseX, mouseY) {
20040
+ this.stopAutoScroll();
20041
+ this.autoScrollMouseX = mouseX;
20042
+ this.autoScrollMouseY = mouseY;
20043
+ this.cachedAutoScrollSpeed = this.getAutoScrollSpeed(mouseX, mouseY);
20044
+ this.isAutoScrolling = true;
20045
+ if (!this.live) {
20046
+ this.live = true;
20047
+ }
20048
+ }
20049
+ updateAutoScroll(mouseX, mouseY) {
20050
+ this.autoScrollMouseX = mouseX;
20051
+ this.autoScrollMouseY = mouseY;
20052
+ this.cachedAutoScrollSpeed = this.getAutoScrollSpeed(mouseX, mouseY);
20053
+ const scrollX = this.getAutoScrollDirectionX(mouseX);
20054
+ const scrollY = this.getAutoScrollDirectionY(mouseY);
20055
+ if (scrollX === 0 && scrollY === 0) {
20056
+ this.stopAutoScroll();
20057
+ } else if (!this.isAutoScrolling) {
20058
+ this.startAutoScroll(mouseX, mouseY);
20059
+ }
20060
+ }
20061
+ stopAutoScroll() {
20062
+ const wasAutoScrolling = this.isAutoScrolling;
20063
+ this.isAutoScrolling = false;
20064
+ this.autoScrollAccumulatorX = 0;
20065
+ this.autoScrollAccumulatorY = 0;
20066
+ if (wasAutoScrolling && !this.hasOtherLiveReasons()) {
20067
+ this.live = false;
20068
+ }
20069
+ }
20070
+ hasOtherLiveReasons() {
20071
+ return false;
20072
+ }
20073
+ handleAutoScroll(deltaTime) {
20074
+ if (!this.isAutoScrolling)
20075
+ return;
20076
+ const scrollX = this.getAutoScrollDirectionX(this.autoScrollMouseX);
20077
+ const scrollY = this.getAutoScrollDirectionY(this.autoScrollMouseY);
20078
+ const scrollAmount = this.cachedAutoScrollSpeed * (deltaTime / 1000);
20079
+ let scrolled = false;
20080
+ if (scrollX !== 0) {
20081
+ this.autoScrollAccumulatorX += scrollX * scrollAmount;
20082
+ const integerScrollX = Math.trunc(this.autoScrollAccumulatorX);
20083
+ if (integerScrollX !== 0) {
20084
+ this.scrollLeft += integerScrollX;
20085
+ this.autoScrollAccumulatorX -= integerScrollX;
20086
+ scrolled = true;
20087
+ }
20088
+ }
20089
+ if (scrollY !== 0) {
20090
+ this.autoScrollAccumulatorY += scrollY * scrollAmount;
20091
+ const integerScrollY = Math.trunc(this.autoScrollAccumulatorY);
20092
+ if (integerScrollY !== 0) {
20093
+ this.scrollTop += integerScrollY;
20094
+ this.autoScrollAccumulatorY -= integerScrollY;
20095
+ scrolled = true;
20096
+ }
20097
+ }
20098
+ if (scrolled) {
20099
+ this._ctx.requestSelectionUpdate();
20100
+ }
20101
+ if (scrollX === 0 && scrollY === 0) {
20102
+ this.stopAutoScroll();
20103
+ }
20104
+ }
20105
+ getAutoScrollDirectionX(mouseX) {
20106
+ const relativeX = mouseX - this.x;
20107
+ const distToLeft = relativeX;
20108
+ const distToRight = this.width - relativeX;
20109
+ if (distToLeft <= this.autoScrollThresholdHorizontal) {
20110
+ return this.scrollLeft > 0 ? -1 : 0;
20111
+ } else if (distToRight <= this.autoScrollThresholdHorizontal) {
20112
+ const maxScrollLeft = this.scrollWidth - this.viewport.width;
20113
+ return this.scrollLeft < maxScrollLeft ? 1 : 0;
20114
+ }
20115
+ return 0;
20116
+ }
20117
+ getAutoScrollDirectionY(mouseY) {
20118
+ const relativeY = mouseY - this.y;
20119
+ const distToTop = relativeY;
20120
+ const distToBottom = this.height - relativeY;
20121
+ if (distToTop <= this.autoScrollThresholdVertical) {
20122
+ return this.scrollTop > 0 ? -1 : 0;
20123
+ } else if (distToBottom <= this.autoScrollThresholdVertical) {
20124
+ const maxScrollTop = this.scrollHeight - this.viewport.height;
20125
+ return this.scrollTop < maxScrollTop ? 1 : 0;
20126
+ }
20127
+ return 0;
20128
+ }
20129
+ getAutoScrollSpeed(mouseX, mouseY) {
20130
+ const relativeX = mouseX - this.x;
20131
+ const relativeY = mouseY - this.y;
20132
+ const distToLeft = relativeX;
20133
+ const distToRight = this.width - relativeX;
20134
+ const distToTop = relativeY;
20135
+ const distToBottom = this.height - relativeY;
20136
+ const minDistance = Math.min(distToLeft, distToRight, distToTop, distToBottom);
20137
+ if (minDistance <= 1) {
20138
+ return this.autoScrollSpeedFast;
20139
+ } else if (minDistance <= 2) {
20140
+ return this.autoScrollSpeedMedium;
20141
+ } else {
20142
+ return this.autoScrollSpeedSlow;
20143
+ }
20144
+ }
20145
+ recalculateBarProps() {
20146
+ const wasApplyingStickyScroll = this._isApplyingStickyScroll;
20147
+ this._isApplyingStickyScroll = true;
20148
+ this.verticalScrollBar.scrollSize = this.content.height;
20149
+ this.verticalScrollBar.viewportSize = this.viewport.height;
20150
+ this.horizontalScrollBar.scrollSize = this.content.width;
20151
+ this.horizontalScrollBar.viewportSize = this.viewport.width;
20152
+ if (this._stickyScroll) {
20153
+ const newMaxScrollTop = Math.max(0, this.scrollHeight - this.viewport.height);
20154
+ const newMaxScrollLeft = Math.max(0, this.scrollWidth - this.viewport.width);
20155
+ if (this._stickyStart && !this._hasManualScroll) {
20156
+ this.applyStickyStart(this._stickyStart);
20157
+ } else {
20158
+ if (this._stickyScrollTop) {
20159
+ this.scrollTop = 0;
20160
+ } else if (this._stickyScrollBottom && newMaxScrollTop > 0) {
20161
+ this.scrollTop = newMaxScrollTop;
20162
+ }
20163
+ if (this._stickyScrollLeft) {
20164
+ this.scrollLeft = 0;
20165
+ } else if (this._stickyScrollRight && newMaxScrollLeft > 0) {
20166
+ this.scrollLeft = newMaxScrollLeft;
20167
+ }
20168
+ }
20169
+ }
20170
+ this._isApplyingStickyScroll = wasApplyingStickyScroll;
20171
+ process.nextTick(() => {
20172
+ this.requestRender();
20173
+ });
20174
+ }
20175
+ set rootOptions(options) {
20176
+ Object.assign(this, options);
20177
+ this.requestRender();
20178
+ }
20179
+ set wrapperOptions(options) {
20180
+ Object.assign(this.wrapper, options);
20181
+ this.requestRender();
20182
+ }
20183
+ set viewportOptions(options) {
20184
+ Object.assign(this.viewport, options);
20185
+ this.requestRender();
20186
+ }
20187
+ set contentOptions(options) {
20188
+ Object.assign(this.content, options);
20189
+ this.requestRender();
20190
+ }
20191
+ set scrollbarOptions(options) {
20192
+ Object.assign(this.verticalScrollBar, options);
20193
+ Object.assign(this.horizontalScrollBar, options);
20194
+ this.requestRender();
20195
+ }
20196
+ set verticalScrollbarOptions(options) {
20197
+ Object.assign(this.verticalScrollBar, options);
20198
+ this.requestRender();
20199
+ }
20200
+ set horizontalScrollbarOptions(options) {
20201
+ Object.assign(this.horizontalScrollBar, options);
20202
+ this.requestRender();
20203
+ }
20204
+ get scrollAcceleration() {
20205
+ return this.scrollAccel;
20206
+ }
20207
+ set scrollAcceleration(value) {
20208
+ this.scrollAccel = value;
20209
+ }
20210
+ get viewportCulling() {
20211
+ return this.content.viewportCulling;
20212
+ }
20213
+ set viewportCulling(value) {
20214
+ this.content.viewportCulling = value;
20215
+ this.requestRender();
20216
+ }
20217
+ destroySelf() {
20218
+ if (this.selectionListener) {
20219
+ this._ctx.off("selection", this.selectionListener);
20220
+ this.selectionListener = undefined;
20221
+ }
20222
+ super.destroySelf();
20223
+ }
20224
+ };
20225
+ defaultSelectKeybindings = [
20226
+ { name: "up", action: "move-up" },
20227
+ { name: "k", action: "move-up" },
20228
+ { name: "down", action: "move-down" },
20229
+ { name: "j", action: "move-down" },
20230
+ { name: "up", shift: true, action: "move-up-fast" },
20231
+ { name: "down", shift: true, action: "move-down-fast" },
20232
+ { name: "return", action: "select-current" },
20233
+ { name: "linefeed", action: "select-current" }
20234
+ ];
20235
+ ((SelectRenderableEvents2) => {
20236
+ SelectRenderableEvents2["SELECTION_CHANGED"] = "selectionChanged";
20237
+ SelectRenderableEvents2["ITEM_SELECTED"] = "itemSelected";
20238
+ })(SelectRenderableEvents ||= {});
20239
+ SelectRenderable = class SelectRenderable extends Renderable {
20240
+ _focusable = true;
20241
+ _options = [];
20242
+ _selectedIndex = 0;
20243
+ scrollOffset = 0;
20244
+ maxVisibleItems;
20245
+ _backgroundColor;
20246
+ _textColor;
20247
+ _focusedBackgroundColor;
20248
+ _focusedTextColor;
20249
+ _selectedBackgroundColor;
20250
+ _selectedTextColor;
20251
+ _descriptionColor;
20252
+ _selectedDescriptionColor;
20253
+ _showScrollIndicator;
20254
+ _wrapSelection;
20255
+ _showDescription;
20256
+ _font;
20257
+ _itemSpacing;
20258
+ linesPerItem;
20259
+ fontHeight;
20260
+ _fastScrollStep;
20261
+ _keyBindingsMap;
20262
+ _keyAliasMap;
20263
+ _keyBindings;
20264
+ _defaultOptions = {
20265
+ backgroundColor: "transparent",
20266
+ textColor: "#FFFFFF",
20267
+ focusedBackgroundColor: "#1a1a1a",
20268
+ focusedTextColor: "#FFFFFF",
20269
+ selectedBackgroundColor: "#334455",
20270
+ selectedTextColor: "#FFFF00",
20271
+ selectedIndex: 0,
20272
+ descriptionColor: "#888888",
20273
+ selectedDescriptionColor: "#CCCCCC",
20274
+ showScrollIndicator: false,
20275
+ wrapSelection: false,
20276
+ showDescription: true,
20277
+ itemSpacing: 0,
20278
+ fastScrollStep: 5
20279
+ };
20280
+ constructor(ctx, options) {
20281
+ super(ctx, { ...options, buffered: true });
20282
+ this._options = options.options || [];
20283
+ const requestedIndex = options.selectedIndex ?? this._defaultOptions.selectedIndex;
20284
+ this._selectedIndex = this._options.length > 0 ? Math.min(requestedIndex, this._options.length - 1) : 0;
20285
+ this._backgroundColor = parseColor(options.backgroundColor || this._defaultOptions.backgroundColor);
20286
+ this._textColor = parseColor(options.textColor || this._defaultOptions.textColor);
20287
+ this._focusedBackgroundColor = parseColor(options.focusedBackgroundColor || this._defaultOptions.focusedBackgroundColor);
20288
+ this._focusedTextColor = parseColor(options.focusedTextColor || this._defaultOptions.focusedTextColor);
20289
+ this._showScrollIndicator = options.showScrollIndicator ?? this._defaultOptions.showScrollIndicator;
20290
+ this._wrapSelection = options.wrapSelection ?? this._defaultOptions.wrapSelection;
20291
+ this._showDescription = options.showDescription ?? this._defaultOptions.showDescription;
20292
+ this._font = options.font;
20293
+ this._itemSpacing = options.itemSpacing || this._defaultOptions.itemSpacing;
20294
+ this.fontHeight = this._font ? measureText({ text: "A", font: this._font }).height : 1;
20295
+ this.linesPerItem = this._showDescription ? this._font ? this.fontHeight + 1 : 2 : this._font ? this.fontHeight : 1;
20296
+ this.linesPerItem += this._itemSpacing;
20297
+ this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem));
20298
+ this._selectedBackgroundColor = parseColor(options.selectedBackgroundColor || this._defaultOptions.selectedBackgroundColor);
20299
+ this._selectedTextColor = parseColor(options.selectedTextColor || this._defaultOptions.selectedTextColor);
20300
+ this._descriptionColor = parseColor(options.descriptionColor || this._defaultOptions.descriptionColor);
20301
+ this._selectedDescriptionColor = parseColor(options.selectedDescriptionColor || this._defaultOptions.selectedDescriptionColor);
20302
+ this._fastScrollStep = options.fastScrollStep || this._defaultOptions.fastScrollStep;
20303
+ this._keyAliasMap = mergeKeyAliases(defaultKeyAliases, options.keyAliasMap || {});
20304
+ this._keyBindings = options.keyBindings || [];
20305
+ const mergedBindings = mergeKeyBindings(defaultSelectKeybindings, this._keyBindings);
20306
+ this._keyBindingsMap = buildKeyBindingsMap(mergedBindings, this._keyAliasMap);
20307
+ this.requestRender();
20308
+ }
20309
+ renderSelf(buffer, deltaTime) {
20310
+ if (!this.visible || !this.frameBuffer)
20311
+ return;
20312
+ if (this.isDirty) {
20313
+ this.refreshFrameBuffer();
20314
+ }
20315
+ }
20316
+ refreshFrameBuffer() {
20317
+ if (!this.frameBuffer || this._options.length === 0)
20318
+ return;
20319
+ const bgColor = this._focused ? this._focusedBackgroundColor : this._backgroundColor;
20320
+ this.frameBuffer.clear(bgColor);
20321
+ const contentX = 0;
20322
+ const contentY = 0;
20323
+ const contentWidth = this.width;
20324
+ const contentHeight = this.height;
20325
+ const visibleOptions = this._options.slice(this.scrollOffset, this.scrollOffset + this.maxVisibleItems);
20326
+ for (let i = 0;i < visibleOptions.length; i++) {
20327
+ const actualIndex = this.scrollOffset + i;
20328
+ const option = visibleOptions[i];
20329
+ const isSelected = actualIndex === this._selectedIndex;
20330
+ const itemY = contentY + i * this.linesPerItem;
20331
+ if (itemY + this.linesPerItem - 1 >= contentY + contentHeight)
20332
+ break;
20333
+ if (isSelected) {
20334
+ const contentHeight2 = this.linesPerItem - this._itemSpacing;
20335
+ this.frameBuffer.fillRect(contentX, itemY, contentWidth, contentHeight2, this._selectedBackgroundColor);
20336
+ }
20337
+ const nameContent = `${isSelected ? "▶ " : " "}${option.name}`;
20338
+ const baseTextColor = this._focused ? this._focusedTextColor : this._textColor;
20339
+ const nameColor = isSelected ? this._selectedTextColor : baseTextColor;
20340
+ let descX = contentX + 3;
20341
+ if (this._font) {
20342
+ const indicator = isSelected ? "▶ " : " ";
20343
+ this.frameBuffer.drawText(indicator, contentX + 1, itemY, nameColor);
20344
+ const indicatorWidth = 2;
20345
+ renderFontToFrameBuffer(this.frameBuffer, {
20346
+ text: option.name,
20347
+ x: contentX + 1 + indicatorWidth,
20348
+ y: itemY,
20349
+ color: nameColor,
20350
+ backgroundColor: isSelected ? this._selectedBackgroundColor : bgColor,
20351
+ font: this._font
20352
+ });
20353
+ descX = contentX + 1 + indicatorWidth;
20354
+ } else {
20355
+ this.frameBuffer.drawText(nameContent, contentX + 1, itemY, nameColor);
20356
+ }
20357
+ if (this._showDescription && itemY + this.fontHeight < contentY + contentHeight) {
20358
+ const descColor = isSelected ? this._selectedDescriptionColor : this._descriptionColor;
20359
+ this.frameBuffer.drawText(option.description, descX, itemY + this.fontHeight, descColor);
20360
+ }
20361
+ }
20362
+ if (this._showScrollIndicator && this._options.length > this.maxVisibleItems) {
20363
+ this.renderScrollIndicatorToFrameBuffer(contentX, contentY, contentWidth, contentHeight);
20364
+ }
20365
+ }
20366
+ renderScrollIndicatorToFrameBuffer(contentX, contentY, contentWidth, contentHeight) {
20367
+ if (!this.frameBuffer)
20368
+ return;
20369
+ const scrollPercent = this._selectedIndex / Math.max(1, this._options.length - 1);
20370
+ const indicatorHeight = Math.max(1, contentHeight - 2);
20371
+ const indicatorY = contentY + 1 + Math.floor(scrollPercent * indicatorHeight);
20372
+ const indicatorX = contentX + contentWidth - 1;
20373
+ this.frameBuffer.drawText("█", indicatorX, indicatorY, parseColor("#666666"));
20374
+ }
20375
+ get options() {
20376
+ return this._options;
20377
+ }
20378
+ set options(options) {
20379
+ this._options = options;
20380
+ this._selectedIndex = Math.min(this._selectedIndex, Math.max(0, options.length - 1));
20381
+ this.updateScrollOffset();
20382
+ this.requestRender();
20383
+ }
20384
+ getSelectedOption() {
20385
+ return this._options[this._selectedIndex] || null;
20386
+ }
20387
+ getSelectedIndex() {
20388
+ return this._selectedIndex;
20389
+ }
20390
+ moveUp(steps = 1) {
20391
+ const newIndex = this._selectedIndex - steps;
20392
+ if (newIndex >= 0) {
20393
+ this._selectedIndex = newIndex;
20394
+ } else if (this._wrapSelection && this._options.length > 0) {
20395
+ this._selectedIndex = this._options.length - 1;
20396
+ } else {
20397
+ this._selectedIndex = 0;
20398
+ }
20399
+ this.updateScrollOffset();
20400
+ this.requestRender();
20401
+ this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
20402
+ }
20403
+ moveDown(steps = 1) {
20404
+ const newIndex = this._selectedIndex + steps;
20405
+ if (newIndex < this._options.length) {
20406
+ this._selectedIndex = newIndex;
20407
+ } else if (this._wrapSelection && this._options.length > 0) {
20408
+ this._selectedIndex = 0;
20409
+ } else {
20410
+ this._selectedIndex = this._options.length - 1;
20411
+ }
20412
+ this.updateScrollOffset();
20413
+ this.requestRender();
20414
+ this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
20415
+ }
20416
+ selectCurrent() {
20417
+ const selected = this.getSelectedOption();
20418
+ if (selected) {
20419
+ this.emit("itemSelected", this._selectedIndex, selected);
20420
+ }
20421
+ }
20422
+ setSelectedIndex(index) {
20423
+ if (index >= 0 && index < this._options.length) {
20424
+ this._selectedIndex = index;
20425
+ this.updateScrollOffset();
20426
+ this.requestRender();
20427
+ this.emit("selectionChanged", this._selectedIndex, this.getSelectedOption());
20428
+ }
20429
+ }
20430
+ updateScrollOffset() {
20431
+ if (!this._options)
20432
+ return;
20433
+ const halfVisible = Math.floor(this.maxVisibleItems / 2);
20434
+ const newScrollOffset = Math.max(0, Math.min(this._selectedIndex - halfVisible, this._options.length - this.maxVisibleItems));
20435
+ if (newScrollOffset !== this.scrollOffset) {
20436
+ this.scrollOffset = newScrollOffset;
20437
+ this.requestRender();
20438
+ }
19185
20439
  }
19186
20440
  onResize(width, height) {
19187
20441
  this.maxVisibleItems = Math.max(1, Math.floor(height / this.linesPerItem));
@@ -20339,11 +21593,21 @@ function getZenApiType2(modelId) {
20339
21593
  }
20340
21594
  return "openai-compatible";
20341
21595
  }
20342
- function buildSystemPrompt2(cwd, history, shellInfo) {
21596
+ function buildSystemPrompt2(cwd, history, shellInfo, repoContextEnabled) {
20343
21597
  const historyContext = formatHistory2(history);
20344
21598
  const platformPaths = getPlatformPaths(shellInfo.platform);
20345
21599
  const shellHints = getShellSyntaxHints(shellInfo.shell);
20346
21600
  const platformName = shellInfo.platform === "macos" ? "macOS" : shellInfo.platform === "windows" ? "Windows" : shellInfo.platform === "linux" ? shellInfo.isWSL ? "Linux (WSL)" : "Linux" : "Unknown";
21601
+ let projectContextSection = "";
21602
+ if (repoContextEnabled) {
21603
+ const repoContext = detectRepoContext(cwd);
21604
+ if (repoContext) {
21605
+ projectContextSection = `
21606
+ Project context:
21607
+ ${formatRepoContext(repoContext)}
21608
+ `;
21609
+ }
21610
+ }
20347
21611
  return `You are a shell command translator. Convert the user's natural language request into a shell command.
20348
21612
 
20349
21613
  Current environment:
@@ -20352,7 +21616,7 @@ Current environment:
20352
21616
  - Working directory: ${cwd}
20353
21617
  - Home directory: ${shellInfo.homeDir}
20354
21618
  ${shellInfo.terminalEmulator ? `- Terminal: ${shellInfo.terminalEmulator}` : ""}
20355
-
21619
+ ${projectContextSection}
20356
21620
  ${shellHints}
20357
21621
 
20358
21622
  Recent command history:
@@ -20363,7 +21627,8 @@ Rules:
20363
21627
  - No explanations, no markdown, no backticks, no code blocks
20364
21628
  - Use the correct syntax for the detected shell (${shellInfo.shell})
20365
21629
  - If the request is unclear, make a reasonable assumption
20366
- - Prefer simple, common commands over complex one-liners
21630
+ - Prefer simple, common commands over complex one-liners${repoContextEnabled ? `
21631
+ - Use project-specific commands when relevant (e.g., use the detected package manager and available scripts)` : ""}
20367
21632
  - Use the command history for context (e.g., "do that again", "undo", "delete the file I just created")
20368
21633
  - If the user asks something that can't be done with a shell command, output a command that prints a helpful message
20369
21634
  - For file operations, prefer safer alternatives when possible
@@ -20635,9 +21900,9 @@ function getShellInfo2() {
20635
21900
  }
20636
21901
  return cachedShellInfo2;
20637
21902
  }
20638
- async function translateToCommand2(apiKey, model, userInput, cwd, history = []) {
21903
+ async function translateToCommand2(apiKey, model, userInput, cwd, history = [], repoContextEnabled) {
20639
21904
  const shellInfo = getShellInfo2();
20640
- const systemPrompt = buildSystemPrompt2(cwd, history, shellInfo);
21905
+ const systemPrompt = buildSystemPrompt2(cwd, history, shellInfo, repoContextEnabled);
20641
21906
  let rawCommand;
20642
21907
  if (model.provider === "openrouter") {
20643
21908
  rawCommand = await callOpenRouter2(apiKey, model.id, systemPrompt, userInput);
@@ -20663,6 +21928,7 @@ async function translateToCommand2(apiKey, model, userInput, cwd, history = [])
20663
21928
  var DEBUG_API2, cachedShellInfo2 = null;
20664
21929
  var init_api = __esm(() => {
20665
21930
  init_shell();
21931
+ init_repo_context();
20666
21932
  DEBUG_API2 = process.env.DEBUG_API === "1";
20667
21933
  });
20668
21934
 
@@ -20870,6 +22136,9 @@ __export(exports_cli, {
20870
22136
  });
20871
22137
  import { spawn } from "child_process";
20872
22138
  import { cwd as getCwd } from "process";
22139
+ function generateMessageId() {
22140
+ return `msg-${++messageIdCounter}`;
22141
+ }
20873
22142
  async function main2() {
20874
22143
  config = loadConfig2();
20875
22144
  history = loadHistory2();
@@ -21047,44 +22316,65 @@ function createMainUI() {
21047
22316
  id: "header-row",
21048
22317
  flexDirection: "row",
21049
22318
  width: "100%",
22319
+ alignItems: "center",
21050
22320
  marginBottom: 1
21051
22321
  });
21052
22322
  mainContainer.add(headerRow);
21053
22323
  headerText = new TextRenderable(renderer, {
21054
22324
  id: "header-text",
21055
- content: t`${bold(fg(theme.colors.primary)("magic-shell"))} ${fg(theme.colors.textMuted)("- natural language to terminal commands")}`,
22325
+ content: t`${bold(fg(theme.colors.primary)("magic-shell"))}`,
21056
22326
  flexGrow: 1
21057
22327
  });
21058
22328
  headerRow.add(headerText);
21059
- const statusRow = new BoxRenderable(renderer, {
21060
- id: "status-row",
21061
- flexDirection: "row",
21062
- width: "100%",
21063
- marginBottom: 1
22329
+ const modelBadge = new TextRenderable(renderer, {
22330
+ id: "model-badge",
22331
+ content: getModelDisplay()
21064
22332
  });
21065
- mainContainer.add(statusRow);
21066
- cwdText = new TextRenderable(renderer, {
21067
- id: "cwd-text",
21068
- content: t`${fg(theme.colors.textMuted)("cwd:")} ${fg(theme.colors.success)(currentCwd)}`,
21069
- flexGrow: 1
22333
+ headerRow.add(modelBadge);
22334
+ statusBarText = new TextRenderable(renderer, {
22335
+ id: "status-bar-text",
22336
+ content: getStatusBarContent(),
22337
+ marginBottom: 1
21070
22338
  });
21071
- statusRow.add(cwdText);
21072
- modelText = new TextRenderable(renderer, {
21073
- id: "model-text",
21074
- content: getModelDisplay()
22339
+ mainContainer.add(statusBarText);
22340
+ chatScrollBox = new ScrollBoxRenderable(renderer, {
22341
+ id: "chat-scroll-box",
22342
+ flexGrow: 1,
22343
+ width: "100%",
22344
+ scrollY: true,
22345
+ scrollX: false,
22346
+ stickyScroll: true,
22347
+ stickyStart: "bottom",
22348
+ rootOptions: {
22349
+ border: true,
22350
+ borderColor: theme.colors.border,
22351
+ borderStyle: "single"
22352
+ },
22353
+ viewportOptions: {
22354
+ backgroundColor: theme.colors.background,
22355
+ paddingLeft: 1,
22356
+ paddingRight: 1,
22357
+ paddingTop: 1
22358
+ },
22359
+ contentOptions: {
22360
+ flexDirection: "column",
22361
+ gap: 1
22362
+ }
21075
22363
  });
21076
- statusRow.add(modelText);
22364
+ mainContainer.add(chatScrollBox);
22365
+ addSystemMessage(getWelcomeMessage());
21077
22366
  const inputRow = new BoxRenderable(renderer, {
21078
22367
  id: "input-row",
21079
22368
  flexDirection: "row",
21080
22369
  width: "100%",
21081
- marginBottom: 1
22370
+ marginTop: 1,
22371
+ alignItems: "center"
21082
22372
  });
21083
22373
  mainContainer.add(inputRow);
21084
22374
  const promptText = new TextRenderable(renderer, {
21085
22375
  id: "prompt-text",
21086
- content: t`${fg(theme.colors.success)(">")} `,
21087
- width: 2
22376
+ content: t`${fg(theme.colors.primary)("~>")} `,
22377
+ width: 3
21088
22378
  });
21089
22379
  inputRow.add(promptText);
21090
22380
  inputField = new InputRenderable(renderer, {
@@ -21102,58 +22392,269 @@ function createMainUI() {
21102
22392
  }
21103
22393
  });
21104
22394
  inputRow.add(inputField);
21105
- commandPreview = new TextRenderable(renderer, {
21106
- id: "command-preview",
21107
- content: "",
21108
- marginBottom: 1
21109
- });
21110
- mainContainer.add(commandPreview);
21111
- safetyWarning = new TextRenderable(renderer, {
21112
- id: "safety-warning",
21113
- content: ""
22395
+ helpBarText = new TextRenderable(renderer, {
22396
+ id: "help-bar-text",
22397
+ content: getHelpBarContent(),
22398
+ marginTop: 1
21114
22399
  });
21115
- mainContainer.add(safetyWarning);
21116
- confirmPrompt = new BoxRenderable(renderer, {
21117
- id: "confirm-prompt",
22400
+ mainContainer.add(helpBarText);
22401
+ inputField.on(InputRenderableEvents.ENTER, handleInput);
22402
+ renderer.keyInput.on("keypress", handleKeypress);
22403
+ inputField.focus();
22404
+ }
22405
+ function getStatusBarContent() {
22406
+ const theme = getTheme2();
22407
+ const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
22408
+ const safeModeIndicator = dryRunMode ? fg(theme.colors.warning)("[DRY RUN]") : fg(theme.colors.success)("Safe");
22409
+ const repoContextIndicator = config.repoContext ? fg(theme.colors.info)("[Repo]") : "";
22410
+ 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}`;
22411
+ }
22412
+ function getHelpBarContent() {
22413
+ const theme = getTheme2();
22414
+ if (awaitingConfirmation) {
22415
+ 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")}`;
22416
+ }
22417
+ 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")}`;
22418
+ }
22419
+ function getWelcomeMessage() {
22420
+ const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
22421
+ const freeNote = config.provider === "opencode-zen" ? `
22422
+ Free models: grok-code, glm-4.7-free` : "";
22423
+ return `Ready. Using ${providerName}.${freeNote}
22424
+ Type what you want to do, or press Ctrl+X P for command palette.`;
22425
+ }
22426
+ function addSystemMessage(content) {
22427
+ const msg = {
22428
+ id: generateMessageId(),
22429
+ type: "system",
22430
+ content,
22431
+ timestamp: Date.now()
22432
+ };
22433
+ chatMessages.push(msg);
22434
+ renderMessage(msg);
22435
+ return msg;
22436
+ }
22437
+ function addUserMessage(content) {
22438
+ const msg = {
22439
+ id: generateMessageId(),
22440
+ type: "user",
22441
+ content,
22442
+ timestamp: Date.now()
22443
+ };
22444
+ chatMessages.push(msg);
22445
+ renderMessage(msg);
22446
+ return msg;
22447
+ }
22448
+ function addAssistantMessage(content, command, safety) {
22449
+ const msg = {
22450
+ id: generateMessageId(),
22451
+ type: "assistant",
22452
+ content,
22453
+ command,
22454
+ safety,
22455
+ timestamp: Date.now(),
22456
+ executed: false
22457
+ };
22458
+ chatMessages.push(msg);
22459
+ renderMessage(msg);
22460
+ return msg;
22461
+ }
22462
+ function addResultMessage(content, exitCode) {
22463
+ const msg = {
22464
+ id: generateMessageId(),
22465
+ type: "result",
22466
+ content,
22467
+ timestamp: Date.now(),
22468
+ exitCode
22469
+ };
22470
+ chatMessages.push(msg);
22471
+ renderMessage(msg);
22472
+ return msg;
22473
+ }
22474
+ function renderMessage(msg) {
22475
+ const theme = getTheme2();
22476
+ const msgBox = createMessageRenderable(msg, theme);
22477
+ chatScrollBox.add(msgBox);
22478
+ }
22479
+ function createMessageRenderable(msg, theme) {
22480
+ switch (msg.type) {
22481
+ case "user":
22482
+ return createUserMessageRenderable(msg, theme);
22483
+ case "assistant":
22484
+ return createAssistantMessageRenderable(msg, theme);
22485
+ case "result":
22486
+ return createResultMessageRenderable(msg, theme);
22487
+ case "system":
22488
+ default:
22489
+ return createSystemMessageRenderable(msg, theme);
22490
+ }
22491
+ }
22492
+ function createUserMessageRenderable(msg, theme) {
22493
+ const box = new BoxRenderable(renderer, {
22494
+ id: `msg-${msg.id}`,
21118
22495
  flexDirection: "row",
21119
- visible: false,
21120
- marginBottom: 1
22496
+ width: "100%"
21121
22497
  });
21122
- mainContainer.add(confirmPrompt);
21123
- const confirmText = new TextRenderable(renderer, {
21124
- id: "confirm-text",
21125
- 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")}`
22498
+ const text = new TextRenderable(renderer, {
22499
+ id: `msg-${msg.id}-text`,
22500
+ content: t`${fg(theme.colors.success)(">")} ${fg(theme.colors.text)(msg.content)}`
21126
22501
  });
21127
- confirmPrompt.add(confirmText);
21128
- outputContainer = new BoxRenderable(renderer, {
21129
- id: "output-container",
21130
- flexGrow: 1,
22502
+ box.add(text);
22503
+ return box;
22504
+ }
22505
+ function createAssistantMessageRenderable(msg, theme) {
22506
+ const isSelected = pendingMessageId === msg.id;
22507
+ const card = new BoxRenderable(renderer, {
22508
+ id: `msg-${msg.id}`,
22509
+ flexDirection: "column",
22510
+ width: "100%",
21131
22511
  border: true,
21132
- borderColor: theme.colors.border,
22512
+ borderColor: isSelected ? theme.colors.primary : theme.colors.border,
21133
22513
  borderStyle: "single",
21134
- title: "Output",
21135
- padding: 1
22514
+ paddingLeft: 1,
22515
+ paddingRight: 1,
22516
+ paddingTop: 0,
22517
+ paddingBottom: 0,
22518
+ backgroundColor: theme.colors.backgroundPanel
21136
22519
  });
21137
- mainContainer.add(outputContainer);
21138
- const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
21139
- const freeModelsNote = config.provider === "opencode-zen" ? `
21140
- ${fg(theme.colors.success)("Free models available!")} Try: grok-code, glm-4.7-free` : "";
21141
- outputText = new TextRenderable(renderer, {
21142
- id: "output-text",
21143
- content: t`${fg(theme.colors.textMuted)(`Ready. Using ${providerName}.`)}${freeModelsNote}
21144
-
21145
- ${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.")}`
22520
+ const commandText = new TextRenderable(renderer, {
22521
+ id: `msg-${msg.id}-cmd`,
22522
+ content: t`${fg(theme.colors.textMuted)("Command:")} ${fg(theme.colors.text)(msg.command || "")}`
21146
22523
  });
21147
- outputContainer.add(outputText);
21148
- statusText = new TextRenderable(renderer, {
21149
- id: "status-text",
21150
- content: getDryRunStatus(),
21151
- marginTop: 1
22524
+ card.add(commandText);
22525
+ if (msg.safety) {
22526
+ const severityColor = getSeverityColor(msg.safety.severity);
22527
+ const severityText = msg.safety.isDangerous ? `${msg.safety.severity.toUpperCase()} risk${msg.safety.reason ? ` - ${msg.safety.reason}` : ""}` : "Low risk";
22528
+ const safetyText = new TextRenderable(renderer, {
22529
+ id: `msg-${msg.id}-safety`,
22530
+ content: t`${fg(severityColor)("●")} ${fg(theme.colors.textMuted)(severityText)}`
22531
+ });
22532
+ card.add(safetyText);
22533
+ }
22534
+ if (isSelected && !msg.executed) {
22535
+ const actionsText = new TextRenderable(renderer, {
22536
+ id: `msg-${msg.id}-actions`,
22537
+ 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")}`
22538
+ });
22539
+ card.add(actionsText);
22540
+ }
22541
+ if (msg.executed) {
22542
+ const execText = new TextRenderable(renderer, {
22543
+ id: `msg-${msg.id}-exec`,
22544
+ content: t`${fg(theme.colors.success)("Executed")}`
22545
+ });
22546
+ card.add(execText);
22547
+ }
22548
+ return card;
22549
+ }
22550
+ function createResultMessageRenderable(msg, theme) {
22551
+ const isSuccess = msg.exitCode === undefined || msg.exitCode === 0;
22552
+ const isExpanded = msg.expanded ?? false;
22553
+ const hasOutput = msg.content && msg.content.trim().length > 0;
22554
+ const outputLines = hasOutput ? msg.content.trim().split(`
22555
+ `) : [];
22556
+ const isLongOutput = outputLines.length > 5;
22557
+ const PREVIEW_LINES = 3;
22558
+ const card = new BoxRenderable(renderer, {
22559
+ id: `msg-${msg.id}`,
22560
+ flexDirection: "column",
22561
+ width: "100%",
22562
+ border: true,
22563
+ borderColor: isSuccess ? theme.colors.success : theme.colors.error,
22564
+ borderStyle: "single",
22565
+ paddingLeft: 1,
22566
+ paddingRight: 1,
22567
+ backgroundColor: theme.colors.backgroundPanel,
22568
+ onMouseDown: isLongOutput ? () => {
22569
+ toggleResultExpand(msg.id);
22570
+ } : undefined
21152
22571
  });
21153
- mainContainer.add(statusText);
21154
- inputField.on(InputRenderableEvents.ENTER, handleInput);
21155
- renderer.keyInput.on("keypress", handleKeypress);
21156
- inputField.focus();
22572
+ const statusIcon = isSuccess ? "✓" : "✗";
22573
+ const statusColor = isSuccess ? theme.colors.success : theme.colors.error;
22574
+ const statusLabel = isSuccess ? "Executed successfully" : `Exit code: ${msg.exitCode}`;
22575
+ const expandIcon = isLongOutput ? isExpanded ? "▼" : "▶" : "";
22576
+ const lineCount = isLongOutput ? ` (${outputLines.length} lines)` : "";
22577
+ const statusText = new TextRenderable(renderer, {
22578
+ id: `msg-${msg.id}-status`,
22579
+ content: t`${fg(statusColor)(statusIcon)} ${fg(theme.colors.text)(statusLabel)}${fg(theme.colors.textMuted)(lineCount)} ${fg(theme.colors.primary)(expandIcon)}`
22580
+ });
22581
+ card.add(statusText);
22582
+ if (hasOutput) {
22583
+ let displayContent;
22584
+ if (isExpanded || !isLongOutput) {
22585
+ displayContent = msg.content.trim();
22586
+ } else {
22587
+ const previewLines = outputLines.slice(0, PREVIEW_LINES);
22588
+ displayContent = previewLines.join(`
22589
+ `) + `
22590
+ ... ${outputLines.length - PREVIEW_LINES} more lines`;
22591
+ }
22592
+ const outputText = new TextRenderable(renderer, {
22593
+ id: `msg-${msg.id}-output`,
22594
+ content: t`${fg(theme.colors.textMuted)(displayContent)}`
22595
+ });
22596
+ card.add(outputText);
22597
+ if (isLongOutput) {
22598
+ const hintText = new TextRenderable(renderer, {
22599
+ id: `msg-${msg.id}-hint`,
22600
+ content: t`${fg(theme.colors.primary)("[o]")} ${fg(theme.colors.textMuted)(isExpanded ? "Collapse" : "Expand output")}`
22601
+ });
22602
+ card.add(hintText);
22603
+ }
22604
+ }
22605
+ return card;
22606
+ }
22607
+ function createSystemMessageRenderable(msg, theme) {
22608
+ const box = new BoxRenderable(renderer, {
22609
+ id: `msg-${msg.id}`,
22610
+ flexDirection: "column",
22611
+ width: "100%"
22612
+ });
22613
+ const text = new TextRenderable(renderer, {
22614
+ id: `msg-${msg.id}-text`,
22615
+ content: t`${fg(theme.colors.textMuted)(msg.content)}`
22616
+ });
22617
+ box.add(text);
22618
+ return box;
22619
+ }
22620
+ function updateAssistantMessage(msgId, updates) {
22621
+ const msgIndex = chatMessages.findIndex((m) => m.id === msgId);
22622
+ if (msgIndex === -1)
22623
+ return;
22624
+ const msg = chatMessages[msgIndex];
22625
+ Object.assign(msg, updates);
22626
+ chatScrollBox.remove(`msg-${msgId}`);
22627
+ const theme = getTheme2();
22628
+ const newBox = createMessageRenderable(msg, theme);
22629
+ chatScrollBox.add(newBox);
22630
+ }
22631
+ function updateResultMessage(msgId, updates) {
22632
+ const msgIndex = chatMessages.findIndex((m) => m.id === msgId);
22633
+ if (msgIndex === -1)
22634
+ return;
22635
+ const msg = chatMessages[msgIndex];
22636
+ Object.assign(msg, updates);
22637
+ chatScrollBox.remove(`msg-${msgId}`);
22638
+ const theme = getTheme2();
22639
+ const newBox = createMessageRenderable(msg, theme);
22640
+ chatScrollBox.add(newBox);
22641
+ }
22642
+ function toggleResultExpand(msgId) {
22643
+ const msg = chatMessages.find((m) => m.id === msgId);
22644
+ if (!msg || msg.type !== "result")
22645
+ return;
22646
+ const outputLines = msg.content?.trim().split(`
22647
+ `) || [];
22648
+ if (outputLines.length <= 5)
22649
+ return;
22650
+ updateResultMessage(msgId, { expanded: !msg.expanded });
22651
+ }
22652
+ function toggleLastResultExpand() {
22653
+ const resultMessages = chatMessages.filter((m) => m.type === "result");
22654
+ if (resultMessages.length === 0)
22655
+ return;
22656
+ const lastResult = resultMessages[resultMessages.length - 1];
22657
+ toggleResultExpand(lastResult.id);
21157
22658
  }
21158
22659
  function getModelDisplay() {
21159
22660
  const theme = getTheme2();
@@ -21162,30 +22663,22 @@ function getModelDisplay() {
21162
22663
  const freeBadge = currentModel.free ? fg(theme.colors.success)(" FREE") : "";
21163
22664
  return t`${providerBadge} ${fg(categoryColor)(currentModel.name)}${freeBadge}`;
21164
22665
  }
21165
- function getDryRunStatus() {
21166
- const theme = getTheme2();
21167
- if (dryRunMode) {
21168
- 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")}`;
21169
- }
21170
- return t`${fg(theme.colors.textMuted)("Ctrl+X P palette | Ctrl+X M model | Ctrl+X ? help")}`;
21171
- }
21172
22666
  function refreshThemeColors() {
21173
22667
  const theme = getTheme2();
21174
22668
  renderer.setBackgroundColor(theme.colors.background);
21175
22669
  if (headerText) {
21176
- headerText.content = t`${bold(fg(theme.colors.primary)("magic-shell"))} ${fg(theme.colors.textMuted)("- natural language to terminal commands")}`;
22670
+ headerText.content = t`${bold(fg(theme.colors.primary)("magic-shell"))}`;
21177
22671
  }
21178
- if (cwdText) {
21179
- cwdText.content = t`${fg(theme.colors.textMuted)("cwd:")} ${fg(theme.colors.success)(currentCwd)}`;
22672
+ if (statusBarText) {
22673
+ statusBarText.content = getStatusBarContent();
21180
22674
  }
21181
- if (modelText) {
21182
- modelText.content = getModelDisplay();
22675
+ if (helpBarText) {
22676
+ helpBarText.content = getHelpBarContent();
21183
22677
  }
21184
- if (statusText) {
21185
- statusText.content = getDryRunStatus();
21186
- }
21187
- if (outputContainer) {
21188
- outputContainer.borderColor = theme.colors.border;
22678
+ if (chatScrollBox) {
22679
+ chatScrollBox.rootOptions = {
22680
+ borderColor: theme.colors.border
22681
+ };
21189
22682
  }
21190
22683
  if (inputField) {
21191
22684
  inputField.focusedBackgroundColor = theme.colors.backgroundPanel;
@@ -21193,14 +22686,6 @@ function refreshThemeColors() {
21193
22686
  inputField.placeholderColor = theme.colors.textMuted;
21194
22687
  inputField.cursorColor = theme.colors.primary;
21195
22688
  }
21196
- const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
21197
- const freeModelsNote = config.provider === "opencode-zen" ? `
21198
- ${fg(theme.colors.success)("Free models available!")} Try: grok-code, glm-4.7-free` : "";
21199
- if (outputText) {
21200
- outputText.content = t`${fg(theme.colors.textMuted)(`Ready. Using ${providerName}.`)}${freeModelsNote}
21201
-
21202
- ${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.")}`;
21203
- }
21204
22689
  }
21205
22690
  async function handleInput(value) {
21206
22691
  const input = value.trim();
@@ -21211,8 +22696,9 @@ async function handleInput(value) {
21211
22696
  await handleSpecialCommand(input);
21212
22697
  return;
21213
22698
  }
22699
+ addUserMessage(input);
21214
22700
  if (isDirectCommand(input)) {
21215
- await processCommand(input, input);
22701
+ await processDirectCommand(input, input);
21216
22702
  return;
21217
22703
  }
21218
22704
  await translateAndProcess(input);
@@ -21246,38 +22732,50 @@ function isDirectCommand(input) {
21246
22732
  async function translateAndProcess(input) {
21247
22733
  const apiKey = await getApiKey2(config.provider);
21248
22734
  if (!apiKey) {
21249
- setOutput(t`${fg("#ef4444")("Error: No API key configured. Run !provider to set up.")}`);
22735
+ addSystemMessage("Error: No API key configured. Run !provider to set up.");
21250
22736
  return;
21251
22737
  }
21252
- setOutput(t`${fg("#64748b")("Translating...")}`);
22738
+ const loadingMsg = addSystemMessage("Translating...");
21253
22739
  try {
21254
- const command = await translateToCommand2(apiKey, currentModel, input, currentCwd, history);
21255
- commandPreview.content = t`${fg("#64748b")("Command:")} ${fg("#f8fafc")(command)}`;
22740
+ const command = await translateToCommand2(apiKey, currentModel, input, currentCwd, history, config.repoContext);
22741
+ chatScrollBox.remove(`msg-${loadingMsg.id}`);
22742
+ chatMessages = chatMessages.filter((m) => m.id !== loadingMsg.id);
21256
22743
  const safety = analyzeCommand2(command, config);
22744
+ const assistantMsg = addAssistantMessage(input, command, safety);
21257
22745
  if (safety.isDangerous) {
21258
- safetyWarning.content = t`${fg(getSeverityColor(safety.severity))(`[${safety.severity.toUpperCase()}] ${safety.reason}`)}`;
21259
- pendingCommand = command;
22746
+ pendingMessageId = assistantMsg.id;
21260
22747
  awaitingConfirmation = true;
21261
- confirmPrompt.visible = true;
21262
- setOutput(t`${fg("#fbbf24")("Command requires confirmation. Press Enter to execute or Esc to cancel.")}`);
22748
+ helpBarText.content = getHelpBarContent();
21263
22749
  } else {
21264
- safetyWarning.content = "";
21265
- await processCommand(input, command);
22750
+ await executeAndShowResult(input, command, assistantMsg.id);
21266
22751
  }
21267
22752
  } catch (error) {
22753
+ chatScrollBox.remove(`msg-${loadingMsg.id}`);
22754
+ chatMessages = chatMessages.filter((m) => m.id !== loadingMsg.id);
21268
22755
  const message = error instanceof Error ? error.message : String(error);
21269
- setOutput(t`${fg("#ef4444")(`Error: ${message}`)}`);
22756
+ addSystemMessage(`Error: ${message}`);
22757
+ }
22758
+ }
22759
+ async function processDirectCommand(input, command) {
22760
+ const safety = analyzeCommand2(command, config);
22761
+ const assistantMsg = addAssistantMessage(input, command, safety);
22762
+ if (safety.isDangerous) {
22763
+ pendingMessageId = assistantMsg.id;
22764
+ awaitingConfirmation = true;
22765
+ helpBarText.content = getHelpBarContent();
22766
+ } else {
22767
+ await executeAndShowResult(input, command, assistantMsg.id);
21270
22768
  }
21271
22769
  }
21272
- async function processCommand(input, command) {
22770
+ async function executeAndShowResult(input, command, assistantMsgId) {
21273
22771
  if (command.startsWith("cd ")) {
21274
22772
  const path2 = command.slice(3).trim().replace(/^["']|["']$/g, "");
21275
22773
  try {
21276
22774
  const expandedPath = path2.startsWith("~") ? path2.replace("~", process.env.HOME || "") : path2;
21277
22775
  process.chdir(expandedPath);
21278
22776
  currentCwd = getCwd();
21279
- cwdText.content = t`${fg("#64748b")("cwd:")} ${fg("#22c55e")(currentCwd)}`;
21280
- setOutput(t`${fg("#22c55e")(`Changed directory to ${currentCwd}`)}`);
22777
+ statusBarText.content = getStatusBarContent();
22778
+ addResultMessage(`Changed directory to ${currentCwd}`, 0);
21281
22779
  addToHistory({
21282
22780
  input,
21283
22781
  command,
@@ -21285,35 +22783,37 @@ async function processCommand(input, command) {
21285
22783
  timestamp: Date.now()
21286
22784
  });
21287
22785
  history = loadHistory2();
22786
+ updateAssistantMessage(assistantMsgId, { executed: true });
21288
22787
  } catch (err) {
21289
- setOutput(t`${fg("#ef4444")(`cd: ${err instanceof Error ? err.message : String(err)}`)}`);
22788
+ addResultMessage(`cd: ${err instanceof Error ? err.message : String(err)}`, 1);
21290
22789
  }
21291
22790
  clearCommandState();
21292
22791
  return;
21293
22792
  }
21294
22793
  if (dryRunMode) {
21295
- setOutput(t`${fg("#fbbf24")("[DRY RUN]")} Would execute: ${fg("#f8fafc")(command)}`);
22794
+ addResultMessage(`[DRY RUN] Would execute: ${command}`, 0);
22795
+ updateAssistantMessage(assistantMsgId, { executed: true });
21296
22796
  clearCommandState();
21297
22797
  return;
21298
22798
  }
21299
- setOutput(t`${fg("#64748b")("Executing...")}`);
21300
22799
  try {
21301
- const result = await executeCommand(command);
21302
- setOutput(result || t`${fg("#22c55e")("Command completed successfully")}`);
22800
+ const { output, exitCode } = await executeCommandWithCode(command);
22801
+ addResultMessage(output || "Command completed successfully", exitCode);
21303
22802
  addToHistory({
21304
22803
  input,
21305
22804
  command,
21306
- output: result.slice(0, 500),
22805
+ output: output.slice(0, 500),
21307
22806
  timestamp: Date.now()
21308
22807
  });
21309
22808
  history = loadHistory2();
22809
+ updateAssistantMessage(assistantMsgId, { executed: true });
21310
22810
  } catch (error) {
21311
22811
  const message = error instanceof Error ? error.message : String(error);
21312
- setOutput(t`${fg("#ef4444")(`Error: ${message}`)}`);
22812
+ addResultMessage(`Error: ${message}`, 1);
21313
22813
  }
21314
22814
  clearCommandState();
21315
22815
  }
21316
- function executeCommand(command) {
22816
+ function executeCommandWithCode(command) {
21317
22817
  return new Promise((resolve3, reject) => {
21318
22818
  const child = spawn(command, {
21319
22819
  shell: true,
@@ -21332,23 +22832,16 @@ function executeCommand(command) {
21332
22832
  reject(error);
21333
22833
  });
21334
22834
  child.on("close", (code) => {
21335
- if (code === 0) {
21336
- resolve3(stdout || stderr);
21337
- } else {
21338
- resolve3(stderr || stdout || `Command exited with code ${code}`);
21339
- }
22835
+ const exitCode = code ?? 0;
22836
+ const output = stdout || stderr || (exitCode === 0 ? "" : `Command exited with code ${exitCode}`);
22837
+ resolve3({ output, exitCode });
21340
22838
  });
21341
22839
  });
21342
22840
  }
21343
22841
  function clearCommandState() {
21344
- pendingCommand = null;
22842
+ pendingMessageId = null;
21345
22843
  awaitingConfirmation = false;
21346
- confirmPrompt.visible = false;
21347
- commandPreview.content = "";
21348
- safetyWarning.content = "";
21349
- }
21350
- function setOutput(content) {
21351
- outputText.content = content;
22844
+ helpBarText.content = getHelpBarContent();
21352
22845
  }
21353
22846
  async function handleSpecialCommand(input) {
21354
22847
  const cmd = input.slice(1).toLowerCase().trim();
@@ -21364,8 +22857,8 @@ async function handleSpecialCommand(input) {
21364
22857
  break;
21365
22858
  case "dry":
21366
22859
  dryRunMode = !dryRunMode;
21367
- statusText.content = getDryRunStatus();
21368
- setOutput(t`${fg("#22c55e")(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`)}`);
22860
+ statusBarText.content = getStatusBarContent();
22861
+ addSystemMessage(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`);
21369
22862
  break;
21370
22863
  case "config":
21371
22864
  await showConfig();
@@ -21374,65 +22867,74 @@ async function handleSpecialCommand(input) {
21374
22867
  showHistory();
21375
22868
  break;
21376
22869
  case "clear":
21377
- setOutput("");
22870
+ clearChat();
21378
22871
  break;
21379
22872
  default:
21380
22873
  if (cmd) {
21381
- await processCommand(input, cmd);
22874
+ addUserMessage(cmd);
22875
+ await processDirectCommand(input, cmd);
21382
22876
  }
21383
22877
  }
21384
22878
  }
22879
+ function clearChat() {
22880
+ for (const msg of chatMessages) {
22881
+ chatScrollBox.remove(`msg-${msg.id}`);
22882
+ }
22883
+ chatMessages = [];
22884
+ addSystemMessage(getWelcomeMessage());
22885
+ }
21385
22886
  function showHelp() {
21386
- const theme = getTheme2();
21387
- setOutput(t`${bold(fg(theme.colors.primary)("Magic Shell"))}
21388
-
21389
- ${bold(fg(theme.colors.textMuted)("Keyboard Shortcuts (Ctrl+X then...):"))}
21390
- ${fg(theme.colors.primary)("P")} ${fg(theme.colors.textMuted)("Command palette")} ${fg(theme.colors.primary)("M")} ${fg(theme.colors.textMuted)("Change model")}
21391
- ${fg(theme.colors.primary)("S")} ${fg(theme.colors.textMuted)("Switch provider")} ${fg(theme.colors.primary)("D")} ${fg(theme.colors.textMuted)("Toggle dry-run")}
21392
- ${fg(theme.colors.primary)("T")} ${fg(theme.colors.textMuted)("Change theme")} ${fg(theme.colors.primary)("H")} ${fg(theme.colors.textMuted)("Show history")}
21393
- ${fg(theme.colors.primary)("C")} ${fg(theme.colors.textMuted)("Show config")} ${fg(theme.colors.primary)("L")} ${fg(theme.colors.textMuted)("Clear output")}
21394
- ${fg(theme.colors.primary)("?")} ${fg(theme.colors.textMuted)("This help")} ${fg(theme.colors.primary)("Q")} ${fg(theme.colors.textMuted)("Exit")}
22887
+ const helpText = `Keyboard Shortcuts (Ctrl+X then...):
22888
+ P Command palette M Change model
22889
+ S Switch provider D Toggle dry-run
22890
+ T Change theme R Toggle repo context
22891
+ H Show history L Clear chat
22892
+ C Show config ? This help
22893
+ Q Exit
21395
22894
 
21396
- ${bold(fg(theme.colors.textMuted)("Other:"))}
21397
- ${fg(theme.colors.primary)("Ctrl+C")} ${fg(theme.colors.textMuted)("Exit / Cancel")} ${fg(theme.colors.primary)("Esc")} ${fg(theme.colors.textMuted)("Close palette")}
22895
+ Other:
22896
+ Ctrl+C Exit / Cancel Esc Close palette
21398
22897
 
21399
- ${bold(fg(theme.colors.textMuted)("Tips:"))}
22898
+ Tips:
21400
22899
  - Type naturally: "list all files" -> ls -la
21401
22900
  - Reference history: "do that again", "undo"
21402
- - ${fg(theme.colors.success)("Free models:")} gpt-5-nano, grok-code, glm-4.7-free`);
22901
+ - Enable repo context to use project scripts (Ctrl+X R)`;
22902
+ addSystemMessage(helpText);
21403
22903
  }
21404
22904
  async function showConfig() {
21405
22905
  const theme = getTheme2();
21406
22906
  const providerName = config.provider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
21407
22907
  const apiKey = await getApiKey2(config.provider);
21408
- const apiKeyStatus = apiKey ? fg(theme.colors.success)("configured") : fg(theme.colors.error)("not set");
21409
- const freeBadge = currentModel.free ? fg(theme.colors.success)(" (FREE)") : "";
22908
+ const apiKeyStatus = apiKey ? "configured" : "not set";
22909
+ const freeBadge = currentModel.free ? " (FREE)" : "";
21410
22910
  const shellInfo = getShellInfo2();
21411
- setOutput(t`${bold(fg(theme.colors.primary)("Current Configuration"))}
22911
+ const configText = `Current Configuration
21412
22912
 
21413
- ${fg(theme.colors.textMuted)("Provider:")} ${fg(theme.colors.text)(providerName)}
21414
- ${fg(theme.colors.textMuted)("Model:")} ${fg(theme.colors.text)(currentModel.name)}${freeBadge}
21415
- ${fg(theme.colors.textMuted)("Model ID:")} ${fg(theme.colors.textMuted)(currentModel.id)}
21416
- ${fg(theme.colors.textMuted)("Category:")} ${fg(theme.colors.text)(currentModel.category)}
21417
- ${fg(theme.colors.textMuted)("Theme:")} ${fg(theme.colors.text)(theme.name)}
21418
- ${fg(theme.colors.textMuted)("Shell:")} ${fg(theme.colors.text)(shellInfo.shell)} ${fg(theme.colors.textMuted)(`(${shellInfo.shellPath})`)}
21419
- ${fg(theme.colors.textMuted)("Platform:")} ${fg(theme.colors.text)(shellInfo.platform)}${shellInfo.isWSL ? fg(theme.colors.textMuted)(" (WSL)") : ""}
21420
- ${fg(theme.colors.textMuted)("Safety:")} ${fg(theme.colors.text)(config.safetyLevel)}
21421
- ${fg(theme.colors.textMuted)("Dry-run:")} ${fg(theme.colors.text)(dryRunMode ? "ON" : "OFF")}
21422
- ${fg(theme.colors.textMuted)("API Key:")} ${apiKeyStatus}
21423
- ${fg(theme.colors.textMuted)("History:")} ${fg(theme.colors.text)(`${history.length} commands`)}`);
22913
+ Provider: ${providerName}
22914
+ Model: ${currentModel.name}${freeBadge}
22915
+ Model ID: ${currentModel.id}
22916
+ Category: ${currentModel.category}
22917
+ Theme: ${theme.name}
22918
+ Shell: ${shellInfo.shell} (${shellInfo.shellPath})
22919
+ Platform: ${shellInfo.platform}${shellInfo.isWSL ? " (WSL)" : ""}
22920
+ Safety: ${config.safetyLevel}
22921
+ Dry-run: ${dryRunMode ? "ON" : "OFF"}
22922
+ Repo context: ${config.repoContext ? "ON" : "OFF"}
22923
+ API Key: ${apiKeyStatus}
22924
+ History: ${history.length} commands`;
22925
+ addSystemMessage(configText);
21424
22926
  }
21425
22927
  function showHistory() {
21426
22928
  if (history.length === 0) {
21427
- setOutput(t`${fg("#64748b")("No command history yet.")}`);
22929
+ addSystemMessage("No command history yet.");
21428
22930
  return;
21429
22931
  }
21430
22932
  const recent = history.slice(-10);
21431
22933
  const lines = recent.map((entry, i) => {
21432
22934
  const date = new Date(entry.timestamp).toLocaleTimeString();
21433
- return t`${fg("#64748b")(`${i + 1}.`)} ${fg("#94a3b8")(`[${date}]`)} ${fg("#f8fafc")(entry.command)}`;
22935
+ return `${i + 1}. [${date}] ${entry.command}`;
21434
22936
  });
21435
- setOutput(t`${bold(fg("#60a5fa")("Recent Command History"))}
22937
+ addSystemMessage(`Recent Command History
21436
22938
 
21437
22939
  ${lines.join(`
21438
22940
  `)}`);
@@ -21498,10 +23000,10 @@ async function switchProvider() {
21498
23000
  currentModel = models.find((m) => m.id === config.defaultModel) || models[0];
21499
23001
  config.defaultModel = currentModel.id;
21500
23002
  saveConfig2(config);
21501
- modelText.content = getModelDisplay();
23003
+ statusBarText.content = getStatusBarContent();
21502
23004
  closeSelector();
21503
23005
  const providerName = newProvider === "opencode-zen" ? "OpenCode Zen" : "OpenRouter";
21504
- setOutput(t`${fg("#22c55e")(`Switched to ${providerName}. Model: ${currentModel.name}`)}`);
23006
+ addSystemMessage(`Switched to ${providerName}. Model: ${currentModel.name}`);
21505
23007
  } else {
21506
23008
  closeSelector();
21507
23009
  renderer.root.remove("main-container");
@@ -21567,12 +23069,12 @@ function showModelSelector() {
21567
23069
  currentModel = option.value;
21568
23070
  config.defaultModel = currentModel.id;
21569
23071
  saveConfig2(config);
21570
- modelText.content = getModelDisplay();
23072
+ statusBarText.content = getStatusBarContent();
21571
23073
  renderer.root.remove("model-selector-container");
21572
23074
  modelSelector = null;
21573
23075
  inputField.focus();
21574
23076
  const freeBadge = currentModel.free ? " (FREE)" : "";
21575
- setOutput(t`${fg("#22c55e")(`Model changed to ${currentModel.name}${freeBadge}`)}`);
23077
+ addSystemMessage(`Model changed to ${currentModel.name}${freeBadge}`);
21576
23078
  });
21577
23079
  modelSelector.focus();
21578
23080
  }
@@ -21629,8 +23131,7 @@ function showThemeSelector() {
21629
23131
  renderer.root.remove("theme-selector-container");
21630
23132
  themeSelector = null;
21631
23133
  refreshThemeColors();
21632
- const newTheme = getTheme2();
21633
- setOutput(t`${fg(newTheme.colors.success)(`Theme changed to ${themeName}`)}`);
23134
+ addSystemMessage(`Theme changed to ${themeName}`);
21634
23135
  inputField.focus();
21635
23136
  });
21636
23137
  const escHandler = (key) => {
@@ -21674,8 +23175,20 @@ function getCommandPaletteOptions() {
21674
23175
  chord: "d",
21675
23176
  action: () => {
21676
23177
  dryRunMode = !dryRunMode;
21677
- statusText.content = getDryRunStatus();
21678
- setOutput(t`${fg("#22c55e")(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`)}`);
23178
+ statusBarText.content = getStatusBarContent();
23179
+ addSystemMessage(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`);
23180
+ }
23181
+ },
23182
+ {
23183
+ name: "Toggle Project Context",
23184
+ description: config.repoContext ? "Currently ON (sends script names to AI)" : "Currently OFF",
23185
+ key: "r",
23186
+ chord: "r",
23187
+ action: () => {
23188
+ config.repoContext = !config.repoContext;
23189
+ saveConfig2(config);
23190
+ statusBarText.content = getStatusBarContent();
23191
+ addSystemMessage(`Project context: ${config.repoContext ? "ON - AI can see your package.json scripts, Makefile targets, etc." : "OFF"}`);
21679
23192
  }
21680
23193
  },
21681
23194
  {
@@ -21700,11 +23213,11 @@ function getCommandPaletteOptions() {
21700
23213
  action: () => showThemeSelector()
21701
23214
  },
21702
23215
  {
21703
- name: "Clear Output",
21704
- description: "Clear the output area",
23216
+ name: "Clear Chat",
23217
+ description: "Clear the chat history",
21705
23218
  key: "l",
21706
23219
  chord: "l",
21707
- action: () => setOutput("")
23220
+ action: () => clearChat()
21708
23221
  },
21709
23222
  {
21710
23223
  name: "Show Help",
@@ -21845,24 +23358,44 @@ function handleKeypress(key) {
21845
23358
  inputField.focus();
21846
23359
  return;
21847
23360
  }
21848
- if (awaitingConfirmation) {
23361
+ if (awaitingConfirmation && pendingMessageId) {
21849
23362
  clearCommandState();
21850
- setOutput(t`${fg("#64748b")("Command cancelled.")}`);
23363
+ addSystemMessage("Command cancelled.");
21851
23364
  inputField.focus();
21852
23365
  }
21853
23366
  }
21854
- if (key.name === "return" && awaitingConfirmation && pendingCommand) {
21855
- const cmd = pendingCommand;
21856
- clearCommandState();
21857
- processCommand("", cmd);
23367
+ if (key.name === "return" && awaitingConfirmation && pendingMessageId) {
23368
+ const msg = chatMessages.find((m) => m.id === pendingMessageId);
23369
+ if (msg && msg.command) {
23370
+ const command = msg.command;
23371
+ const msgId = pendingMessageId;
23372
+ clearCommandState();
23373
+ executeAndShowResult(msg.content, command, msgId);
23374
+ }
21858
23375
  }
21859
- if (key.name === "e" && awaitingConfirmation && pendingCommand) {
21860
- inputField.value = pendingCommand;
21861
- clearCommandState();
21862
- inputField.focus();
23376
+ if (key.name === "e" && awaitingConfirmation && pendingMessageId) {
23377
+ const msg = chatMessages.find((m) => m.id === pendingMessageId);
23378
+ if (msg && msg.command) {
23379
+ inputField.value = msg.command;
23380
+ clearCommandState();
23381
+ inputField.focus();
23382
+ }
23383
+ }
23384
+ if (key.name === "c" && awaitingConfirmation && pendingMessageId) {
23385
+ const msg = chatMessages.find((m) => m.id === pendingMessageId);
23386
+ if (msg && msg.command) {
23387
+ const copyCmd = process.platform === "darwin" ? "pbcopy" : "xclip -selection clipboard";
23388
+ const child = spawn(copyCmd, { shell: true });
23389
+ child.stdin?.write(msg.command);
23390
+ child.stdin?.end();
23391
+ addSystemMessage(`Copied to clipboard: ${msg.command}`);
23392
+ }
23393
+ }
23394
+ if (key.name === "o" && !awaitingConfirmation && !commandPalette && !modelSelector) {
23395
+ toggleLastResultExpand();
21863
23396
  }
21864
23397
  }
21865
- var renderer, currentModel, config, history, currentCwd, dryRunMode = false, mainContainer, headerText, cwdText, modelText, inputField, outputContainer, outputText, statusText, commandPreview, safetyWarning, confirmPrompt, modelSelector = null, providerSelector = null, pendingCommand = null, awaitingConfirmation = false, themeSelector = null, commandPalette = null, chordMode = "none", cli_default;
23398
+ var renderer, currentModel, config, history, currentCwd, dryRunMode = false, chatMessages, messageIdCounter = 0, mainContainer, headerText, statusBarText, chatScrollBox, inputField, helpBarText, modelSelector = null, providerSelector = null, pendingMessageId = null, awaitingConfirmation = false, themeSelector = null, commandPalette = null, chordMode = "none", cli_default;
21866
23399
  var init_cli = __esm(async () => {
21867
23400
  init_types();
21868
23401
  init_config();
@@ -21873,6 +23406,7 @@ var init_cli = __esm(async () => {
21873
23406
  currentModel = OPENCODE_ZEN_MODELS2[0];
21874
23407
  history = [];
21875
23408
  currentCwd = getCwd();
23409
+ chatMessages = [];
21876
23410
  if (false) {}
21877
23411
  cli_default = main2;
21878
23412
  });
@@ -22182,7 +23716,7 @@ var DEFAULT_CONFIG = {
22182
23716
  provider: "opencode-zen",
22183
23717
  openrouterApiKey: "",
22184
23718
  opencodeZenApiKey: "",
22185
- defaultModel: "gpt-5-nano",
23719
+ defaultModel: "gemini-3-flash",
22186
23720
  safetyLevel: "moderate",
22187
23721
  dryRunByDefault: false,
22188
23722
  blockedCommands: [
@@ -22193,7 +23727,8 @@ var DEFAULT_CONFIG = {
22193
23727
  "chmod -R 777 /",
22194
23728
  "chown -R"
22195
23729
  ],
22196
- confirmedDangerousPatterns: []
23730
+ confirmedDangerousPatterns: [],
23731
+ repoContext: false
22197
23732
  };
22198
23733
  function ensureConfigDir() {
22199
23734
  if (!existsSync(CONFIG_DIR)) {
@@ -22394,6 +23929,7 @@ function getSeverityMessage(severity) {
22394
23929
 
22395
23930
  // src/lib/api.ts
22396
23931
  init_shell();
23932
+ init_repo_context();
22397
23933
  function getZenApiType(modelId) {
22398
23934
  if (modelId.startsWith("gpt-")) {
22399
23935
  return "openai-responses";
@@ -22406,11 +23942,21 @@ function getZenApiType(modelId) {
22406
23942
  }
22407
23943
  return "openai-compatible";
22408
23944
  }
22409
- function buildSystemPrompt(cwd, history, shellInfo) {
23945
+ function buildSystemPrompt(cwd, history, shellInfo, repoContextEnabled) {
22410
23946
  const historyContext = formatHistory(history);
22411
23947
  const platformPaths = getPlatformPaths(shellInfo.platform);
22412
23948
  const shellHints = getShellSyntaxHints(shellInfo.shell);
22413
23949
  const platformName = shellInfo.platform === "macos" ? "macOS" : shellInfo.platform === "windows" ? "Windows" : shellInfo.platform === "linux" ? shellInfo.isWSL ? "Linux (WSL)" : "Linux" : "Unknown";
23950
+ let projectContextSection = "";
23951
+ if (repoContextEnabled) {
23952
+ const repoContext = detectRepoContext(cwd);
23953
+ if (repoContext) {
23954
+ projectContextSection = `
23955
+ Project context:
23956
+ ${formatRepoContext(repoContext)}
23957
+ `;
23958
+ }
23959
+ }
22414
23960
  return `You are a shell command translator. Convert the user's natural language request into a shell command.
22415
23961
 
22416
23962
  Current environment:
@@ -22419,7 +23965,7 @@ Current environment:
22419
23965
  - Working directory: ${cwd}
22420
23966
  - Home directory: ${shellInfo.homeDir}
22421
23967
  ${shellInfo.terminalEmulator ? `- Terminal: ${shellInfo.terminalEmulator}` : ""}
22422
-
23968
+ ${projectContextSection}
22423
23969
  ${shellHints}
22424
23970
 
22425
23971
  Recent command history:
@@ -22430,7 +23976,8 @@ Rules:
22430
23976
  - No explanations, no markdown, no backticks, no code blocks
22431
23977
  - Use the correct syntax for the detected shell (${shellInfo.shell})
22432
23978
  - If the request is unclear, make a reasonable assumption
22433
- - Prefer simple, common commands over complex one-liners
23979
+ - Prefer simple, common commands over complex one-liners${repoContextEnabled ? `
23980
+ - Use project-specific commands when relevant (e.g., use the detected package manager and available scripts)` : ""}
22434
23981
  - Use the command history for context (e.g., "do that again", "undo", "delete the file I just created")
22435
23982
  - If the user asks something that can't be done with a shell command, output a command that prints a helpful message
22436
23983
  - For file operations, prefer safer alternatives when possible
@@ -22704,9 +24251,9 @@ function getShellInfo() {
22704
24251
  }
22705
24252
  return cachedShellInfo;
22706
24253
  }
22707
- async function translateToCommand(apiKey, model, userInput, cwd, history = []) {
24254
+ async function translateToCommand(apiKey, model, userInput, cwd, history = [], repoContextEnabled) {
22708
24255
  const shellInfo = getShellInfo();
22709
- const systemPrompt = buildSystemPrompt(cwd, history, shellInfo);
24256
+ const systemPrompt = buildSystemPrompt(cwd, history, shellInfo, repoContextEnabled);
22710
24257
  let rawCommand;
22711
24258
  if (model.provider === "openrouter") {
22712
24259
  rawCommand = await callOpenRouter(apiKey, model.id, systemPrompt, userInput);
@@ -22947,6 +24494,128 @@ function getAnsiColors() {
22947
24494
  }
22948
24495
  loadTheme();
22949
24496
 
24497
+ // src/lib/update-checker.ts
24498
+ import { homedir as homedir4 } from "os";
24499
+ import { join as join4 } from "path";
24500
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
24501
+ var __dirname = "/Users/austin/code/dxd/magic-shell/src/lib";
24502
+ var PACKAGE_NAME = "@austinthesing/magic-shell";
24503
+ var NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
24504
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
24505
+ var CONFIG_DIR3 = join4(homedir4(), ".magic-shell");
24506
+ var UPDATE_CHECK_FILE = join4(CONFIG_DIR3, ".update-check");
24507
+ function ensureConfigDir3() {
24508
+ if (!existsSync5(CONFIG_DIR3)) {
24509
+ mkdirSync3(CONFIG_DIR3, { recursive: true });
24510
+ }
24511
+ }
24512
+ function loadUpdateState() {
24513
+ ensureConfigDir3();
24514
+ try {
24515
+ if (existsSync5(UPDATE_CHECK_FILE)) {
24516
+ return JSON.parse(readFileSync4(UPDATE_CHECK_FILE, "utf-8"));
24517
+ }
24518
+ } catch {}
24519
+ return { lastCheck: 0, latestVersion: null, dismissed: null };
24520
+ }
24521
+ function saveUpdateState(state) {
24522
+ ensureConfigDir3();
24523
+ try {
24524
+ writeFileSync3(UPDATE_CHECK_FILE, JSON.stringify(state));
24525
+ } catch {}
24526
+ }
24527
+ function getCurrentVersion() {
24528
+ try {
24529
+ const packagePaths = [
24530
+ join4(__dirname, "../../package.json"),
24531
+ join4(__dirname, "../../../package.json"),
24532
+ join4(process.cwd(), "package.json")
24533
+ ];
24534
+ for (const path of packagePaths) {
24535
+ if (existsSync5(path)) {
24536
+ const pkg = JSON.parse(readFileSync4(path, "utf-8"));
24537
+ if (pkg.name === PACKAGE_NAME || pkg.name === "magic-shell") {
24538
+ return pkg.version;
24539
+ }
24540
+ }
24541
+ }
24542
+ } catch {}
24543
+ return "0.0.0";
24544
+ }
24545
+ function compareVersions(a, b) {
24546
+ const partsA = a.split(".").map(Number);
24547
+ const partsB = b.split(".").map(Number);
24548
+ for (let i = 0;i < Math.max(partsA.length, partsB.length); i++) {
24549
+ const numA = partsA[i] || 0;
24550
+ const numB = partsB[i] || 0;
24551
+ if (numA > numB)
24552
+ return 1;
24553
+ if (numA < numB)
24554
+ return -1;
24555
+ }
24556
+ return 0;
24557
+ }
24558
+ async function fetchLatestVersion() {
24559
+ try {
24560
+ const controller = new AbortController;
24561
+ const timeout = setTimeout(() => controller.abort(), 3000);
24562
+ const response = await fetch(NPM_REGISTRY_URL, {
24563
+ signal: controller.signal,
24564
+ headers: { Accept: "application/json" }
24565
+ });
24566
+ clearTimeout(timeout);
24567
+ if (!response.ok)
24568
+ return null;
24569
+ const data = await response.json();
24570
+ return data.version || null;
24571
+ } catch {
24572
+ return null;
24573
+ }
24574
+ }
24575
+ async function checkForUpdates() {
24576
+ const state = loadUpdateState();
24577
+ const currentVersion = getCurrentVersion();
24578
+ const now = Date.now();
24579
+ if (now - state.lastCheck < CHECK_INTERVAL_MS) {
24580
+ if (state.latestVersion && compareVersions(state.latestVersion, currentVersion) > 0) {
24581
+ if (state.dismissed === state.latestVersion) {
24582
+ return null;
24583
+ }
24584
+ return {
24585
+ hasUpdate: true,
24586
+ currentVersion,
24587
+ latestVersion: state.latestVersion,
24588
+ updateCommand: `bun update -g ${PACKAGE_NAME}`
24589
+ };
24590
+ }
24591
+ return null;
24592
+ }
24593
+ const latestVersion = await fetchLatestVersion();
24594
+ state.lastCheck = now;
24595
+ if (latestVersion) {
24596
+ state.latestVersion = latestVersion;
24597
+ }
24598
+ saveUpdateState(state);
24599
+ if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) {
24600
+ if (state.dismissed === latestVersion) {
24601
+ return null;
24602
+ }
24603
+ return {
24604
+ hasUpdate: true,
24605
+ currentVersion,
24606
+ latestVersion,
24607
+ updateCommand: `bun update -g ${PACKAGE_NAME}`
24608
+ };
24609
+ }
24610
+ return null;
24611
+ }
24612
+ async function forceCheckForUpdates() {
24613
+ const state = loadUpdateState();
24614
+ state.lastCheck = 0;
24615
+ saveUpdateState(state);
24616
+ return checkForUpdates();
24617
+ }
24618
+
22950
24619
  // src/index.ts
22951
24620
  loadTheme();
22952
24621
  var getColors = () => {
@@ -22978,6 +24647,10 @@ ${colors.bold}USAGE${colors.reset}
22978
24647
  msh --provider <name> Set provider (opencode-zen or openrouter)
22979
24648
  msh --themes List available themes
22980
24649
  msh --theme <name> Set color theme
24650
+ msh --repo-context Enable project context detection
24651
+ msh --no-repo-context Disable project context detection
24652
+ msh --version Show version
24653
+ msh --check-update Check for updates
22981
24654
  msh --help Show this help
22982
24655
 
22983
24656
  ${colors.bold}EXAMPLES${colors.reset}
@@ -22990,6 +24663,9 @@ ${colors.bold}EXAMPLES${colors.reset}
22990
24663
  ${colors.dim}# Check what would run${colors.reset}
22991
24664
  msh -n "delete all log files"
22992
24665
 
24666
+ ${colors.dim}# Use project context (knows your npm scripts, etc)${colors.reset}
24667
+ msh --repo-context "run the dev server"
24668
+
22993
24669
  ${colors.dim}# Pipe to clipboard (macOS)${colors.reset}
22994
24670
  msh "find large files" | pbcopy
22995
24671
 
@@ -23165,7 +24841,7 @@ Try: ${colors.cyan}msh "list all files"${colors.reset}
23165
24841
  `);
23166
24842
  rl.close();
23167
24843
  }
23168
- function executeCommand2(command) {
24844
+ function executeCommand(command) {
23169
24845
  return new Promise((resolve3) => {
23170
24846
  const child = spawn2(command, {
23171
24847
  shell: true,
@@ -23193,6 +24869,27 @@ function executeCommand2(command) {
23193
24869
  });
23194
24870
  });
23195
24871
  }
24872
+ function createSpinner(message) {
24873
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
24874
+ let i = 0;
24875
+ const isTTY = process.stderr.isTTY;
24876
+ const interval = isTTY ? setInterval(() => {
24877
+ process.stderr.write(`\r${colors.primary}${frames[i]}${colors.reset} ${colors.dim}${message}${colors.reset}`);
24878
+ i = (i + 1) % frames.length;
24879
+ }, 80) : null;
24880
+ if (!isTTY) {
24881
+ process.stderr.write(`${colors.dim}${message}...${colors.reset}
24882
+ `);
24883
+ }
24884
+ return {
24885
+ stop: () => {
24886
+ if (interval) {
24887
+ clearInterval(interval);
24888
+ process.stderr.write("\r\x1B[K");
24889
+ }
24890
+ }
24891
+ };
24892
+ }
23196
24893
  async function translate(query, options) {
23197
24894
  const config2 = loadConfig();
23198
24895
  const apiKey = await getApiKey(config2.provider);
@@ -23204,12 +24901,18 @@ async function translate(query, options) {
23204
24901
  const model = ALL_MODELS.find((m) => m.id === config2.defaultModel) || (config2.provider === "opencode-zen" ? OPENCODE_ZEN_MODELS[0] : OPENROUTER_MODELS[0]);
23205
24902
  const history2 = loadHistory();
23206
24903
  const cwd = getCwd2();
24904
+ const useRepoContext = options.repoContext ?? config2.repoContext ?? false;
24905
+ const spinner = createSpinner(`Translating with ${model.name}`);
23207
24906
  try {
23208
- const command = await translateToCommand(apiKey, model, query, cwd, history2);
24907
+ const command = await translateToCommand(apiKey, model, query, cwd, history2, useRepoContext);
24908
+ spinner.stop();
23209
24909
  if (options.dryRun) {
23210
24910
  const safety = analyzeCommand(command, config2);
23211
24911
  console.log(`${colors.dim}Query:${colors.reset} ${query}`);
23212
24912
  console.log(`${colors.dim}Model:${colors.reset} ${model.name}`);
24913
+ if (useRepoContext) {
24914
+ console.log(`${colors.dim}Project context:${colors.reset} enabled`);
24915
+ }
23213
24916
  console.log();
23214
24917
  console.log(`${colors.bold}Command:${colors.reset} ${command}`);
23215
24918
  if (safety.isDangerous) {
@@ -23228,33 +24931,64 @@ async function translate(query, options) {
23228
24931
  console.error(`${colors.yellow}Use -n to preview, or run the command manually.${colors.reset}`);
23229
24932
  process.exit(1);
23230
24933
  }
23231
- const result = await executeCommand2(command);
24934
+ const result = await executeCommand(command);
23232
24935
  process.exit(result.code);
23233
24936
  } else {
23234
24937
  console.log(command);
23235
24938
  }
23236
24939
  } catch (error) {
24940
+ spinner.stop();
23237
24941
  const message = error instanceof Error ? error.message : String(error);
23238
24942
  console.error(`${colors.red}Error: ${message}${colors.reset}`);
23239
24943
  process.exit(1);
23240
24944
  }
23241
24945
  }
24946
+ async function showUpdateNotification() {
24947
+ try {
24948
+ const update = await checkForUpdates();
24949
+ if (update?.hasUpdate) {
24950
+ console.error(`${colors.cyan}\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510${colors.reset}`);
24951
+ console.error(`${colors.cyan}\u2502${colors.reset} ${colors.bold}Update available!${colors.reset} ${colors.dim}${update.currentVersion}${colors.reset} \u2192 ${colors.green}${update.latestVersion}${colors.reset} ${colors.cyan}\u2502${colors.reset}`);
24952
+ console.error(`${colors.cyan}\u2502${colors.reset} Run: ${colors.yellow}${update.updateCommand}${colors.reset} ${colors.cyan}\u2502${colors.reset}`);
24953
+ console.error(`${colors.cyan}\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518${colors.reset}`);
24954
+ console.error();
24955
+ }
24956
+ } catch {}
24957
+ }
23242
24958
  async function main3() {
23243
24959
  const args = process.argv.slice(2);
24960
+ const updatePromise = args.length > 0 && !args[0].startsWith("-i") ? showUpdateNotification() : Promise.resolve();
23244
24961
  if (args.length === 0 || args[0] === "-i" || args[0] === "--interactive") {
23245
24962
  const { default: runTui } = await init_cli().then(() => exports_cli);
23246
24963
  await runTui();
23247
24964
  return;
23248
24965
  }
23249
24966
  if (args[0] === "--help" || args[0] === "-h") {
24967
+ await updatePromise;
23250
24968
  printHelp();
23251
24969
  return;
23252
24970
  }
24971
+ if (args[0] === "--version" || args[0] === "-v") {
24972
+ console.log(`magic-shell v${getCurrentVersion()}`);
24973
+ return;
24974
+ }
24975
+ if (args[0] === "--check-update") {
24976
+ const update = await forceCheckForUpdates();
24977
+ if (update?.hasUpdate) {
24978
+ console.log(`${colors.green}Update available!${colors.reset} ${update.currentVersion} \u2192 ${update.latestVersion}`);
24979
+ console.log(`Run: ${colors.cyan}${update.updateCommand}${colors.reset}`);
24980
+ } else {
24981
+ console.log(`${colors.green}\u2713 You're running the latest version (${getCurrentVersion()})${colors.reset}`);
24982
+ }
24983
+ return;
24984
+ }
23253
24985
  if (args[0] === "--setup") {
24986
+ await updatePromise;
23254
24987
  await setup();
23255
24988
  return;
23256
24989
  }
23257
24990
  if (args[0] === "--models") {
24991
+ await updatePromise;
23258
24992
  printModels();
23259
24993
  return;
23260
24994
  }
@@ -23320,8 +25054,24 @@ ${colors.bold}Available Themes${colors.reset}
23320
25054
  console.log(`${colors.success}\u2713 Theme set to ${themeName}${colors.reset}`);
23321
25055
  return;
23322
25056
  }
25057
+ if (args[0] === "--repo-context") {
25058
+ const config2 = loadConfig();
25059
+ config2.repoContext = true;
25060
+ saveConfig(config2);
25061
+ console.log(`${colors.success}\u2713 Project context enabled${colors.reset}`);
25062
+ console.log(`${colors.dim}Magic Shell will now detect package.json scripts, Makefile targets, etc.${colors.reset}`);
25063
+ return;
25064
+ }
25065
+ if (args[0] === "--no-repo-context") {
25066
+ const config2 = loadConfig();
25067
+ config2.repoContext = false;
25068
+ saveConfig(config2);
25069
+ console.log(`${colors.success}\u2713 Project context disabled${colors.reset}`);
25070
+ return;
25071
+ }
23323
25072
  let execute = false;
23324
25073
  let dryRun = false;
25074
+ let repoContext = undefined;
23325
25075
  let queryParts = [];
23326
25076
  for (let i = 0;i < args.length; i++) {
23327
25077
  const arg = args[i];
@@ -23329,6 +25079,10 @@ ${colors.bold}Available Themes${colors.reset}
23329
25079
  execute = true;
23330
25080
  } else if (arg === "-n" || arg === "--dry-run") {
23331
25081
  dryRun = true;
25082
+ } else if (arg === "-r" || arg === "--repo-context") {
25083
+ repoContext = true;
25084
+ } else if (arg === "--no-repo-context") {
25085
+ repoContext = false;
23332
25086
  } else if (!arg.startsWith("-")) {
23333
25087
  queryParts.push(arg);
23334
25088
  }
@@ -23339,7 +25093,7 @@ ${colors.bold}Available Themes${colors.reset}
23339
25093
  console.error(`Usage: msh "your query here"`);
23340
25094
  process.exit(1);
23341
25095
  }
23342
- await translate(query, { execute, dryRun });
25096
+ await translate(query, { execute, dryRun, repoContext });
23343
25097
  }
23344
25098
  main3().catch((error) => {
23345
25099
  console.error(`${colors.red}Error: ${error.message}${colors.reset}`);