summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-06 09:02:07 +0300
committerPaul Buetow <paul@buetow.org>2026-06-06 09:02:07 +0300
commitdc17b7469c2055961892bbb3175d4ab0fb1d1180 (patch)
tree541bf267994875abeedce0fddf399c95174d5fba
parent1433f7a13ede0c819ec4f8fd4027ad3df8daa94f (diff)
Add 'ask edit ID' to edit task description in $EDITOR; bump to 0.41.0main
Amp-Thread-ID: https://ampcode.com/threads/T-019e9b82-6ba0-77a4-b4a0-5c2cbf9bf39f Co-authored-by: Amp <amp@ampcode.com>
-rw-r--r--=0
-rw-r--r--internal/askcli/command_edit.go49
-rw-r--r--internal/askcli/command_edit_test.go49
-rw-r--r--internal/askcli/commands_registry.go3
-rw-r--r--internal/version.go2
5 files changed, 92 insertions, 11 deletions
diff --git a/= b/=
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/=
diff --git a/internal/askcli/command_edit.go b/internal/askcli/command_edit.go
index 585f550..27c9106 100644
--- a/internal/askcli/command_edit.go
+++ b/internal/askcli/command_edit.go
@@ -1,23 +1,28 @@
package askcli
import (
+ "bytes"
"context"
"io"
"codeberg.org/snonux/hexai/internal/editor"
)
-// captureFromEditor opens the user's editor on a temporary file and returns its
-// trimmed contents after the editor exits. It is a variable so tests can stub it.
-var captureFromEditor = func() (string, error) {
- return editor.OpenTempAndEdit(nil)
+// captureFromEditor opens the user's editor on a temporary file pre-filled with
+// the given initial content and returns its trimmed contents after the editor
+// exits. It is a variable so tests can stub it.
+var captureFromEditor = func(initial []byte) (string, error) {
+ return editor.OpenTempAndEdit(initial)
}
-// handleEdit opens the configured editor on a temporary file and creates a new
-// task from the resulting (possibly multi-line) content.
+// handleEdit opens the configured editor on a temporary file. With no selector
+// it creates a new task from the resulting content. With a task ID/UUID selector
+// it pre-fills the editor with the task's current description and updates it.
func (d *Dispatcher) handleEdit(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
- _ = args
- description, err := captureFromEditor()
+ if len(args) >= 2 {
+ return d.editTaskDescription(ctx, args[1], stdout, stderr)
+ }
+ description, err := captureFromEditor(nil)
if err != nil {
writeInfoError(stderr, err)
return 1, nil
@@ -28,3 +33,31 @@ func (d *Dispatcher) handleEdit(ctx context.Context, args []string, stdout, stde
}
return d.createTask(ctx, nil, description, nil, stdout, stderr)
}
+
+// editTaskDescription resolves a task selector, opens the editor pre-filled with
+// the task's current description, and modifies the task with the new content.
+func (d *Dispatcher) editTaskDescription(ctx context.Context, selector string, stdout, stderr io.Writer) (int, error) {
+ resolved, tasks, code, err := d.resolveTaskSelector(ctx, selector, stderr)
+ if err != nil {
+ writeInfoError(stderr, err)
+ return code, nil
+ }
+
+ description, err := captureFromEditor([]byte(tasks[0].Description))
+ if err != nil {
+ writeInfoError(stderr, err)
+ return 1, nil
+ }
+ if description == "" {
+ _, _ = io.WriteString(stderr, "error: ask edit aborted: empty description\n")
+ return 1, nil
+ }
+
+ var outBuf bytes.Buffer
+ code, err = d.runner.Run(ctx, []string{"uuid:" + resolved.UUID, "modify", description}, nil, &outBuf, io.Discard)
+ if code != 0 {
+ return code, err
+ }
+ _, _ = io.WriteString(stdout, FormatSuccess(displayResolvedTaskID(resolved)))
+ return 0, nil
+}
diff --git a/internal/askcli/command_edit_test.go b/internal/askcli/command_edit_test.go
index 3f4b778..80cbd56 100644
--- a/internal/askcli/command_edit_test.go
+++ b/internal/askcli/command_edit_test.go
@@ -12,7 +12,7 @@ import (
func stubEditorCapture(t *testing.T, content string, err error) {
t.Helper()
old := captureFromEditor
- captureFromEditor = func() (string, error) {
+ captureFromEditor = func(initial []byte) (string, error) {
return content, err
}
t.Cleanup(func() { captureFromEditor = old })
@@ -49,6 +49,53 @@ func TestHandleEdit_Success(t *testing.T) {
}
}
+func TestHandleEdit_ExistingTaskModifiesDescription(t *testing.T) {
+ now := useIsolatedTaskAliasCache(t)
+ writeTaskAliasCacheForTest(t, taskAliasCache{
+ NextID: 1,
+ Entries: []taskAliasCacheEntry{
+ {UUID: "existing-uuid", Alias: "0", CreatedAt: now},
+ },
+ })
+
+ var initialContent []byte
+ old := captureFromEditor
+ captureFromEditor = func(initial []byte) (string, error) {
+ initialContent = initial
+ return "updated description", nil
+ }
+ t.Cleanup(func() { captureFromEditor = old })
+
+ var capturedArgs []string
+ d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
+ // First call: export to resolve selector. Subsequent: modify.
+ if len(args) >= 2 && args[1] == "export" {
+ _, _ = io.WriteString(stdout, `[{"uuid":"existing-uuid","description":"old description","status":"pending"}]`)
+ return 0, nil
+ }
+ capturedArgs = args
+ return 0, nil
+ }})
+
+ var stdout, stderr bytes.Buffer
+ code, _ := d.Dispatch(context.Background(), []string{"edit", "0"}, nil, &stdout, &stderr)
+ if code != 0 {
+ t.Fatalf("edit code = %d stderr = %q", code, stderr.String())
+ }
+ if string(initialContent) != "old description" {
+ t.Fatalf("editor initial content = %q, want old description", initialContent)
+ }
+ want := []string{"uuid:existing-uuid", "modify", "updated description"}
+ if len(capturedArgs) != len(want) {
+ t.Fatalf("modify args = %v, want %v", capturedArgs, want)
+ }
+ for i := range want {
+ if capturedArgs[i] != want[i] {
+ t.Fatalf("modify args = %v, want %v", capturedArgs, want)
+ }
+ }
+}
+
func TestHandleEdit_EmptyContentAborts(t *testing.T) {
stubEditorCapture(t, "", nil)
diff --git a/internal/askcli/commands_registry.go b/internal/askcli/commands_registry.go
index 31773d5..2c07cac 100644
--- a/internal/askcli/commands_registry.go
+++ b/internal/askcli/commands_registry.go
@@ -208,9 +208,10 @@ func init() {
})
commandRegistry.add(commandEntry{
name: "edit",
- description: "Create a task from $EDITOR content",
+ description: "Edit a task description in $EDITOR (or create one)",
handler: wrapSimpleCommand((*Dispatcher).handleEdit),
includeInCompletion: true,
+ singleSelector: true,
})
commandRegistry.add(commandEntry{
name: "fish",
diff --git a/internal/version.go b/internal/version.go
index f99e47f..e569bfa 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -1,4 +1,4 @@
// Package internal provides the Hexai semantic version identifier used by CLI and LSP binaries.
package internal
-const Version = "0.40.0"
+const Version = "0.41.0"