- Add --here position option to insert steps before the current step - Add --id option to specify custom step IDs for expression references - Add --help flag support for all step commands with detailed usage info - Update browser extension UI with ID field and improved position dropdown
24 KiB
Step Commands Refinements: --here, --id, and Help
Status: Draft
Author: GitHub Actions Team
Date: January 2026
Prerequisites: dap-step-manipulation.md (completed)
Progress Checklist
- Chunk 1:
--herePosition Option - Chunk 2:
--idOption for Step Identification - Chunk 3: Help Commands (
--help) - Chunk 4: Browser Extension UI Updates
Overview
This plan addresses three refinements to the step manipulation commands based on user feedback:
--hereposition option: Insert a step before the current step (the one you're paused at), so it runs immediately when stepping forward--idoption: Allow users to specify a custom step ID for later reference (e.g.,steps.<id>.outputs)- Help commands: Add
--helpflag support to all step commands for discoverability
Problem Statement
Issue 1: "First pending position" inserts in the wrong place
When paused before a step (e.g., checkout at position 1), using --first inserts the new step after the current step, not before it:
Before (paused at step 1):
▶ 1. Checkout
After "steps add run 'echo hello' --first":
▶ 1. Checkout
2. hello [ADDED] <-- Wrong! Should be before Checkout
Root cause: PositionType.First returns index 0 of the JobSteps queue, which contains steps after the current step. The current step is held separately in _currentStep.
Expected behavior: User wants to insert a step that will run immediately when they continue, i.e., before the current step.
Issue 2: No way to specify step ID
Dynamically added steps get auto-generated IDs like _dynamic_<guid>, making them impossible to reference in expressions like steps.<id>.outputs.foo.
Issue 3: Command options are hard to remember
With growing options (--name, --shell, --after, --before, --at, --first, --last, etc.), users need a way to quickly see available options without consulting documentation.
Chunk 1: --here Position Option
Goal: Add a new position option that inserts a step before the current step (the one paused at a breakpoint).
Design
| Flag | Meaning |
|---|---|
--here |
Insert before the current step, so it becomes the next step to run |
Behavior:
- Only valid when paused at a breakpoint
- Returns error if not paused: "Can only use --here when paused at a breakpoint"
- Inserts the new step such that it will execute immediately when the user continues/steps forward
Example:
Before (paused at step 1):
▶ 1. Checkout
2. Build
3. Test
After "steps add run 'echo hello' --here":
▶ 1. hello [ADDED] <-- New step runs next
2. Checkout
3. Build
4. Test
Files to Modify
| File | Changes |
|---|---|
StepCommandParser.cs |
Add Here to PositionType enum; add StepPosition.Here() factory; parse --here flag in add/move commands |
StepManipulator.cs |
Handle PositionType.Here in CalculateInsertIndex() and CalculateMoveTargetIndex() |
Implementation Details
StepCommandParser.cs:
// Add to PositionType enum
public enum PositionType
{
At,
After,
Before,
First,
Last,
Here // NEW: Insert before current step (requires paused state)
}
// Add factory method to StepPosition
public static StepPosition Here() => new StepPosition { Type = PositionType.Here };
// Update ToString()
PositionType.Here => "here",
// In ParseReplAddRunCommand and ParseReplAddUsesCommand, add case:
case "--here":
cmd.Position = StepPosition.Here();
break;
// Same for ParseReplMoveCommand
StepManipulator.cs:
// In CalculateInsertIndex():
case PositionType.Here:
{
// "Here" means before the current step
// Since current step is held separately (not in JobSteps queue),
// we need to:
// 1. Verify we're paused (have a current step)
// 2. Insert at position 0 of pending AND move current step after it
if (_currentStep == null)
{
throw new StepCommandException(StepCommandErrors.InvalidPosition,
"Can only use --here when paused at a breakpoint.");
}
// The new step goes at index 0, and we need to re-queue the current step
// Actually, we need a different approach - see "Special handling" below
}
Special handling for --here:
The current architecture has _currentStep held separately from JobSteps. To insert "before" the current step, we need to:
- Insert the new step at position 0 of
JobSteps - Move
_currentStepback intoJobStepsat position 1 - Set the new step as
_currentStep
Alternative (simpler): Modify InsertStep to handle Here specially:
public int InsertStep(IStep step, StepPosition position)
{
// Special case: --here inserts before current step
if (position.Type == PositionType.Here)
{
if (_currentStep == null)
{
throw new StepCommandException(StepCommandErrors.InvalidPosition,
"Can only use --here when paused at a breakpoint.");
}
// Re-queue current step at the front
var pending = _jobContext.JobSteps.ToList();
pending.Insert(0, _currentStep);
// Insert new step before it (at position 0)
pending.Insert(0, step);
// Clear and re-queue
_jobContext.JobSteps.Clear();
foreach (var s in pending)
_jobContext.JobSteps.Enqueue(s);
// New step becomes current
_currentStep = step;
// Track change and return index
var newIndex = _completedSteps.Count + 1;
// ... track change ...
return newIndex;
}
// ... existing logic for other position types ...
}
Testing
steps add run "echo test" --herewhen paused at step 1 inserts at position 1- New step becomes the current step (shows as
▶in list) - Original current step moves to position 2
- Stepping forward runs the new step first
--herewhen not paused returns appropriate errorsteps move 3 --heremoves step 3 to before current step
Chunk 2: --id Option for Step Identification
Goal: Allow users to specify a custom ID for dynamically added steps.
Design
| Flag | Meaning |
|---|---|
--id <identifier> |
Set the step's ID (used in steps.<id>.outputs, etc.) |
Validation:
- ID must be a non-empty string
- No format restrictions (matches YAML behavior - users can use any string)
Duplicate handling:
- If a step with the same ID already exists, return error: "Step with ID '' already exists"
Default behavior (unchanged):
- If
--idis not provided, auto-generate_dynamic_<guid>as before
Files to Modify
| File | Changes |
|---|---|
StepCommandParser.cs |
Add Id property to AddRunCommand and AddUsesCommand; parse --id flag |
StepFactory.cs |
Add id parameter to CreateRunStep() and CreateUsesStep(); use provided ID or generate one |
StepCommandHandler.cs |
Pass Id from command to factory; validate uniqueness |
StepManipulator.cs |
Add HasStepWithId(string id) method for uniqueness check |
Implementation Details
StepCommandParser.cs:
// Add to AddRunCommand and AddUsesCommand classes:
public string Id { get; set; }
// In ParseReplAddRunCommand and ParseReplAddUsesCommand:
case "--id":
cmd.Id = GetNextArg(tokens, ref i, "--id");
break;
StepFactory.cs:
// Update method signatures:
ActionStep CreateRunStep(
string script,
string id = null, // NEW
string name = null,
// ... rest unchanged
);
ActionStep CreateUsesStep(
string actionReference,
string id = null, // NEW
string name = null,
// ... rest unchanged
);
// In implementation:
public ActionStep CreateRunStep(string script, string id = null, ...)
{
var stepId = Guid.NewGuid();
var step = new ActionStep
{
Id = stepId,
Name = id ?? $"_dynamic_{stepId:N}", // Use provided ID or generate
DisplayName = name ?? "Run script",
// ...
};
// ...
}
StepManipulator.cs:
// Add method to check for duplicate IDs:
public bool HasStepWithId(string id)
{
if (string.IsNullOrEmpty(id))
return false;
// Check completed steps
foreach (var step in _completedSteps)
{
if (step is IActionRunner runner && runner.Action?.Name == id)
return true;
}
// Check current step
if (_currentStep is IActionRunner currentRunner && currentRunner.Action?.Name == id)
return true;
// Check pending steps
foreach (var step in _jobContext.JobSteps)
{
if (step is IActionRunner pendingRunner && pendingRunner.Action?.Name == id)
return true;
}
return false;
}
StepCommandHandler.cs:
// In HandleAddRunCommand and HandleAddUsesCommand:
if (!string.IsNullOrEmpty(cmd.Id) && _manipulator.HasStepWithId(cmd.Id))
{
throw new StepCommandException(StepCommandErrors.DuplicateId,
$"Step with ID '{cmd.Id}' already exists.");
}
var actionStep = _factory.CreateRunStep(
cmd.Script,
cmd.Id, // NEW
cmd.Name,
// ...
);
Command Examples
# Add step with custom ID
steps add run "echo hello" --id greet --name "Greeting"
# Reference in later step
steps add run "echo ${{ steps.greet.outputs.result }}"
# Duplicate ID returns error
steps add run "echo bye" --id greet
# Error: Step with ID 'greet' already exists
Testing
steps add run "echo test" --id my_stepcreates step with IDmy_step- Step ID appears correctly in
steps listoutput - Attempting duplicate ID returns clear error
- Omitting
--idstill generates_dynamic_<guid>IDs - ID is correctly set on the underlying
ActionStep.Nameproperty
Chunk 3: Help Commands (--help)
Goal: Add --help flag support to provide usage information for all step commands.
Design
| Command | Output |
|---|---|
steps |
List of available subcommands |
steps --help |
Same as above |
steps add --help |
Help for add command (shows run and uses subcommands) |
steps add run --help |
Help for add run with all options |
steps add uses --help |
Help for add uses with all options |
steps edit --help |
Help for edit command |
steps remove --help |
Help for remove command |
steps move --help |
Help for move command |
steps list --help |
Help for list command |
steps export --help |
Help for export command |
Output format: Text only (no JSON support needed)
Files to Modify
| File | Changes |
|---|---|
StepCommandParser.cs |
Add HelpCommand class; detect --help flag and return appropriate help command |
StepCommandHandler.cs |
Add HandleHelpCommand() with help text for each command |
Implementation Details
StepCommandParser.cs:
// Add new command class:
public class HelpCommand : StepCommand
{
/// <summary>
/// The command to show help for (null = top-level help)
/// </summary>
public string Command { get; set; }
/// <summary>
/// Sub-command if applicable (e.g., "run" for "steps add run --help")
/// </summary>
public string SubCommand { get; set; }
}
// Modify ParseReplCommand to detect --help:
private StepCommand ParseReplCommand(string input)
{
var tokens = Tokenize(input);
// Handle bare "steps" command
if (tokens.Count == 1 && tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
{
return new HelpCommand { Command = null };
}
if (tokens.Count < 2 || !tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
{
throw new StepCommandException(StepCommandErrors.ParseError,
"Invalid command format. Expected: steps <command> [args...]");
}
// Check for --help anywhere in tokens
if (tokens.Contains("--help") || tokens.Contains("-h"))
{
return ParseHelpCommand(tokens);
}
var subCommand = tokens[1].ToLower();
// ... existing switch ...
}
private HelpCommand ParseHelpCommand(List<string> tokens)
{
// Remove --help/-h from tokens
tokens.RemoveAll(t => t == "--help" || t == "-h");
// "steps --help" or "steps"
if (tokens.Count == 1)
{
return new HelpCommand { Command = null };
}
// "steps add --help"
var cmd = tokens[1].ToLower();
// "steps add run --help"
string subCmd = null;
if (tokens.Count >= 3 && (cmd == "add"))
{
subCmd = tokens[2].ToLower();
if (subCmd != "run" && subCmd != "uses")
subCmd = null;
}
return new HelpCommand { Command = cmd, SubCommand = subCmd };
}
StepCommandHandler.cs:
private StepCommandResult HandleHelpCommand(HelpCommand cmd)
{
string helpText = (cmd.Command, cmd.SubCommand) switch
{
(null, _) => GetTopLevelHelp(),
("add", null) => GetAddHelp(),
("add", "run") => GetAddRunHelp(),
("add", "uses") => GetAddUsesHelp(),
("edit", _) => GetEditHelp(),
("remove", _) => GetRemoveHelp(),
("move", _) => GetMoveHelp(),
("list", _) => GetListHelp(),
("export", _) => GetExportHelp(),
_ => $"Unknown command: {cmd.Command}"
};
return new StepCommandResult
{
Success = true,
Message = helpText
};
}
private string GetTopLevelHelp() => @"
steps - Manipulate job steps during debug session
COMMANDS:
list Show all steps with status
add Add a new step (run or uses)
edit Modify a pending step
remove Delete a pending step
move Reorder a pending step
export Generate YAML for modified steps
Use 'steps <command> --help' for more information about a command.
".Trim();
private string GetAddHelp() => @"
steps add - Add a new step to the job
USAGE:
steps add run <script> [options] Add a shell command step
steps add uses <action> [options] Add an action step
Use 'steps add run --help' or 'steps add uses --help' for detailed options.
".Trim();
private string GetAddRunHelp() => @"
steps add run - Add a shell command step
USAGE:
steps add run ""<script>"" [options]
OPTIONS:
--id <id> Step ID for referencing in expressions
--name ""<name>"" Display name for the step
--shell <shell> Shell to use (bash, sh, pwsh, python, cmd)
--working-directory <dir> Working directory for the script
--if ""<condition>"" Condition expression (default: success())
--env KEY=value Environment variable (can repeat)
--continue-on-error Don't fail job if step fails
--timeout <minutes> Step timeout in minutes
POSITION OPTIONS:
--here Insert before current step (default)
--after <index> Insert after step at index
--before <index> Insert before step at index
--at <index> Insert at specific index
--first Insert at first pending position
--last Insert at end of job
EXAMPLES:
steps add run ""npm test""
steps add run ""echo hello"" --name ""Greeting"" --id greet
steps add run ""./build.sh"" --shell bash --after 3
".Trim();
private string GetAddUsesHelp() => @"
steps add uses - Add an action step
USAGE:
steps add uses <action@ref> [options]
OPTIONS:
--id <id> Step ID for referencing in expressions
--name ""<name>"" Display name for the step
--with key=value Action input (can repeat)
--env KEY=value Environment variable (can repeat)
--if ""<condition>"" Condition expression (default: success())
--continue-on-error Don't fail job if step fails
--timeout <minutes> Step timeout in minutes
POSITION OPTIONS:
--here Insert before current step (default)
--after <index> Insert after step at index
--before <index> Insert before step at index
--at <index> Insert at specific index
--first Insert at first pending position
--last Insert at end of job
EXAMPLES:
steps add uses actions/checkout@v4
steps add uses actions/setup-node@v4 --with node-version=20
steps add uses ./my-action --name ""Local Action"" --after 2
".Trim();
private string GetEditHelp() => @"
steps edit - Modify a pending step
USAGE:
steps edit <index> [modifications]
MODIFICATIONS:
--name ""<name>"" Change display name
--script ""<script>"" Change script (run steps only)
--shell <shell> Change shell (run steps only)
--working-directory <dir> Change working directory
--if ""<condition>"" Change condition expression
--with key=value Set/update action input (uses steps only)
--env KEY=value Set/update environment variable
--remove-with <key> Remove action input
--remove-env <key> Remove environment variable
--continue-on-error Enable continue-on-error
--no-continue-on-error Disable continue-on-error
--timeout <minutes> Change timeout
EXAMPLES:
steps edit 3 --name ""Updated Name""
steps edit 4 --script ""npm run test:ci""
steps edit 2 --env DEBUG=true --timeout 30
".Trim();
private string GetRemoveHelp() => @"
steps remove - Delete a pending step
USAGE:
steps remove <index>
ARGUMENTS:
<index> 1-based index of the step to remove (must be pending)
EXAMPLES:
steps remove 5
steps remove 3
".Trim();
private string GetMoveHelp() => @"
steps move - Reorder a pending step
USAGE:
steps move <from> <position>
ARGUMENTS:
<from> 1-based index of the step to move (must be pending)
POSITION OPTIONS:
--here Move before current step
--after <index> Move after step at index
--before <index> Move before step at index
--to <index> Move to specific index
--first Move to first pending position
--last Move to end of job
EXAMPLES:
steps move 5 --after 2
steps move 4 --first
steps move 3 --here
".Trim();
private string GetListHelp() => @"
steps list - Show all steps with status
USAGE:
steps list [options]
OPTIONS:
--verbose Show additional step details
--output json|text Output format (default: text)
OUTPUT:
Shows all steps with:
- Index number
- Status indicator (completed, current, pending)
- Step name
- Step type (run/uses) and details
- Change indicator ([ADDED], [MODIFIED], [MOVED])
".Trim();
private string GetExportHelp() => @"
steps export - Generate YAML for modified steps
USAGE:
steps export [options]
OPTIONS:
--changes-only Only export added/modified steps
--with-comments Include change markers as YAML comments
--output json|text Output format (default: text)
OUTPUT:
Generates valid YAML that can be pasted into a workflow file.
EXAMPLES:
steps export
steps export --changes-only --with-comments
".Trim();
Testing
stepsshows top-level helpsteps --helpshows top-level helpsteps -hshows top-level helpsteps add --helpshows add command helpsteps add run --helpshows add run help with all optionssteps add uses --helpshows add uses help with all optionssteps edit --helpshows edit helpsteps remove --helpshows remove helpsteps move --helpshows move helpsteps list --helpshows list helpsteps export --helpshows export help--helpcan appear anywhere in command (e.g.,steps add --help run)
Chunk 4: Browser Extension UI Updates
Goal: Update the Add Step form to use --here as default and add the ID field.
Changes to browser-ext/content/content.js
1. Update Position Dropdown
Current options:
- "At end (default)"
- "At first pending position"
- "After current step"
New options:
- "Before next step" (default) - uses
--here - "At end"
- "After current step"
// In showAddStepDialog():
<div class="dap-form-group">
<label class="dap-label">Position</label>
<select class="form-control dap-position-select">
<option value="here" selected>Before next step</option>
<option value="last">At end</option>
<option value="after">After current step</option>
</select>
</div>
2. Add ID Field
Add after the Name field:
<div class="dap-form-group">
<label class="dap-label">ID (optional)</label>
<input type="text" class="form-control dap-id-input"
placeholder="my_step_id">
<span class="dap-help-text">Used to reference step outputs: steps.<id>.outputs</span>
</div>
3. Update handleAddStep()
async function handleAddStep(modal) {
const type = modal.querySelector('.dap-step-type-select').value;
const name = modal.querySelector('.dap-name-input').value.trim() || undefined;
const id = modal.querySelector('.dap-id-input').value.trim() || undefined; // NEW
const positionSelect = modal.querySelector('.dap-position-select').value;
let position = {};
if (positionSelect === 'here') {
position.here = true; // NEW
} else if (positionSelect === 'after') {
const currentStep = stepsList.find((s) => s.status === 'current');
if (currentStep) {
position.after = currentStep.index;
} else {
position.here = true;
}
} else {
position.last = true;
}
// Pass id to sendStepCommand
result = await sendStepCommand('step.add', {
type: 'run',
script,
id, // NEW
name,
shell,
position,
});
// ...
}
4. Update buildAddStepCommand()
function buildAddStepCommand(options) {
let cmd = 'steps add';
if (options.type === 'run') {
cmd += ` run ${quoteString(options.script)}`;
if (options.shell) cmd += ` --shell ${options.shell}`;
if (options.workingDirectory) cmd += ` --working-directory ${quoteString(options.workingDirectory)}`;
} else if (options.type === 'uses') {
cmd += ` uses ${options.action}`;
if (options.with) {
for (const [key, value] of Object.entries(options.with)) {
cmd += ` --with ${key}=${value}`;
}
}
}
if (options.id) cmd += ` --id ${quoteString(options.id)}`; // NEW
if (options.name) cmd += ` --name ${quoteString(options.name)}`;
if (options.if) cmd += ` --if ${quoteString(options.if)}`;
// ... rest of options ...
// Position
if (options.position) {
if (options.position.here) cmd += ' --here'; // NEW
else if (options.position.after !== undefined) cmd += ` --after ${options.position.after}`;
else if (options.position.before !== undefined) cmd += ` --before ${options.position.before}`;
else if (options.position.at !== undefined) cmd += ` --at ${options.position.at}`;
else if (options.position.first) cmd += ' --first';
// --last is default, no need to specify
}
return cmd;
}
CSS Updates (browser-ext/content/content.css)
.dap-help-text {
font-size: 11px;
color: var(--fgColor-muted, #8b949e);
margin-top: 4px;
display: block;
}
Testing
- Position dropdown defaults to "Before next step"
- ID field is visible and optional
- ID placeholder text is helpful
- Help text explains the purpose of ID
- Adding step with ID works correctly
- Adding step with "Before next step" uses
--hereflag - Form validation doesn't require ID
File Summary
Files to Create
None - all changes are modifications to existing files.
Files to Modify
| File | Chunks | Changes |
|---|---|---|
src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs |
1, 2, 3 | Add Here position type, Id property, HelpCommand class |
src/Runner.Worker/Dap/StepCommands/StepManipulator.cs |
1, 2 | Handle Here position, add HasStepWithId() method |
src/Runner.Worker/Dap/StepCommands/StepFactory.cs |
2 | Add id parameter to create methods |
src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs |
2, 3 | Pass ID to factory, add help text handlers |
browser-ext/content/content.js |
4 | Update form with ID field and position options |
browser-ext/content/content.css |
4 | Add help text styling |
Error Messages
| Code | Message |
|---|---|
INVALID_POSITION |
Can only use --here when paused at a breakpoint |
DUPLICATE_ID |
Step with ID '' already exists |
Estimated Effort
| Chunk | Effort |
|---|---|
Chunk 1: --here position |
~1-2 hours |
Chunk 2: --id option |
~1 hour |
| Chunk 3: Help commands | ~1-2 hours |
| Chunk 4: Browser extension UI | ~30 min |
| Total | ~4-5 hours |