summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/usage.md2
-rw-r--r--internal/askcli/command_info_add.go73
-rw-r--r--internal/askcli/command_info_add_test.go47
-rw-r--r--internal/askcli/command_list_test.go5
-rw-r--r--internal/askcli/dispatch.go2
-rw-r--r--internal/askcli/formatter.go21
-rw-r--r--internal/askcli/formatter_test.go15
7 files changed, 138 insertions, 27 deletions
diff --git a/docs/usage.md b/docs/usage.md
index 2a02e05..87a6778 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -146,7 +146,7 @@ cat SOMEFILE.txt | hexai --tps-simulation 20
| `ask list started` | List started tasks |
| `ask list limit:N` | Limit results |
| `ask list sort:priority-,urgency-` | Sort by priority then urgency |
-| `ask info <uuid>` | Show task details |
+| `ask info [uuid]` | Show task details, or the current started task if UUID is omitted |
| `ask annotate <uuid> "note"` | Add annotation |
| `ask start <uuid>` | Start working on a task |
| `ask stop <uuid>` | Stop work on a task |
diff --git a/internal/askcli/command_info_add.go b/internal/askcli/command_info_add.go
index 11aea7b..9a7b518 100644
--- a/internal/askcli/command_info_add.go
+++ b/internal/askcli/command_info_add.go
@@ -10,24 +10,10 @@ import (
)
func (d Dispatcher) handleInfo(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
- if len(args) < 2 {
- io.WriteString(stderr, "error: ask info requires a UUID argument\n")
- return 1, nil
- }
- uuid := NormalizeUUID(args[1])
- if IsNumericID(uuid) {
- io.WriteString(stderr, RejectNumericID())
- return 1, nil
- }
- var outBuf bytes.Buffer
- code, err := d.runner.Run(ctx, []string{"uuid:" + uuid, "export"}, nil, &outBuf, stderr)
- if code != 0 {
- return code, err
- }
- tasks, err := ParseTaskExport(&outBuf)
- if err != nil || len(tasks) == 0 {
- io.WriteString(stderr, "error: task not found\n")
- return 1, nil
+ tasks, code, err := d.infoTasks(ctx, args, stderr)
+ if err != nil {
+ writeInfoError(stderr, err)
+ return code, nil
}
if d.jsonOutput {
data, err := json.Marshal(tasks)
@@ -43,6 +29,57 @@ func (d Dispatcher) handleInfo(ctx context.Context, args []string, stdout, stder
return 0, nil
}
+func (d Dispatcher) infoTasks(ctx context.Context, args []string, stderr io.Writer) ([]TaskExport, int, error) {
+ if len(args) >= 2 {
+ uuid := NormalizeUUID(args[1])
+ if IsNumericID(uuid) {
+ return nil, 1, fmt.Errorf(strings.TrimSpace(RejectNumericID()))
+ }
+ return d.exportTasks(ctx, []string{"uuid:" + uuid, "export"}, stderr)
+ }
+ return d.startedInfoTasks(ctx, stderr)
+}
+
+func (d Dispatcher) startedInfoTasks(ctx context.Context, stderr io.Writer) ([]TaskExport, int, error) {
+ tasks, code, err := d.exportTasks(ctx, []string{"started", "export"}, stderr)
+ if err != nil {
+ return nil, code, err
+ }
+ switch len(tasks) {
+ case 0:
+ return nil, 1, fmt.Errorf("no started task found")
+ case 1:
+ return tasks, 0, nil
+ default:
+ return nil, 1, fmt.Errorf("multiple started tasks found; pass a UUID explicitly")
+ }
+}
+
+func (d Dispatcher) exportTasks(ctx context.Context, args []string, stderr io.Writer) ([]TaskExport, int, error) {
+ var outBuf bytes.Buffer
+ code, err := d.runner.Run(ctx, args, nil, &outBuf, stderr)
+ if code != 0 {
+ return nil, code, err
+ }
+ tasks, err := ParseTaskExport(&outBuf)
+ if err != nil {
+ return nil, 1, err
+ }
+ if len(tasks) == 0 && len(args) > 0 && strings.HasPrefix(args[0], "uuid:") {
+ return nil, 1, fmt.Errorf("task not found")
+ }
+ return tasks, 0, nil
+}
+
+func writeInfoError(stderr io.Writer, err error) {
+ msg := err.Error()
+ if strings.HasPrefix(msg, "error:") {
+ io.WriteString(stderr, msg+"\n")
+ return
+ }
+ fmt.Fprintf(stderr, "error: %v\n", err)
+}
+
func (d Dispatcher) handleAdd(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
if len(args) < 2 {
io.WriteString(stderr, "error: ask add requires a description\n")
diff --git a/internal/askcli/command_info_add_test.go b/internal/askcli/command_info_add_test.go
index fc36930..9bc13df 100644
--- a/internal/askcli/command_info_add_test.go
+++ b/internal/askcli/command_info_add_test.go
@@ -29,6 +29,9 @@ func TestHandleInfo_Success(t *testing.T) {
if !strings.Contains(output, "H") {
t.Fatalf("output missing priority: %s", output)
}
+ if !strings.Contains(output, "Started: no") {
+ t.Fatalf("output missing explicit started state: %s", output)
+ }
}
func TestHandleInfo_NumericID(t *testing.T) {
@@ -43,13 +46,55 @@ func TestHandleInfo_NumericID(t *testing.T) {
}
func TestHandleInfo_MissingUUID(t *testing.T) {
+ jsonData := `[{"uuid":"started-uuid","description":"Started task","status":"pending","priority":"M","start":"2026-03-26T10:00:00Z","urgency":5.0,"depends":[]}]`
+ d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
+ if len(args) == 2 && args[0] == "started" && args[1] == "export" {
+ io.WriteString(stdout, jsonData)
+ }
+ return 0, nil
+ }})
+ var stdout, stderr bytes.Buffer
+ code, _ := d.Dispatch(context.Background(), []string{"info"}, nil, &stdout, &stderr)
+ if code != 0 {
+ t.Fatalf("info code = %d, want 0 for implicit started task", code)
+ }
+ if !strings.Contains(stdout.String(), "started-uuid") {
+ t.Fatalf("output missing started task UUID: %s", stdout.String())
+ }
+}
+
+func TestHandleInfo_MissingUUID_NoStartedTask(t *testing.T) {
d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
+ if len(args) == 2 && args[0] == "started" && args[1] == "export" {
+ io.WriteString(stdout, "[]")
+ }
return 0, nil
}})
var stdout, stderr bytes.Buffer
code, _ := d.Dispatch(context.Background(), []string{"info"}, nil, &stdout, &stderr)
if code != 1 {
- t.Fatalf("info code = %d, want 1 for missing UUID", code)
+ t.Fatalf("info code = %d, want 1 when no started task exists", code)
+ }
+ if !strings.Contains(stderr.String(), "no started task found") {
+ t.Fatalf("stderr = %q, want no-started-task error", stderr.String())
+ }
+}
+
+func TestHandleInfo_MissingUUID_MultipleStartedTasks(t *testing.T) {
+ jsonData := `[{"uuid":"started-1","description":"Started task 1","status":"pending","priority":"M","start":"2026-03-26T10:00:00Z","urgency":5.0,"depends":[]},{"uuid":"started-2","description":"Started task 2","status":"pending","priority":"H","start":"2026-03-26T11:00:00Z","urgency":8.0,"depends":[]}]`
+ d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
+ if len(args) == 2 && args[0] == "started" && args[1] == "export" {
+ io.WriteString(stdout, jsonData)
+ }
+ return 0, nil
+ }})
+ var stdout, stderr bytes.Buffer
+ code, _ := d.Dispatch(context.Background(), []string{"info"}, nil, &stdout, &stderr)
+ if code != 1 {
+ t.Fatalf("info code = %d, want 1 when multiple started tasks exist", code)
+ }
+ if !strings.Contains(stderr.String(), "multiple started tasks found") {
+ t.Fatalf("stderr = %q, want multiple-started-tasks error", stderr.String())
}
}
diff --git a/internal/askcli/command_list_test.go b/internal/askcli/command_list_test.go
index 903f397..dade889 100644
--- a/internal/askcli/command_list_test.go
+++ b/internal/askcli/command_list_test.go
@@ -9,7 +9,7 @@ import (
)
func TestHandleList_Success(t *testing.T) {
- jsonData := `[{"uuid":"uuid-1","description":"Task 1","status":"pending","priority":"H","tags":["cli"],"urgency":15.0,"depends":[]},{"uuid":"uuid-2","description":"Task 2","status":"completed","priority":"M","tags":["agent"],"urgency":10.0,"depends":[]}]`
+ jsonData := `[{"uuid":"uuid-1","description":"Task 1","status":"pending","priority":"H","tags":["cli"],"start":"2026-03-26T10:00:00Z","urgency":15.0,"depends":[]},{"uuid":"uuid-2","description":"Task 2","status":"completed","priority":"M","tags":["agent"],"urgency":10.0,"depends":[]}]`
d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
for _, arg := range args {
if arg == "export" {
@@ -28,6 +28,9 @@ func TestHandleList_Success(t *testing.T) {
if !strings.Contains(output, "uuid-1") || !strings.Contains(output, "uuid-2") {
t.Fatalf("output missing UUIDs: %s", output)
}
+ if !strings.Contains(output, "Started") || !strings.Contains(output, "yes") || !strings.Contains(output, "no") {
+ t.Fatalf("output missing explicit started state: %s", output)
+ }
}
func TestHandleList_SortedByPriority(t *testing.T) {
diff --git a/internal/askcli/dispatch.go b/internal/askcli/dispatch.go
index e828989..b361111 100644
--- a/internal/askcli/dispatch.go
+++ b/internal/askcli/dispatch.go
@@ -92,7 +92,7 @@ func (d Dispatcher) help(w io.Writer) (int, error) {
io.WriteString(w, " ask list [filters] List active tasks (default)\n")
io.WriteString(w, " ask ready List READY tasks (not blocked)\n")
io.WriteString(w, " ask all [filters] List all tasks including completed/deleted\n")
- io.WriteString(w, " ask info <uuid> Show task details\n")
+ io.WriteString(w, " ask info [uuid] Show task details or current started task\n")
io.WriteString(w, " ask annotate <uuid> \"note\" Add annotation to task\n")
io.WriteString(w, " ask start <uuid> Start working on task\n")
io.WriteString(w, " ask stop <uuid> Stop working on task\n")
diff --git a/internal/askcli/formatter.go b/internal/askcli/formatter.go
index 828588b..f4c0104 100644
--- a/internal/askcli/formatter.go
+++ b/internal/askcli/formatter.go
@@ -22,6 +22,7 @@ type taskListWidths struct {
Priority int
UUID int
Status int
+ Started int
Tags int
Description int
}
@@ -32,6 +33,7 @@ func taskListWidthsFor(tasks []TaskExport) taskListWidths {
Priority: len("Priority"),
UUID: len("UUID"),
Status: len("Status"),
+ Started: len("Started"),
Tags: len("Tags"),
Description: len("Description"),
}
@@ -40,6 +42,7 @@ func taskListWidthsFor(tasks []TaskExport) taskListWidths {
widths.Priority = maxInt(widths.Priority, len(t.Priority))
widths.UUID = maxInt(widths.UUID, len(t.UUID))
widths.Status = maxInt(widths.Status, len(t.Status))
+ widths.Started = maxInt(widths.Started, len(formatTaskStarted(t)))
widths.Tags = maxInt(widths.Tags, len(formatTaskTags(t.Tags)))
widths.Description = maxInt(widths.Description, len(formatTaskDescription(t.Description)))
}
@@ -47,27 +50,29 @@ func taskListWidthsFor(tasks []TaskExport) taskListWidths {
}
func writeTaskListHeader(b *strings.Builder, widths taskListWidths) {
- fmt.Fprintf(b, "%-*s | %-*s | %-*s | %-*s | %-*s | %-*s\n",
+ fmt.Fprintf(b, "%-*s | %-*s | %-*s | %-*s | %-*s | %-*s | %-*s\n",
widths.Urgency, "Urgency",
widths.Priority, "Priority",
widths.UUID, "UUID",
widths.Status, "Status",
+ widths.Started, "Started",
widths.Tags, "Tags",
widths.Description, "Description",
)
}
func writeTaskListSeparator(b *strings.Builder, widths taskListWidths) {
- total := widths.Urgency + widths.Priority + widths.UUID + widths.Status + widths.Tags + widths.Description + 15
+ total := widths.Urgency + widths.Priority + widths.UUID + widths.Status + widths.Started + widths.Tags + widths.Description + 18
io.WriteString(b, strings.Repeat("-", total)+"\n")
}
func writeTaskListRow(b *strings.Builder, widths taskListWidths, t TaskExport) {
- fmt.Fprintf(b, "%-*s | %-*s | %-*s | %-*s | %-*s | %-*s\n",
+ fmt.Fprintf(b, "%-*s | %-*s | %-*s | %-*s | %-*s | %-*s | %-*s\n",
widths.Urgency, fmt.Sprintf("%.1f", t.Urgency),
widths.Priority, t.Priority,
widths.UUID, t.UUID,
widths.Status, t.Status,
+ widths.Started, formatTaskStarted(t),
widths.Tags, formatTaskTags(t.Tags),
widths.Description, formatTaskDescription(t.Description),
)
@@ -87,6 +92,13 @@ func formatTaskDescription(desc string) string {
return desc
}
+func formatTaskStarted(t TaskExport) string {
+ if t.Start == "" {
+ return "no"
+ }
+ return "yes"
+}
+
func maxInt(a, b int) int {
if a > b {
return a
@@ -99,13 +111,14 @@ func FormatTaskInfo(t TaskExport) string {
fmt.Fprintf(&b, "UUID: %s\n", t.UUID)
fmt.Fprintf(&b, "Description: %s\n", t.Description)
fmt.Fprintf(&b, "Status: %s\n", t.Status)
+ fmt.Fprintf(&b, "Started: %s\n", formatTaskStarted(t))
fmt.Fprintf(&b, "Priority: %s\n", t.Priority)
fmt.Fprintf(&b, "Urgency: %.1f\n", t.Urgency)
if len(t.Tags) > 0 {
fmt.Fprintf(&b, "Tags: %s\n", strings.Join(t.Tags, ", "))
}
if t.Start != "" {
- fmt.Fprintf(&b, "Started: %s\n", t.Start)
+ fmt.Fprintf(&b, "Start time: %s\n", t.Start)
}
if len(t.Depends) > 0 {
fmt.Fprintf(&b, "Depends: %s\n", strings.Join(t.Depends, ", "))
diff --git a/internal/askcli/formatter_test.go b/internal/askcli/formatter_test.go
index adb1e6e..52632f5 100644
--- a/internal/askcli/formatter_test.go
+++ b/internal/askcli/formatter_test.go
@@ -20,6 +20,9 @@ func TestFormatTaskList(t *testing.T) {
if !strings.Contains(lines[0], "UUID") || !strings.Contains(lines[0], "Priority") {
t.Fatalf("header missing UUID or Priority column: %s", lines[0])
}
+ if !strings.Contains(lines[0], "Started") {
+ t.Fatalf("header missing Started column: %s", lines[0])
+ }
if !strings.Contains(lines[2], "uuid-1") {
t.Fatalf("first task line missing uuid-1: %s", lines[2])
}
@@ -55,11 +58,12 @@ func TestFormatTaskList_AlignsHeaderAndSeparator(t *testing.T) {
}
widths := taskListWidthsFor(tasks)
- wantHeader := fmt.Sprintf("%-*s | %-*s | %-*s | %-*s | %-*s | %-*s",
+ wantHeader := fmt.Sprintf("%-*s | %-*s | %-*s | %-*s | %-*s | %-*s | %-*s",
widths.Urgency, "Urgency",
widths.Priority, "Priority",
widths.UUID, "UUID",
widths.Status, "Status",
+ widths.Started, "Started",
widths.Tags, "Tags",
widths.Description, "Description",
)
@@ -95,6 +99,12 @@ func TestFormatTaskInfo(t *testing.T) {
if !strings.Contains(output, "H") {
t.Fatalf("FormatTaskInfo missing priority H: %s", output)
}
+ if !strings.Contains(output, "Started: yes") {
+ t.Fatalf("FormatTaskInfo missing explicit started state: %s", output)
+ }
+ if !strings.Contains(output, "Start time: 2026-03-22T10:00:00Z") {
+ t.Fatalf("FormatTaskInfo missing start timestamp: %s", output)
+ }
if !strings.Contains(output, "cli, agent") {
t.Fatalf("FormatTaskInfo missing tags: %s", output)
}
@@ -189,6 +199,9 @@ func TestFormatTaskInfo_NoOptionalFields(t *testing.T) {
if !strings.Contains(output, "simple-uuid") {
t.Fatalf("FormatTaskInfo missing UUID: %s", output)
}
+ if !strings.Contains(output, "Started: no") {
+ t.Fatalf("FormatTaskInfo should show Started: no when not started: %s", output)
+ }
if strings.Contains(output, "Tags:") {
t.Fatalf("FormatTaskInfo should not contain Tags: for empty tags: %s", output)
}