From b5faa7d4bef1784cad28e032ef0a79cd9bcf15f9 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sat, 14 Mar 2026 13:34:26 +0100 Subject: [PATCH] feat(coding-agent): added cursor navigation to message boundaries - Added `moveCursorToMessageStart()` and `moveCursorToMessageEnd()` prompt actions for cursor navigation to message boundaries. - Exposed editor methods for cursor movement to message start and end positions in InputController keybindings. - Implemented message boundary navigation in Editor class with private helper methods for cursor positioning. - Removed remote compaction settings test suite to align with feature deprecation. --- packages/coding-agent/CHANGELOG.md | 2 ++ .../coding-agent/src/exec/bash-executor.ts | 1 - .../src/modes/controllers/input-controller.ts | 2 ++ .../src/modes/prompt-action-autocomplete.ts | 20 ++++++++++++--- .../test/prompt-action-autocomplete.test.ts | 24 ++++++++++++------ .../test/settings-manager.test.ts | 25 ------------------- packages/tui/CHANGELOG.md | 5 ++++ packages/tui/src/components/editor.ts | 21 ++++++++++++++++ packages/tui/test/editor.test.ts | 12 +++++++++ 9 files changed, 75 insertions(+), 37 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5c685f7eb..20c0edb24 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,8 +1,10 @@ # Changelog ## [Unreleased] + ### Added +- Added `moveCursorToMessageEnd` and `moveCursorToMessageStart` prompt actions to navigate to the beginning and end of the entire message - Added support for provider-level `compat` configuration to apply OpenAI compatibility settings across all models from a provider - Added `reasoningEffortMap` configuration option to map reasoning effort levels to provider-specific values - Added support for `supportsUsageInStreaming`, `requiresToolResultName`, `requiresAssistantAfterToolResult`, `requiresThinkingAsText`, `thinkingFormat`, and `supportsStrictMode` OpenAI compatibility options diff --git a/packages/coding-agent/src/exec/bash-executor.ts b/packages/coding-agent/src/exec/bash-executor.ts index 7d99d4f17..87240d56a 100644 --- a/packages/coding-agent/src/exec/bash-executor.ts +++ b/packages/coding-agent/src/exec/bash-executor.ts @@ -41,7 +41,6 @@ const HARD_TIMEOUT_GRACE_MS = 5_000; const shellSessions = new Map(); const brokenShellSessions = new Set(); - async function resolveShellCwd(cwd: string | undefined): Promise { if (!cwd) return undefined; diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index aeb24894d..dab6e0fef 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -535,6 +535,8 @@ export class InputController { keybindings: this.ctx.keybindings, copyCurrentLine: () => this.handleCopyCurrentLine(), copyPrompt: () => this.handleCopyPrompt(), + moveCursorToMessageEnd: () => this.ctx.editor.moveToMessageEnd(), + moveCursorToMessageStart: () => this.ctx.editor.moveToMessageStart(), moveCursorToLineStart: () => this.ctx.editor.moveToLineStart(), moveCursorToLineEnd: () => this.ctx.editor.moveToLineEnd(), }); diff --git a/packages/coding-agent/src/modes/prompt-action-autocomplete.ts b/packages/coding-agent/src/modes/prompt-action-autocomplete.ts index 850c0f777..f6340b2c4 100644 --- a/packages/coding-agent/src/modes/prompt-action-autocomplete.ts +++ b/packages/coding-agent/src/modes/prompt-action-autocomplete.ts @@ -26,6 +26,8 @@ interface PromptActionAutocompleteOptions { keybindings: KeybindingsManager; copyCurrentLine: () => void; copyPrompt: () => void; + moveCursorToMessageEnd: () => void; + moveCursorToMessageStart: () => void; moveCursorToLineStart: () => void; moveCursorToLineEnd: () => void; } @@ -68,9 +70,7 @@ function fuzzyScore(query: string, target: string): number { } function isPromptActionItem(item: AutocompleteItem): item is PromptActionAutocompleteItem { - return ( - "actionId" in item && "execute" in item && typeof (item as PromptActionAutocompleteItem).execute === "function" - ); + return "actionId" in item && "execute" in item && typeof item.execute === "function"; } function getPromptActionPrefix(textBeforeCursor: string): string | null { @@ -181,6 +181,20 @@ export function createPromptActionAutocompleteProvider( keywords: ["copy", "prompt", "clipboard", "message"], execute: options.copyPrompt, }, + { + id: "cursor-message-end", + label: "Move cursor to end of message", + description: "Current message", + keywords: ["move", "cursor", "message", "end", "prompt", "last", "bottom"], + execute: options.moveCursorToMessageEnd, + }, + { + id: "cursor-message-start", + label: "Move cursor to beginning of message", + description: "Current message", + keywords: ["move", "cursor", "message", "start", "beginning", "prompt", "first", "top"], + execute: options.moveCursorToMessageStart, + }, { id: "cursor-line-start", label: "Move cursor to beginning of line", diff --git a/packages/coding-agent/test/prompt-action-autocomplete.test.ts b/packages/coding-agent/test/prompt-action-autocomplete.test.ts index 78d3f8d58..b46f727e3 100644 --- a/packages/coding-agent/test/prompt-action-autocomplete.test.ts +++ b/packages/coding-agent/test/prompt-action-autocomplete.test.ts @@ -27,6 +27,8 @@ describe("prompt action autocomplete", () => { }), copyCurrentLine: () => {}, copyPrompt: () => {}, + moveCursorToMessageEnd: () => {}, + moveCursorToMessageStart: () => {}, moveCursorToLineStart: () => {}, moveCursorToLineEnd: () => {}, }); @@ -37,6 +39,8 @@ describe("prompt action autocomplete", () => { expect(suggestions?.items.map(item => item.label)).toEqual([ "Copy current line", "Copy whole prompt", + "Move cursor to end of message", + "Move cursor to beginning of message", "Move cursor to beginning of line", "Move cursor to end of line", ]); @@ -51,32 +55,34 @@ describe("prompt action autocomplete", () => { }); it("executes selected prompt actions and removes the trigger text", async () => { - let promptCopies = 0; + let messageEndMoves = 0; const provider = createPromptActionAutocompleteProvider({ commands: [], basePath: "/tmp", keybindings: KeybindingsManager.inMemory(), copyCurrentLine: () => {}, - copyPrompt: () => { - promptCopies += 1; + copyPrompt: () => {}, + moveCursorToMessageEnd: () => { + messageEndMoves += 1; }, + moveCursorToMessageStart: () => {}, moveCursorToLineStart: () => {}, moveCursorToLineEnd: () => {}, }); - const suggestions = await provider.getSuggestions(["hello #cop"], 0, 10); - const item = suggestions?.items.find(entry => entry.label === "Copy whole prompt"); + const suggestions = await provider.getSuggestions(["hello #mess"], 0, 11); + const item = suggestions?.items.find(entry => entry.label === "Move cursor to end of message"); expect(item).toBeDefined(); if (!item || !suggestions) { - throw new Error("expected copy whole prompt suggestion"); + throw new Error("expected move cursor to end of message suggestion"); } - const result = provider.applyCompletion(["hello #cop"], 0, 10, item, suggestions.prefix); + const result = provider.applyCompletion(["hello #mess"], 0, 11, item, suggestions.prefix); expect(result.lines).toEqual(["hello "]); expect(result.cursorLine).toBe(0); expect(result.cursorCol).toBe(6); result.onApplied?.(); - expect(promptCopies).toBe(1); + expect(messageEndMoves).toBe(1); }); it("falls back to normal typing for literal hashtags with no matching action", async () => { @@ -86,6 +92,8 @@ describe("prompt action autocomplete", () => { keybindings: KeybindingsManager.inMemory(), copyCurrentLine: () => {}, copyPrompt: () => {}, + moveCursorToMessageEnd: () => {}, + moveCursorToMessageStart: () => {}, moveCursorToLineStart: () => {}, moveCursorToLineEnd: () => {}, }); diff --git a/packages/coding-agent/test/settings-manager.test.ts b/packages/coding-agent/test/settings-manager.test.ts index 3666eb294..bca97cd4c 100644 --- a/packages/coding-agent/test/settings-manager.test.ts +++ b/packages/coding-agent/test/settings-manager.test.ts @@ -122,29 +122,4 @@ describe("Settings", () => { expect(savedSettings.defaultThinkingLevel).toBe(Effort.High); }); }); - describe("compaction remote setting", () => { - it("includes remote compaction in the agent settings UI", () => { - expect(getPathsForTab("agent")).toContain("compaction.remoteEnabled"); - expect(getUi("compaction.remoteEnabled")).toMatchObject({ - tab: "agent", - label: "Remote compaction", - }); - }); - - it("persists the remote compaction toggle", async () => { - const settings = await Settings.init({ cwd: projectDir, agentDir }); - - expect(settings.get("compaction.remoteEnabled")).toBe(true); - - settings.set("compaction.remoteEnabled", false); - await settings.flush(); - - const savedSettings = await readSettings(); - expect(savedSettings.compaction).toEqual({ remoteEnabled: false }); - - _resetSettingsForTest(); - const reloaded = await Settings.init({ cwd: projectDir, agentDir }); - expect(reloaded.get("compaction.remoteEnabled")).toBe(false); - }); - }); }); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 90f53a26a..83d976ca4 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog ## [Unreleased] + +### Added + +- Added `moveToMessageStart()` and `moveToMessageEnd()` methods to move cursor to the beginning and end of the entire message + ### Fixed - Fixed autocomplete to preserve `./` prefix when completing relative file and directory paths diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 5fd7f96a1..d08bfc443 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -1154,6 +1154,14 @@ export class Editor implements Component, Focusable { this.#moveToLineEnd(); } + moveToMessageStart(): void { + this.#moveToMessageStart(); + } + + moveToMessageEnd(): void { + this.#moveToMessageEnd(); + } + setText(text: string): void { this.#historyIndex = -1; // Exit history browsing mode this.#resetKillSequence(); @@ -1510,6 +1518,19 @@ export class Editor implements Component, Focusable { this.#setCursorCol(currentLine.length); } + #moveToMessageStart(): void { + this.#resetKillSequence(); + this.#state.cursorLine = 0; + this.#setCursorCol(0); + } + + #moveToMessageEnd(): void { + this.#resetKillSequence(); + this.#state.cursorLine = this.#state.lines.length - 1; + const currentLine = this.#state.lines[this.#state.cursorLine] || ""; + this.#setCursorCol(currentLine.length); + } + #resetKillSequence(): void { this.#lastAction = null; } diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index 006c897ad..736833517 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -245,6 +245,18 @@ describe("Editor component", () => { expect(editor.getCursor()).toEqual({ line: 0, col: 2 }); }); + it("moves cursor to message boundaries", () => { + const editor = new Editor(defaultEditorTheme); + editor.setText("first line\nsecond line\nthird"); + + editor.moveToMessageStart(); + expect(editor.getCursor()).toEqual({ line: 0, col: 0 }); + + editor.moveToMessageEnd(); + expect(editor.getCursor()).toEqual({ line: 2, col: 5 }); + }); + + it("returns lines as a defensive copy", () => { const editor = new Editor(defaultEditorTheme); editor.setText("a\nb");