@austinthesing/magic-shell 0.2.27 → 0.2.29

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 +13 -8
  2. package/dist/cli.js +291 -45
  3. package/dist/tui.js +291 -45
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -145,7 +145,7 @@ Launch with `mshell` for a full interactive experience.
145
145
 
146
146
  ### Keyboard Shortcuts
147
147
 
148
- Magic Shell follows OpenCode-style TUI shortcuts where they map cleanly:
148
+ Magic Shell follows OpenCode-style TUI shortcuts where they fit cleanly:
149
149
 
150
150
  | Shortcut | Action |
151
151
  | ---------- | ---------------------- |
@@ -154,23 +154,28 @@ Magic Shell follows OpenCode-style TUI shortcuts where they map cleanly:
154
154
  | `Ctrl+X T` | Change theme |
155
155
  | `Ctrl+X Q` | Exit |
156
156
  | `Ctrl+T` | Cycle thinking level |
157
- | `Ctrl+C` | Exit / Cancel active UI |
157
+ | `Ctrl+C` | Exit, or cancel/close the active dialog first |
158
158
  | `Ctrl+D` | Exit |
159
- | `Esc` | Close dialogs |
159
+ | `Esc` | Close dialogs or cancel pending actions |
160
160
 
161
- ### Direct Commands in TUI
161
+ ### Slash Commands in TUI
162
162
 
163
- You can also type commands directly in the TUI:
163
+ Typing `/` opens a slash command menu under the prompt. Type to filter, use arrow keys to move, `Tab` to complete, and `Enter` to run the selected command.
164
+
165
+ You can also submit slash commands directly:
164
166
 
165
167
  - `!help` or `/help` - Show help
166
- - `!model` or `/model` - Change model
167
- - `!provider` or `/provider` - Switch provider
168
+ - `!model` or `/models` - Change model
169
+ - `!provider` or `/providers` - Switch provider
170
+ - `!theme` or `/themes` - Change theme
168
171
  - `!dry` or `/dry` - Toggle dry-run mode
172
+ - `!thinking` or `/thinking` - Cycle thinking level
169
173
  - `!config` or `/config` - Show current configuration
170
174
  - `!history` or `/history` - Show command history
171
175
  - `!clear` or `/clear` - Clear output
176
+ - `!exit` or `/exit` - Exit the TUI
172
177
 
173
- > **Note:** Both `!` and `/` prefixes work for all commands. Use whichever feels more natural!
178
+ Both `!` and `/` prefixes work for these commands.
174
179
 
175
180
  ## AI Providers
176
181
 
package/dist/cli.js CHANGED
@@ -76817,6 +76817,10 @@ var inputHintText;
76817
76817
  var helpBarText;
76818
76818
  var modelSelector = null;
76819
76819
  var providerSelector = null;
76820
+ var slashCommandMatches = [];
76821
+ var slashCommandSelectedIndex = 0;
76822
+ var slashCommandSyncQueued = false;
76823
+ var slashCommandExecuting = false;
76820
76824
  var pendingMessageId = null;
76821
76825
  var awaitingConfirmation = false;
76822
76826
  function generateMessageId() {
@@ -77101,6 +77105,10 @@ function createMainUI() {
77101
77105
  { name: "j", ctrl: true, action: "newline" }
77102
77106
  ],
77103
77107
  onSubmit: () => {
77108
+ if (slashCommandModal && slashCommandMatches.length > 0) {
77109
+ executeSelectedSlashCommand();
77110
+ return;
77111
+ }
77104
77112
  const value = inputField.editBuffer.getText();
77105
77113
  handleInput(value);
77106
77114
  }
@@ -77119,6 +77127,7 @@ function createMainUI() {
77119
77127
  });
77120
77128
  mainContainer.add(helpBarText);
77121
77129
  renderer.keyInput.on("keypress", handleKeypress);
77130
+ renderer.keyInput.on("keypress", queueSlashCommandSync);
77122
77131
  inputField.focus();
77123
77132
  }
