# Plan: Simplify Step Commands to Use REPL Format **Status:** Complete **Date:** January 2026 **Prerequisites:** dap-step-manipulation.md (Chunks 1-9 completed) ## Overview Remove the JSON API for step commands and use a single REPL command format (`steps `) for both human input and browser extension UI. Add `--output` flag for controlling response format. ## Problem Currently the step command system has two input formats: 1. REPL format: `!step list` (for humans typing in console) 2. JSON format: `{"cmd":"step.list"}` (for browser extension UI) This causes issues: - The `!` prefix is awkward for humans typing commands - The JSON API is unnecessary complexity (browser extension is just another DAP client) - Debugging is harder because UI sends different format than humans would type - Two code paths to maintain and test ## Goals 1. Replace `!step` prefix with `steps` (more ergonomic, no special character) 2. Remove JSON command parsing (unnecessary complexity) 3. Add `--output` flag for response format control (`text` or `json`) 4. Browser extension sends same command strings a human would type 5. Single code path for all step command input ## Progress Checklist - [x] **Chunk 1:** Update StepCommandParser - `steps` prefix, `--output` flag, remove JSON parsing - [x] **Chunk 2:** Update StepCommandHandler - format responses based on OutputFormat - [x] **Chunk 3:** Update Browser Extension - build REPL command strings - [x] **Chunk 4:** Update REPL context detection in browser extension - [x] **Chunk 5:** Update/remove tests - [x] **Chunk 6:** Update plan documentation --- ## Implementation Chunks ### Chunk 1: Update StepCommandParser to Use `steps` Prefix **Files to modify:** - `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs` **Changes:** 1. **Add `OutputFormat` enum and update `StepCommand` base class:** ```csharp public enum OutputFormat { Text, Json } public abstract class StepCommand { /// /// Output format for the command response. /// public OutputFormat Output { get; set; } = OutputFormat.Text; } ``` Remove the `WasJsonInput` property (replaced by `OutputFormat`). 2. **Update `IsStepCommand()`** - recognize `steps` prefix, remove JSON detection: ```csharp public bool IsStepCommand(string input) { if (string.IsNullOrWhiteSpace(input)) return false; var trimmed = input.Trim(); // Command format: steps ... if (trimmed.StartsWith("steps ", StringComparison.OrdinalIgnoreCase) || trimmed.Equals("steps", StringComparison.OrdinalIgnoreCase)) return true; return false; } ``` 3. **Update `Parse()`** - remove JSON branch: ```csharp public StepCommand Parse(string input) { var trimmed = input?.Trim() ?? ""; return ParseReplCommand(trimmed); } ``` 4. **Update `ParseReplCommand()`** - expect `steps` as first token: ```csharp if (tokens.Count < 2 || !tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase)) { throw new StepCommandException(StepCommandErrors.ParseError, "Invalid command format. Expected: steps [args...]"); } ``` 5. **Add `--output` flag parsing** - create a helper method and call it in each Parse*Command method: ```csharp private OutputFormat ParseOutputFlag(List tokens, ref int index) { // Look for --output, --output=json, --output=text, -o json, -o text for (int i = index; i < tokens.Count; i++) { var token = tokens[i].ToLower(); if (token == "--output" || token == "-o") { if (i + 1 < tokens.Count) { var format = tokens[i + 1].ToLower(); tokens.RemoveAt(i); // Remove flag tokens.RemoveAt(i); // Remove value return format == "json" ? OutputFormat.Json : OutputFormat.Text; } } else if (token.StartsWith("--output=")) { var format = token.Substring("--output=".Length); tokens.RemoveAt(i); return format == "json" ? OutputFormat.Json : OutputFormat.Text; } } return OutputFormat.Text; } ``` Apply to each command parser before processing other flags. 6. **Delete all JSON parsing methods:** - `ParseJsonCommand()` - `ParseJsonListCommand()` - `ParseJsonAddCommand()` - `ParseJsonEditCommand()` - `ParseJsonRemoveCommand()` - `ParseJsonMoveCommand()` - `ParseJsonExportCommand()` - `ParseJsonPosition()` - `ParseJsonDictionary()` - `ParseJsonStringList()` 7. **Update error messages** to reference `steps ` format. **Estimated effort:** Small-medium --- ### Chunk 2: Update StepCommandHandler Response Format **Files to modify:** - `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs` **Changes:** 1. **Update each command handler** to format response based on `command.Output`: For `ListCommand`: ```csharp if (command.Output == OutputFormat.Json) { return new StepCommandResult { Success = true, Message = JsonConvert.SerializeObject(new { Success = true, Result = steps }), Result = steps }; } else { return new StepCommandResult { Success = true, Message = FormatStepListAsText(steps), Result = steps }; } ``` 2. **Add text formatting helpers:** ```csharp private string FormatStepListAsText(IReadOnlyList steps) { var sb = new StringBuilder(); sb.AppendLine("Steps:"); foreach (var step in steps) { var statusIcon = step.Status switch { StepStatus.Completed => "✓", StepStatus.Current => "▶", _ => " " }; var changeBadge = step.Change.HasValue ? $"[{step.Change}]" : ""; sb.AppendLine($" {statusIcon} {step.Index}. {step.Name,-30} {changeBadge,-12} {step.Type,-5} {step.TypeDetail}"); } sb.AppendLine(); sb.AppendLine("Legend: ✓ = completed, ▶ = current, [ADDED] = new, [MODIFIED] = edited"); return sb.ToString(); } ``` 3. **Update error responses** to also respect output format. 4. **Remove `WasJsonInput` checks** throughout the handler. **Estimated effort:** Small --- ### Chunk 3: Update Browser Extension - Build Command Strings **Files to modify:** - `browser-ext/content/content.js` **Changes:** 1. **Replace `sendStepCommand()` implementation:** ```javascript /** * Send step command via REPL format */ async function sendStepCommand(action, options = {}) { const expression = buildStepCommand(action, options); try { const response = await sendDapRequest('evaluate', { expression, frameId: currentFrameId, context: 'repl', }); if (response.result) { try { return JSON.parse(response.result); } catch (e) { // Response might be plain text for non-JSON output return { Success: true, Message: response.result }; } } return { Success: false, Error: 'NO_RESPONSE', Message: 'No response from server' }; } catch (error) { return { Success: false, Error: 'REQUEST_FAILED', Message: error.message }; } } ``` 2. **Add `buildStepCommand()` function:** ```javascript /** * Build REPL command string from action and options */ function buildStepCommand(action, options) { let cmd; switch (action) { case 'step.list': cmd = options.verbose ? 'steps list --verbose' : 'steps list'; break; case 'step.add': cmd = buildAddStepCommand(options); break; case 'step.edit': cmd = buildEditStepCommand(options); break; case 'step.remove': cmd = `steps remove ${options.index}`; break; case 'step.move': cmd = buildMoveStepCommand(options); break; case 'step.export': cmd = buildExportCommand(options); break; default: throw new Error(`Unknown step command: ${action}`); } // Always request JSON output for programmatic use return cmd + ' --output json'; } ``` 3. **Add command builder helpers:** ```javascript 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.name) cmd += ` --name ${quoteString(options.name)}`; if (options.if) cmd += ` --if ${quoteString(options.if)}`; if (options.env) { for (const [key, value] of Object.entries(options.env)) { cmd += ` --env ${key}=${value}`; } } if (options.continueOnError) cmd += ' --continue-on-error'; if (options.timeout) cmd += ` --timeout ${options.timeout}`; // Position if (options.position) { if (options.position.after) cmd += ` --after ${options.position.after}`; else if (options.position.before) cmd += ` --before ${options.position.before}`; else if (options.position.at) cmd += ` --at ${options.position.at}`; else if (options.position.first) cmd += ' --first'; // --last is default, no need to specify } return cmd; } function buildEditStepCommand(options) { let cmd = `steps edit ${options.index}`; if (options.name) cmd += ` --name ${quoteString(options.name)}`; if (options.script) cmd += ` --script ${quoteString(options.script)}`; if (options.if) cmd += ` --if ${quoteString(options.if)}`; if (options.shell) cmd += ` --shell ${options.shell}`; if (options.workingDirectory) cmd += ` --working-directory ${quoteString(options.workingDirectory)}`; return cmd; } function buildMoveStepCommand(options) { let cmd = `steps move ${options.from}`; const pos = options.position; if (pos.after) cmd += ` --after ${pos.after}`; else if (pos.before) cmd += ` --before ${pos.before}`; else if (pos.at) cmd += ` --to ${pos.at}`; else if (pos.first) cmd += ' --first'; else if (pos.last) cmd += ' --last'; return cmd; } function buildExportCommand(options) { let cmd = 'steps export'; if (options.changesOnly) cmd += ' --changes-only'; if (options.withComments) cmd += ' --with-comments'; return cmd; } /** * Quote a string for use in command, escaping as needed */ function quoteString(str) { // Escape backslashes and quotes, wrap in quotes return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; } ``` 4. **Update `loadSteps()`:** ```javascript async function loadSteps() { try { const response = await sendDapRequest('evaluate', { expression: 'steps list --output json', frameId: currentFrameId, context: 'repl', }); // ... rest of parsing logic unchanged } } ``` **Estimated effort:** Medium --- ### Chunk 4: Update REPL Context Detection **Files to modify:** - `browser-ext/content/content.js` **Changes:** Update `handleReplKeydown()` to set context to 'repl' for `steps` commands: ```javascript async function handleReplKeydown(e) { const input = e.target; if (e.key === 'Enter') { const command = input.value.trim(); if (!command) return; replHistory.push(command); replHistoryIndex = replHistory.length; input.value = ''; // Show command appendOutput(`> ${command}`, 'input'); // Send to DAP try { const response = await sendDapRequest('evaluate', { expression: command, frameId: currentFrameId, // Use 'repl' context for shell commands (!) and step commands context: (command.startsWith('!') || command.startsWith('steps')) ? 'repl' : 'watch', }); // ... rest unchanged } } // ... arrow key handling unchanged } ``` **Estimated effort:** Trivial --- ### Chunk 5: Update/Remove Tests **Files to modify:** - `src/Test/L0/Worker/Dap/StepCommands/StepCommandParserJsonL0.cs` - **Delete** - `src/Test/L0/Worker/Dap/StepCommands/StepCommandParserL0.cs` - Modify **Changes:** 1. **Delete `StepCommandParserJsonL0.cs`** entirely (JSON parsing tests no longer needed) 2. **Update `StepCommandParserL0.cs`:** a. Update `IsStepCommand` tests: ```csharp [Fact] public void IsStepCommand_DetectsStepsPrefix() { Assert.True(_parser.IsStepCommand("steps list")); Assert.True(_parser.IsStepCommand("steps add run \"test\"")); Assert.True(_parser.IsStepCommand("STEPS LIST")); // case insensitive Assert.True(_parser.IsStepCommand(" steps list ")); // whitespace } [Fact] public void IsStepCommand_RejectsInvalid() { Assert.False(_parser.IsStepCommand("step list")); // missing 's' Assert.False(_parser.IsStepCommand("!step list")); // old format Assert.False(_parser.IsStepCommand("stepslist")); // no space Assert.False(_parser.IsStepCommand("")); Assert.False(_parser.IsStepCommand(null)); } ``` b. Change all `!step` to `steps` in existing test cases: ```csharp // Before: var cmd = _parser.Parse("!step list --verbose"); // After: var cmd = _parser.Parse("steps list --verbose"); ``` c. Add tests for `--output` flag: ```csharp [Fact] public void Parse_ListCommand_WithOutputJson() { var cmd = _parser.Parse("steps list --output json") as ListCommand; Assert.NotNull(cmd); Assert.Equal(OutputFormat.Json, cmd.Output); } [Fact] public void Parse_ListCommand_WithOutputText() { var cmd = _parser.Parse("steps list --output text") as ListCommand; Assert.NotNull(cmd); Assert.Equal(OutputFormat.Text, cmd.Output); } [Fact] public void Parse_ListCommand_DefaultOutputIsText() { var cmd = _parser.Parse("steps list") as ListCommand; Assert.NotNull(cmd); Assert.Equal(OutputFormat.Text, cmd.Output); } [Fact] public void Parse_AddCommand_WithOutputFlag() { var cmd = _parser.Parse("steps add run \"echo test\" --output json") as AddRunCommand; Assert.NotNull(cmd); Assert.Equal(OutputFormat.Json, cmd.Output); Assert.Equal("echo test", cmd.Script); } [Fact] public void Parse_OutputFlag_ShortForm() { var cmd = _parser.Parse("steps list -o json") as ListCommand; Assert.NotNull(cmd); Assert.Equal(OutputFormat.Json, cmd.Output); } [Fact] public void Parse_OutputFlag_EqualsForm() { var cmd = _parser.Parse("steps list --output=json") as ListCommand; Assert.NotNull(cmd); Assert.Equal(OutputFormat.Json, cmd.Output); } ``` d. Update error message expectations to reference `steps` format. **Estimated effort:** Small --- ### Chunk 6: Update Plan Documentation **Files to modify:** - `.opencode/plans/dap-step-manipulation.md` **Changes:** 1. **Update command format documentation** - change all `!step` references to `steps` 2. **Document `--output` flag** in command reference: ``` ### Output Format All commands support the `--output` flag to control response format: - `--output text` (default) - Human-readable text output - `--output json` - JSON output for programmatic use - Short form: `-o json`, `-o text` - Equals form: `--output=json`, `--output=text` ``` 3. **Update Chunk 8 description** - note that JSON API was replaced with `--output` flag 4. **Update command reference table:** ``` | Command | Purpose | Example | |---------|---------|---------| | `steps list` | Show all steps | `steps list --verbose` | | `steps add` | Add new step | `steps add run "npm test" --after 3` | | `steps edit` | Modify step | `steps edit 4 --script "npm run test:ci"` | | `steps remove` | Delete step | `steps remove 5` | | `steps move` | Reorder step | `steps move 5 --after 2` | | `steps export` | Generate YAML | `steps export --with-comments` | ``` **Estimated effort:** Trivial --- ## File Summary | File | Action | Chunk | Description | |------|--------|-------|-------------| | `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs` | Modify | 1 | Change prefix to `steps`, add `--output` flag, remove JSON parsing | | `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs` | Modify | 2 | Format responses based on `OutputFormat` | | `browser-ext/content/content.js` | Modify | 3, 4 | Build REPL command strings, update context detection | | `src/Test/L0/Worker/Dap/StepCommands/StepCommandParserJsonL0.cs` | Delete | 5 | No longer needed | | `src/Test/L0/Worker/Dap/StepCommands/StepCommandParserL0.cs` | Modify | 5 | Update for `steps` prefix, add `--output` tests | | `.opencode/plans/dap-step-manipulation.md` | Modify | 6 | Update documentation | --- ## Command Reference (After Changes) ### Human Usage (text output, default) | Action | Command | |--------|---------| | List steps | `steps list` | | List verbose | `steps list --verbose` | | Add run step | `steps add run "echo hello"` | | Add run with options | `steps add run "npm test" --name "Run tests" --shell bash` | | Add uses step | `steps add uses actions/checkout@v4` | | Add uses with inputs | `steps add uses actions/setup-node@v4 --with node-version=20` | | Edit step | `steps edit 4 --name "New name" --script "new script"` | | Remove step | `steps remove 5` | | Move step | `steps move 5 --after 2` | | Export | `steps export` | | Export with options | `steps export --changes-only --with-comments` | ### Browser Extension (JSON output) The browser extension appends `--output json` to all commands: | Action | Command Sent | |--------|--------------| | List steps | `steps list --output json` | | Add step | `steps add uses actions/checkout@v4 --output json` | | Remove step | `steps remove 5 --output json` | --- ## Output Format Examples **`steps list` (text, default):** ``` Steps: ✓ 1. Checkout uses actions/checkout@v4 ✓ 2. Setup Node uses actions/setup-node@v4 ▶ 3. Install deps run npm ci 4. Run tests [MODIFIED] run npm test 5. Build [ADDED] run npm run build Legend: ✓ = completed, ▶ = current, [ADDED] = new, [MODIFIED] = edited ``` **`steps list --output json`:** ```json { "Success": true, "Result": [ {"index": 1, "name": "Checkout", "type": "uses", "typeDetail": "actions/checkout@v4", "status": "completed"}, {"index": 2, "name": "Setup Node", "type": "uses", "typeDetail": "actions/setup-node@v4", "status": "completed"}, {"index": 3, "name": "Install deps", "type": "run", "typeDetail": "npm ci", "status": "current"}, {"index": 4, "name": "Run tests", "type": "run", "typeDetail": "npm test", "status": "pending", "change": "MODIFIED"}, {"index": 5, "name": "Build", "type": "run", "typeDetail": "npm run build", "status": "pending", "change": "ADDED"} ] } ``` **`steps add run "echo hello" --name "Greeting"` (text):** ``` Step added at position 6: Greeting ``` **`steps add run "echo hello" --name "Greeting" --output json`:** ```json { "Success": true, "Message": "Step added at position 6", "Result": {"index": 6, "name": "Greeting", "type": "run", "typeDetail": "echo hello", "status": "pending", "change": "ADDED"} } ```