77124
77133
  function getStatusBarContent() {
@@ -77433,8 +77442,14 @@ async function handleInput(value) {
77433
77442
  const input = value.trim();
77434
77443
  if (!input)
77435
77444
  return;
77445
+ if (input.startsWith("/") && await tryHandleSlashCommand(input)) {
77446
+ inputField.setText("");
77447
+ closeSlashCommandMenu();
77448
+ return;
77449
+ }
77436
77450
  inputField.setText("");
77437
- if (input.startsWith("!") || input.startsWith("/")) {
77451
+ closeSlashCommandMenu();
77452
+ if (input.startsWith("!")) {
77438
77453
  await handleSpecialCommand(input);
77439
77454
  return;
77440
77455
  }
@@ -77445,6 +77460,25 @@ async function handleInput(value) {
77445
77460
  }
77446
77461
  await translateAndProcess(input);
77447
77462
  }
77463
+ async function executeSelectedSlashCommand() {
77464
+ if (slashCommandExecuting || !slashCommandModal || slashCommandMatches.length === 0)
77465
+ return;
77466
+ slashCommandExecuting = true;
77467
+ try {
77468
+ const selected = slashCommandMatches[Math.min(slashCommandSelectedIndex, slashCommandMatches.length - 1)];
77469
+ inputField.setText("");
77470
+ closeSlashCommandMenu();
77471
+ await selected.action();
77472
+ } finally {
77473
+ slashCommandExecuting = false;
77474
+ }
77475
+ }
77476
+ async function tryHandleSlashCommand(input) {
77477
+ if (!input.startsWith("/"))
77478
+ return false;
77479
+ await handleSpecialCommand(input);
77480
+ return true;
77481
+ }
77448
77482
  function isDirectCommand(input) {
77449
77483
  const directCommands = ["ls", "pwd", "cd", "cat", "echo", "mkdir", "touch", "rm", "cp", "mv", "git", "npm", "bun", "node", "python", "pip", "brew", "apt", "docker", "kubectl"];
77450
77484
  const firstWord = input.split(/\s+/)[0].toLowerCase();
@@ -77620,6 +77654,7 @@ async function handleSpecialCommand(input) {
77620
77654
  }
77621
77655
  }
77622
77656
  function clearChat() {
77657
+ closeSlashCommandMenu();
77623
77658
  for (const msg of chatMessages) {
77624
77659
  chatScrollBox.remove(`msg-${msg.id}`);
77625
77660
  }
@@ -77923,7 +77958,10 @@ function showThemeSelector() {
77923
77958
  renderer.keyInput.on("keypress", escHandler);
77924
77959
  themeSelector.focus();
77925
77960
  }
77926
- var commandPalette = null;
77961
+ var MODAL_LIST_WIDTH = 55;
77962
+ var MODAL_LIST_MAX_ITEMS = 12;
77963
+ var commandPaletteModal = null;
77964
+ var slashCommandModal = null;
77927
77965
  var commandPaletteQuery = "";
77928
77966
  var chordMode = "none";
77929
77967
  function cycleThinkingLevel() {
@@ -78031,56 +78069,236 @@ function getCommandPaletteOptions() {
78031
78069
  }
78032
78070
  ];
78033
78071
  }
78034
- function showCommandPalette() {
78035
- if (commandPalette) {
78036
- return;
78037
- }
78038
- commandPaletteQuery = "";
78039
- const commands = getCommandPaletteOptions();
78040
- const paletteWidth = 55;
78072
+ function getSlashCommands() {
78073
+ return [
78074
+ {
78075
+ slash: "help",
78076
+ name: "Help",
78077
+ description: "Show commands and shortcuts",
78078
+ action: () => showHelp()
78079
+ },
78080
+ {
78081
+ slash: "models",
78082
+ name: "Models",
78083
+ description: `Change model · ${currentModel.name}`,
78084
+ action: () => showModelSelector()
78085
+ },
78086
+ {
78087
+ slash: "providers",
78088
+ name: "Providers",
78089
+ description: `Switch provider · ${getProviderDisplayName(config2.provider)}`,
78090
+ action: () => switchProvider()
78091
+ },
78092
+ {
78093
+ slash: "themes",
78094
+ name: "Themes",
78095
+ description: `Change theme · ${getTheme().name}`,
78096
+ action: () => showThemeSelector()
78097
+ },
78098
+ {
78099
+ slash: "dry",
78100
+ name: "Dry Run",
78101
+ description: dryRunMode ? "Turn dry-run off" : "Turn dry-run on",
78102
+ action: () => {
78103
+ dryRunMode = !dryRunMode;
78104
+ statusBarText.content = getStatusBarContent();
78105
+ addSystemMessage(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`);
78106
+ }
78107
+ },
78108
+ {
78109
+ slash: "thinking",
78110
+ name: "Thinking",
78111
+ description: `Cycle thinking level · ${config2.thinkingLevel}`,
78112
+ action: () => cycleThinkingLevel()
78113
+ },
78114
+ {
78115
+ slash: "config",
78116
+ name: "Config",
78117
+ description: "Show current configuration",
78118
+ action: () => showConfig()
78119
+ },
78120
+ {
78121
+ slash: "history",
78122
+ name: "History",
78123
+ description: `${history.length} commands`,
78124
+ action: () => showHistory()
78125
+ },
78126
+ {
78127
+ slash: "clear",
78128
+ name: "Clear",
78129
+ description: "Clear the chat history",
78130
+ action: () => clearChat()
78131
+ },
78132
+ {
78133
+ slash: "exit",
78134
+ name: "Exit",
78135
+ description: "Close magic-shell",
78136
+ action: () => {
78137
+ renderer.destroy();
78138
+ process.exit(0);
78139
+ }
78140
+ }
78141
+ ];
78142
+ }
78143
+ function getSlashCommandMatches(inputText) {
78144
+ if (!inputText.startsWith("/"))
78145
+ return [];
78146
+ const slashBody = inputText.slice(1);
78147
+ if (slashBody.includes(" "))
78148
+ return [];
78149
+ const query = slashBody.trim().toLowerCase();
78150
+ return getSlashCommands().filter((cmd) => {
78151
+ if (!query)
78152
+ return true;
78153
+ return `${cmd.slash} ${cmd.name} ${cmd.description}`.toLowerCase().includes(query);
78154
+ });
78155
+ }
78156
+ function openModalList(config3) {
78157
+ const theme = getTheme();
78158
+ const width = MODAL_LIST_WIDTH;
78041
78159
  const termWidth = process.stdout.columns || 80;
78042
- const paletteLeft = Math.max(2, Math.floor((termWidth - paletteWidth) / 2));
78160
+ const left = Math.max(2, Math.floor((termWidth - width) / 2));
78161
+ const itemCount = Math.max(config3.options.length, 1);
78162
+ const listHeight = Math.min(itemCount + 2, MODAL_LIST_MAX_ITEMS);
78163
+ const containerHeight = listHeight + 2;
78043
78164
  const container = new BoxRenderable(renderer, {
78044
- id: "command-palette-container",
78165
+ id: config3.containerId,
78045
78166
  position: "absolute",
78046
- left: paletteLeft,
78167
+ left,
78047
78168
  top: 3,
78048
- width: paletteWidth,
78049
- height: Math.min(commands.length + 4, 16),
78050
- backgroundColor: "#1e293b",
78169
+ width,
78170
+ height: containerHeight,
78171
+ backgroundColor: theme.colors.backgroundPanel,
78051
78172
  border: true,
78052
- borderColor: "#60a5fa",
78173
+ borderColor: theme.colors.primary,
78053
78174
  borderStyle: "single",
78054
- title: "Commands",
78175
+ title: config3.title,
78055
78176
  titleAlignment: "center",
78056
78177
  zIndex: 200,
78057
78178
  padding: 1
78058
78179
  });
78059
78180
  renderer.root.add(container);
78060
- commandPalette = new SelectRenderable(renderer, {
78061
- id: "command-palette-select",
78181
+ const selector = new SelectRenderable(renderer, {
78182
+ id: config3.selectorId,
78062
78183
  width: "100%",
78063
- height: Math.min(commands.length + 2, 12),
78064
- options: getCommandPaletteSelectOptions(),
78184
+ height: listHeight,
78185
+ options: config3.options,
78065
78186
  backgroundColor: "transparent",
78066
78187
  focusedBackgroundColor: "transparent",
78067
- selectedBackgroundColor: "#334155",
78068
- textColor: "#e2e8f0",
78069
- selectedTextColor: "#60a5fa",
78070
- descriptionColor: "#64748b",
78071
- selectedDescriptionColor: "#94a3b8",
78188
+ selectedBackgroundColor: theme.colors.backgroundElement,
78189
+ textColor: theme.colors.text,
78190
+ selectedTextColor: theme.colors.primary,
78191
+ descriptionColor: theme.colors.textMuted,
78192
+ selectedDescriptionColor: theme.colors.textMuted,
78072
78193
  showDescription: true,
78073
78194
  wrapSelection: true
78074
78195
  });
78075
- container.add(commandPalette);
78076
- commandPalette.on(SelectRenderableEvents.ITEM_SELECTED, async (_2, option) => {
78077
- if (!option.value)
78078
- return;
78079
- const cmd = option.value;
78080
- closeCommandPalette();
78081
- await cmd.action();
78196
+ container.add(selector);
78197
+ const handle = {
78198
+ containerId: config3.containerId,
78199
+ container,
78200
+ selector,
78201
+ updateOptions(options, selectedIndex = 0) {
78202
+ const count = Math.max(options.length, 1);
78203
+ const newListHeight = Math.min(count + 2, MODAL_LIST_MAX_ITEMS);
78204
+ selector.options = options;
78205
+ selector.height = newListHeight;
78206
+ container.height = newListHeight + 2;
78207
+ selector.setSelectedIndex(Math.min(selectedIndex, Math.max(options.length - 1, 0)));
78208
+ },
78209
+ setSelectedIndex(index) {
78210
+ selector.setSelectedIndex(index);
78211
+ },
78212
+ close() {
78213
+ renderer.root.remove(config3.containerId);
78214
+ inputField?.focus();
78215
+ }
78216
+ };
78217
+ if (config3.onSelect) {
78218
+ selector.on(SelectRenderableEvents.ITEM_SELECTED, async (index, option) => {
78219
+ await config3.onSelect?.(index, option);
78220
+ });
78221
+ }
78222
+ const initialIndex = config3.selectedIndex ?? 0;
78223
+ if (config3.focusList !== false) {
78224
+ selector.focus();
78225
+ } else {
78226
+ selector.setSelectedIndex(initialIndex);
78227
+ }
78228
+ return handle;
78229
+ }
78230
+ function getSlashCommandSelectOptions() {
78231
+ return slashCommandMatches.map((cmd) => ({
78232
+ name: `/${cmd.slash}`,
78233
+ description: cmd.description,
78234
+ value: cmd
78235
+ }));
78236
+ }
78237
+ function closeSlashCommandMenu() {
78238
+ if (slashCommandModal) {
78239
+ slashCommandModal.close();
78240
+ slashCommandModal = null;
78241
+ }
78242
+ slashCommandMatches = [];
78243
+ slashCommandSelectedIndex = 0;
78244
+ }
78245
+ function updateSlashCommandModalOptions() {
78246
+ if (!slashCommandModal)
78247
+ return;
78248
+ slashCommandModal.updateOptions(getSlashCommandSelectOptions(), slashCommandSelectedIndex);
78249
+ }
78250
+ function syncSlashCommandMenu() {
78251
+ const inputText = inputField.editBuffer.getText();
78252
+ const matches = getSlashCommandMatches(inputText);
78253
+ if (matches.length === 0) {
78254
+ closeSlashCommandMenu();
78255
+ return;
78256
+ }
78257
+ slashCommandMatches = matches;
78258
+ slashCommandSelectedIndex = Math.min(slashCommandSelectedIndex, slashCommandMatches.length - 1);
78259
+ const options = getSlashCommandSelectOptions();
78260
+ if (slashCommandModal) {
78261
+ slashCommandModal.updateOptions(options, slashCommandSelectedIndex);
78262
+ return;
78263
+ }
78264
+ slashCommandModal = openModalList({
78265
+ containerId: "slash-command-modal",
78266
+ selectorId: "slash-command-select",
78267
+ title: "Commands",
78268
+ options,
78269
+ focusList: false,
78270
+ selectedIndex: slashCommandSelectedIndex
78271
+ });
78272
+ }
78273
+ function queueSlashCommandSync() {
78274
+ if (slashCommandSyncQueued)
78275
+ return;
78276
+ slashCommandSyncQueued = true;
78277
+ setTimeout(() => {
78278
+ slashCommandSyncQueued = false;
78279
+ if (inputField)
78280
+ syncSlashCommandMenu();
78281
+ }, 0);
78282
+ }
78283
+ function showCommandPalette() {
78284
+ if (commandPaletteModal) {
78285
+ return;
78286
+ }
78287
+ commandPaletteQuery = "";
78288
+ commandPaletteModal = openModalList({
78289
+ containerId: "command-palette-container",
78290
+ selectorId: "command-palette-select",
78291
+ title: "Commands",
78292
+ options: getCommandPaletteSelectOptions(),
78293
+ focusList: true,
78294
+ onSelect: async (_2, option) => {
78295
+ if (!option.value)
78296
+ return;
78297
+ const cmd = option.value;
78298
+ closeCommandPalette();
78299
+ await cmd.action();
78300
+ }
78082
78301
  });
78083
- commandPalette.focus();
78084
78302
  }
78085
78303
  function getCommandPaletteSelectOptions() {
78086
78304
  const query = commandPaletteQuery.toLowerCase();
@@ -78103,21 +78321,41 @@ function getCommandPaletteSelectOptions() {
78103
78321
  }));
78104
78322
  }
78105
78323
  function updateCommandPaletteOptions() {
78106
- if (!commandPalette)
78324
+ if (!commandPaletteModal)
78107
78325
  return;
78108
- commandPalette.options = getCommandPaletteSelectOptions();
78109
- commandPalette.setSelectedIndex(0);
78326
+ commandPaletteModal.updateOptions(getCommandPaletteSelectOptions(), 0);
78110
78327
  }
78111
78328
  function closeCommandPalette() {
78112
- if (commandPalette) {
78113
- renderer.root.remove("command-palette-container");
78114
- commandPalette = null;
78329
+ if (commandPaletteModal) {
78330
+ commandPaletteModal.close();
78331
+ commandPaletteModal = null;
78115
78332
  commandPaletteQuery = "";
78116
- inputField?.focus();
78117
78333
  }
78118
78334
  }
78119
78335
  function handleKeypress(key) {
78120
78336
  const commands = getCommandPaletteOptions();
78337
+ if (slashCommandModal && slashCommandMatches.length > 0) {
78338
+ if (key.name === "up") {
78339
+ slashCommandSelectedIndex = slashCommandSelectedIndex === 0 ? slashCommandMatches.length - 1 : slashCommandSelectedIndex - 1;
78340
+ updateSlashCommandModalOptions();
78341
+ return;
78342
+ }
78343
+ if (key.name === "down") {
78344
+ slashCommandSelectedIndex = (slashCommandSelectedIndex + 1) % slashCommandMatches.length;
78345
+ updateSlashCommandModalOptions();
78346
+ return;
78347
+ }
78348
+ if (key.name === "tab") {
78349
+ inputField.setText(`/${slashCommandMatches[slashCommandSelectedIndex].slash}`);
78350
+ syncSlashCommandMenu();
78351
+ inputField.focus();
78352
+ return;
78353
+ }
78354
+ if (key.name === "return" && !key.shift && !key.ctrl && !key.meta) {
78355
+ executeSelectedSlashCommand();
78356
+ return;
78357
+ }
78358
+ }
78121
78359
  if (key.ctrl && key.name === "p") {
78122
78360
  showCommandPalette();
78123
78361
  return;
@@ -78144,7 +78382,7 @@ function handleKeypress(key) {
78144
78382
  }
78145
78383
  return;
78146
78384
  }
78147
- if (commandPalette) {
78385
+ if (commandPaletteModal) {
78148
78386
  if (key.name === "backspace" || key.name === "delete") {
78149
78387
  commandPaletteQuery = commandPaletteQuery.slice(0, -1);
78150
78388
  updateCommandPaletteOptions();
@@ -78158,10 +78396,14 @@ function handleKeypress(key) {
78158
78396
  }
78159
78397
  }
78160
78398
  if (key.ctrl && key.name === "c") {
78161
- if (commandPalette) {
78399
+ if (commandPaletteModal) {
78162
78400
  closeCommandPalette();
78163
78401
  return;
78164
78402
  }
78403
+ if (slashCommandMatches.length > 0) {
78404
+ closeSlashCommandMenu();
78405
+ return;
78406
+ }
78165
78407
  if (modelSelector) {
78166
78408
  renderer.root.remove("model-selector-container");
78167
78409
  modelSelector = null;
@@ -78185,10 +78427,14 @@ function handleKeypress(key) {
78185
78427
  }
78186
78428
  if (key.name === "escape") {
78187
78429
  chordMode = "none";
78188
- if (commandPalette) {
78430
+ if (commandPaletteModal) {
78189
78431
  closeCommandPalette();
78190
78432
  return;
78191
78433
  }
78434
+ if (slashCommandMatches.length > 0) {
78435
+ closeSlashCommandMenu();
78436
+ return;
78437
+ }
78192
78438
  if (modelSelector) {
78193
78439
  renderer.root.remove("model-selector-container");
78194
78440
  modelSelector = null;
@@ -78228,7 +78474,7 @@ function handleKeypress(key) {
78228
78474
  addSystemMessage(`Copied to clipboard: ${msg.command}`);
78229
78475
  }
78230
78476
  }
78231
- if (key.name === "o" && !awaitingConfirmation && !commandPalette && !modelSelector) {
78477
+ if (key.name === "o" && !awaitingConfirmation && !commandPaletteModal && !modelSelector) {
78232
78478
  toggleLastResultExpand();
78233
78479
  }
78234
78480
  }
package/dist/tui.js CHANGED
@@ -76817,6 +76817,10 @@ var inputHintText;
76817
76817
  var helpBarText;
76818
76818
  var modelSelector = null;
76819
76819
  var providerSelector = null;
76820
+ var slashCommandMatches = [];
76821
+ var slashCommandSelectedIndex = 0;
76822
+ var slashCommandSyncQueued = false;
76823
+ var slashCommandExecuting = false;
76820
76824
  var pendingMessageId = null;
76821
76825
  var awaitingConfirmation = false;
76822
76826
  function generateMessageId() {
@@ -77101,6 +77105,10 @@ function createMainUI() {
77101
77105
  { name: "j", ctrl: true, action: "newline" }
77102
77106
  ],
77103
77107
  onSubmit: () => {
77108
+ if (slashCommandModal && slashCommandMatches.length > 0) {
77109
+ executeSelectedSlashCommand();
77110
+ return;
77111
+ }
77104
77112
  const value = inputField.editBuffer.getText();
77105
77113
  handleInput(value);
77106
77114
  }
@@ -77119,6 +77127,7 @@ function createMainUI() {
77119
77127
  });
77120
77128
  mainContainer.add(helpBarText);
77121
77129
  renderer.keyInput.on("keypress", handleKeypress);
77130
+ renderer.keyInput.on("keypress", queueSlashCommandSync);
77122
77131
  inputField.focus();
77123
77132
  }
77124
77133
  function getStatusBarContent() {
@@ -77433,8 +77442,14 @@ async function handleInput(value) {
77433
77442
  const input = value.trim();
77434
77443
  if (!input)
77435
77444
  return;
77445
+ if (input.startsWith("/") && await tryHandleSlashCommand(input)) {
77446
+ inputField.setText("");
77447
+ closeSlashCommandMenu();
77448
+ return;
77449
+ }
77436
77450
  inputField.setText("");
77437
- if (input.startsWith("!") || input.startsWith("/")) {
77451
+ closeSlashCommandMenu();
77452
+ if (input.startsWith("!")) {
77438
77453
  await handleSpecialCommand(input);
77439
77454
  return;
77440
77455
  }
@@ -77445,6 +77460,25 @@ async function handleInput(value) {
77445
77460
  }
77446
77461
  await translateAndProcess(input);
77447
77462
  }
77463
+ async function executeSelectedSlashCommand() {
77464
+ if (slashCommandExecuting || !slashCommandModal || slashCommandMatches.length === 0)
77465
+ return;
77466
+ slashCommandExecuting = true;
77467
+ try {
77468
+ const selected = slashCommandMatches[Math.min(slashCommandSelectedIndex, slashCommandMatches.length - 1)];
77469
+ inputField.setText("");
77470
+ closeSlashCommandMenu();
77471
+ await selected.action();
77472
+ } finally {
77473
+ slashCommandExecuting = false;
77474
+ }
77475
+ }
77476
+ async function tryHandleSlashCommand(input) {
77477
+ if (!input.startsWith("/"))
77478
+ return false;
77479
+ await handleSpecialCommand(input);
77480
+ return true;
77481
+ }
77448
77482
  function isDirectCommand(input) {
77449
77483
  const directCommands = ["ls", "pwd", "cd", "cat", "echo", "mkdir", "touch", "rm", "cp", "mv", "git", "npm", "bun", "node", "python", "pip", "brew", "apt", "docker", "kubectl"];
77450
77484
  const firstWord = input.split(/\s+/)[0].toLowerCase();
@@ -77620,6 +77654,7 @@ async function handleSpecialCommand(input) {
77620
77654
  }
77621
77655
  }
77622
77656
  function clearChat() {
77657
+ closeSlashCommandMenu();
77623
77658
  for (const msg of chatMessages) {
77624
77659
  chatScrollBox.remove(`msg-${msg.id}`);
77625
77660
  }
@@ -77923,7 +77958,10 @@ function showThemeSelector() {
77923
77958
  renderer.keyInput.on("keypress", escHandler);
77924
77959
  themeSelector.focus();
77925
77960
  }
77926
- var commandPalette = null;
77961
+ var MODAL_LIST_WIDTH = 55;
77962
+ var MODAL_LIST_MAX_ITEMS = 12;
77963
+ var commandPaletteModal = null;
77964
+ var slashCommandModal = null;
77927
77965
  var commandPaletteQuery = "";
77928
77966
  var chordMode = "none";
77929
77967
  function cycleThinkingLevel() {
@@ -78031,56 +78069,236 @@ function getCommandPaletteOptions() {
78031
78069
  }
78032
78070
  ];
78033
78071
  }
78034
- function showCommandPalette() {
78035
- if (commandPalette) {
78036
- return;
78037
- }
78038
- commandPaletteQuery = "";
78039
- const commands = getCommandPaletteOptions();
78040
- const paletteWidth = 55;
78072
+ function getSlashCommands() {
78073
+ return [
78074
+ {
78075
+ slash: "help",
78076
+ name: "Help",
78077
+ description: "Show commands and shortcuts",
78078
+ action: () => showHelp()
78079
+ },
78080
+ {
78081
+ slash: "models",
78082
+ name: "Models",
78083
+ description: `Change model · ${currentModel.name}`,
78084
+ action: () => showModelSelector()
78085
+ },
78086
+ {
78087
+ slash: "providers",
78088
+ name: "Providers",
78089
+ description: `Switch provider · ${getProviderDisplayName(config2.provider)}`,
78090
+ action: () => switchProvider()
78091
+ },
78092
+ {
78093
+ slash: "themes",
78094
+ name: "Themes",
78095
+ description: `Change theme · ${getTheme().name}`,
78096
+ action: () => showThemeSelector()
78097
+ },
78098
+ {
78099
+ slash: "dry",
78100
+ name: "Dry Run",
78101
+ description: dryRunMode ? "Turn dry-run off" : "Turn dry-run on",
78102
+ action: () => {
78103
+ dryRunMode = !dryRunMode;
78104
+ statusBarText.content = getStatusBarContent();
78105
+ addSystemMessage(`Dry-run mode: ${dryRunMode ? "ON" : "OFF"}`);
78106
+ }
78107
+ },
78108
+ {
78109
+ slash: "thinking",
78110
+ name: "Thinking",
78111
+ description: `Cycle thinking level · ${config2.thinkingLevel}`,
78112
+ action: () => cycleThinkingLevel()
78113
+ },
78114
+ {
78115
+ slash: "config",
78116
+ name: "Config",
78117
+ description: "Show current configuration",
78118
+ action: () => showConfig()
78119
+ },
78120
+ {
78121
+ slash: "history",
78122
+ name: "History",
78123
+ description: `${history.length} commands`,
78124
+ action: () => showHistory()
78125
+ },
78126
+ {
78127
+ slash: "clear",
78128
+ name: "Clear",
78129
+ description: "Clear the chat history",
78130
+ action: () => clearChat()
78131
+ },
78132
+ {
78133
+ slash: "exit",
78134
+ name: "Exit",
78135
+ description: "Close magic-shell",
78136
+ action: () => {
78137
+ renderer.destroy();
78138
+ process.exit(0);
78139
+ }
78140
+ }
78141
+ ];
78142
+ }
78143
+ function getSlashCommandMatches(inputText) {
78144
+ if (!inputText.startsWith("/"))
78145
+ return [];
78146
+ const slashBody = inputText.slice(1);
78147
+ if (slashBody.includes(" "))
78148
+ return [];
78149
+ const query = slashBody.trim().toLowerCase();
78150
+ return getSlashCommands().filter((cmd) => {
78151
+ if (!query)
78152
+ return true;
78153
+ return `${cmd.slash} ${cmd.name} ${cmd.description}`.toLowerCase().includes(query);
78154
+ });
78155
+ }
78156
+ function openModalList(config3) {
78157
+ const theme = getTheme();
78158
+ const width = MODAL_LIST_WIDTH;
78041
78159
  const termWidth = process.stdout.columns || 80;
78042
- const paletteLeft = Math.max(2, Math.floor((termWidth - paletteWidth) / 2));
78160
+ const left = Math.max(2, Math.floor((termWidth - width) / 2));
78161
+ const itemCount = Math.max(config3.options.length, 1);
78162
+ const listHeight = Math.min(itemCount + 2, MODAL_LIST_MAX_ITEMS);
78163
+ const containerHeight = listHeight + 2;
78043
78164
  const container = new BoxRenderable(renderer, {
78044
- id: "command-palette-container",
78165
+ id: config3.containerId,
78045
78166
  position: "absolute",
78046
- left: paletteLeft,
78167
+ left,
78047
78168
  top: 3,
78048
- width: paletteWidth,
78049
- height: Math.min(commands.length + 4, 16),
78050
- backgroundColor: "#1e293b",
78169
+ width,
78170
+ height: containerHeight,
78171
+ backgroundColor: theme.colors.backgroundPanel,
78051
78172
  border: true,
78052
- borderColor: "#60a5fa",
78173
+ borderColor: theme.colors.primary,
78053
78174
  borderStyle: "single",
78054
- title: "Commands",
78175
+ title: config3.title,
78055
78176
  titleAlignment: "center",
78056
78177
  zIndex: 200,
78057
78178
  padding: 1
78058
78179
  });
78059
78180
  renderer.root.add(container);
78060
- commandPalette = new SelectRenderable(renderer, {
78061
- id: "command-palette-select",
78181
+ const selector = new SelectRenderable(renderer, {
78182
+ id: config3.selectorId,
78062
78183
  width: "100%",
78063
- height: Math.min(commands.length + 2, 12),
78064
- options: getCommandPaletteSelectOptions(),
78184
+ height: listHeight,
78185
+ options: config3.options,
78065
78186
  backgroundColor: "transparent",
78066
78187
  focusedBackgroundColor: "transparent",
78067
- selectedBackgroundColor: "#334155",
78068
- textColor: "#e2e8f0",
78069
- selectedTextColor: "#60a5fa",
78070
- descriptionColor: "#64748b",
78071
- selectedDescriptionColor: "#94a3b8",
78188
+ selectedBackgroundColor: theme.colors.backgroundElement,
78189
+ textColor: theme.colors.text,
78190
+ selectedTextColor: theme.colors.primary,
78191
+ descriptionColor: theme.colors.textMuted,
78192
+ selectedDescriptionColor: theme.colors.textMuted,
78072
78193
  showDescription: true,
78073
78194
  wrapSelection: true
78074
78195
  });
78075
- container.add(commandPalette);
78076
- commandPalette.on(SelectRenderableEvents.ITEM_SELECTED, async (_2, option) => {
78077
- if (!option.value)
78078
- return;
78079
- const cmd = option.value;
78080
- closeCommandPalette();
78081
- await cmd.action();
78196
+ container.add(selector);
78197
+ const handle = {
78198
+ containerId: config3.containerId,
78199
+ container,
78200
+ selector,
78201
+ updateOptions(options, selectedIndex = 0) {
78202
+ const count = Math.max(options.length, 1);
78203
+ const newListHeight = Math.min(count + 2, MODAL_LIST_MAX_ITEMS);
78204
+ selector.options = options;
78205
+ selector.height = newListHeight;
78206
+ container.height = newListHeight + 2;
78207
+ selector.setSelectedIndex(Math.min(selectedIndex, Math.max(options.length - 1, 0)));
78208
+ },
78209
+ setSelectedIndex(index) {
78210
+ selector.setSelectedIndex(index);
78211
+ },
78212
+ close() {
78213
+ renderer.root.remove(config3.containerId);
78214
+ inputField?.focus();
78215
+ }
78216
+ };
78217
+ if (config3.onSelect) {
78218
+ selector.on(SelectRenderableEvents.ITEM_SELECTED, async (index, option) => {
78219
+ await config3.onSelect?.(index, option);
78220
+ });
78221
+ }
78222
+ const initialIndex = config3.selectedIndex ?? 0;
78223
+ if (config3.focusList !== false) {
78224
+ selector.focus();
78225
+ } else {
78226
+ selector.setSelectedIndex(initialIndex);
78227
+ }
78228
+ return handle;
78229
+ }
78230
+ function getSlashCommandSelectOptions() {
78231
+ return slashCommandMatches.map((cmd) => ({
78232
+ name: `/${cmd.slash}`,
78233
+ description: cmd.description,
78234
+ value: cmd
78235
+ }));
78236
+ }
78237
+ function closeSlashCommandMenu() {
78238
+ if (slashCommandModal) {
78239
+ slashCommandModal.close();
78240
+ slashCommandModal = null;
78241
+ }
78242
+ slashCommandMatches = [];
78243
+ slashCommandSelectedIndex = 0;
78244
+ }
78245
+ function updateSlashCommandModalOptions() {
78246
+ if (!slashCommandModal)
78247
+ return;
78248
+ slashCommandModal.updateOptions(getSlashCommandSelectOptions(), slashCommandSelectedIndex);
78249
+ }
78250
+ function syncSlashCommandMenu() {
78251
+ const inputText = inputField.editBuffer.getText();
78252
+ const matches = getSlashCommandMatches(inputText);
78253
+ if (matches.length === 0) {
78254
+ closeSlashCommandMenu();
78255
+ return;
78256
+ }
78257
+ slashCommandMatches = matches;
78258
+ slashCommandSelectedIndex = Math.min(slashCommandSelectedIndex, slashCommandMatches.length - 1);
78259
+ const options = getSlashCommandSelectOptions();
78260
+ if (slashCommandModal) {
78261
+ slashCommandModal.updateOptions(options, slashCommandSelectedIndex);
78262
+ return;
78263
+ }
78264
+ slashCommandModal = openModalList({
78265
+ containerId: "slash-command-modal",
78266
+ selectorId: "slash-command-select",
78267
+ title: "Commands",
78268
+ options,
78269
+ focusList: false,
78270
+ selectedIndex: slashCommandSelectedIndex
78271
+ });
78272
+ }
78273
+ function queueSlashCommandSync() {
78274
+ if (slashCommandSyncQueued)
78275
+ return;
78276
+ slashCommandSyncQueued = true;
78277
+ setTimeout(() => {
78278
+ slashCommandSyncQueued = false;
78279
+ if (inputField)
78280
+ syncSlashCommandMenu();
78281
+ }, 0);
78282
+ }
78283
+ function showCommandPalette() {
78284
+ if (commandPaletteModal) {
78285
+ return;
78286
+ }
78287
+ commandPaletteQuery = "";
78288
+ commandPaletteModal = openModalList({
78289
+ containerId: "command-palette-container",
78290
+ selectorId: "command-palette-select",
78291
+ title: "Commands",
78292
+ options: getCommandPaletteSelectOptions(),
78293
+ focusList: true,
78294
+ onSelect: async (_2, option) => {
78295
+ if (!option.value)
78296
+ return;
78297
+ const cmd = option.value;
78298
+ closeCommandPalette();
78299
+ await cmd.action();
78300
+ }
78082
78301
  });
78083
- commandPalette.focus();
78084
78302
  }
78085
78303
  function getCommandPaletteSelectOptions() {
78086
78304
  const query = commandPaletteQuery.toLowerCase();
@@ -78103,21 +78321,41 @@ function getCommandPaletteSelectOptions() {
78103
78321
  }));
78104
78322
  }
78105
78323
  function updateCommandPaletteOptions() {
78106
- if (!commandPalette)
78324
+ if (!commandPaletteModal)
78107
78325
  return;
78108
- commandPalette.options = getCommandPaletteSelectOptions();
78109
- commandPalette.setSelectedIndex(0);
78326
+ commandPaletteModal.updateOptions(getCommandPaletteSelectOptions(), 0);
78110
78327
  }
78111
78328
  function closeCommandPalette() {
78112
- if (commandPalette) {
78113
- renderer.root.remove("command-palette-container");
78114
- commandPalette = null;
78329
+ if (commandPaletteModal) {
78330
+ commandPaletteModal.close();
78331
+ commandPaletteModal = null;
78115
78332
  commandPaletteQuery = "";
78116
- inputField?.focus();
78117
78333
  }
78118
78334
  }
78119
78335
  function handleKeypress(key) {
78120
78336
  const commands = getCommandPaletteOptions();
78337
+ if (slashCommandModal && slashCommandMatches.length > 0) {
78338
+ if (key.name === "up") {
78339
+ slashCommandSelectedIndex = slashCommandSelectedIndex === 0 ? slashCommandMatches.length - 1 : slashCommandSelectedIndex - 1;
78340
+ updateSlashCommandModalOptions();
78341
+ return;
78342
+ }
78343
+ if (key.name === "down") {
78344
+ slashCommandSelectedIndex = (slashCommandSelectedIndex + 1) % slashCommandMatches.length;
78345
+ updateSlashCommandModalOptions();
78346
+ return;
78347
+ }
78348
+ if (key.name === "tab") {
78349
+ inputField.setText(`/${slashCommandMatches[slashCommandSelectedIndex].slash}`);
78350
+ syncSlashCommandMenu();
78351
+ inputField.focus();
78352
+ return;
78353
+ }
78354
+ if (key.name === "return" && !key.shift && !key.ctrl && !key.meta) {
78355
+ executeSelectedSlashCommand();
78356
+ return;
78357
+ }
78358
+ }
78121
78359
  if (key.ctrl && key.name === "p") {
78122
78360
  showCommandPalette();
78123
78361
  return;
@@ -78144,7 +78382,7 @@ function handleKeypress(key) {
78144
78382
  }
78145
78383
  return;
78146
78384
  }
78147
- if (commandPalette) {
78385
+ if (commandPaletteModal) {
78148
78386
  if (key.name === "backspace" || key.name === "delete") {
78149
78387
  commandPaletteQuery = commandPaletteQuery.slice(0, -1);
78150
78388
  updateCommandPaletteOptions();
@@ -78158,10 +78396,14 @@ function handleKeypress(key) {
78158
78396
  }
78159
78397
  }
78160
78398
  if (key.ctrl && key.name === "c") {
78161
- if (commandPalette) {
78399
+ if (commandPaletteModal) {
78162
78400
  closeCommandPalette();
78163
78401
  return;
78164
78402
  }
78403
+ if (slashCommandMatches.length > 0) {
78404
+ closeSlashCommandMenu();
78405
+ return;
78406
+ }
78165
78407
  if (modelSelector) {
78166
78408
  renderer.root.remove("model-selector-container");
78167
78409
  modelSelector = null;
@@ -78185,10 +78427,14 @@ function handleKeypress(key) {
78185
78427
  }
78186
78428
  if (key.name === "escape") {
78187
78429
  chordMode = "none";
78188
- if (commandPalette) {
78430
+ if (commandPaletteModal) {
78189
78431
  closeCommandPalette();
78190
78432
  return;
78191
78433
  }
78434
+ if (slashCommandMatches.length > 0) {
78435
+ closeSlashCommandMenu();
78436
+ return;
78437
+ }
78192
78438
  if (modelSelector) {
78193
78439
  renderer.root.remove("model-selector-container");
78194
78440
  modelSelector = null;
@@ -78228,7 +78474,7 @@ function handleKeypress(key) {
78228
78474
  addSystemMessage(`Copied to clipboard: ${msg.command}`);
78229
78475
  }
78230
78476
  }
78231
- if (key.name === "o" && !awaitingConfirmation && !commandPalette && !modelSelector) {
78477
+ if (key.name === "o" && !awaitingConfirmation && !commandPaletteModal && !modelSelector) {
78232
78478
  toggleLastResultExpand();
78233
78479
  }
78234
78480
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@austinthesing/magic-shell",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
4
4
  "description": "Natural language to terminal commands with safety features. Supports OpenCode Zen, OpenRouter, AI gateways, Workers AI, and custom models.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",