Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
Expand All @@ -66,7 +66,7 @@ jobs:
enable-cache: true

- name: Cache dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.cache/uv
Expand Down Expand Up @@ -111,7 +111,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
Expand All @@ -125,7 +125,7 @@ jobs:
enable-cache: true

- name: Cache dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.cache/uv
Expand Down Expand Up @@ -159,7 +159,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
Expand All @@ -173,7 +173,7 @@ jobs:
enable-cache: true

- name: Cache dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.cache/uv
Expand All @@ -193,7 +193,7 @@ jobs:
echo "βœ… Coverage report generated"

- name: Upload coverage artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: coverage-report
path: coverage.xml
Expand All @@ -208,7 +208,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
Expand Down Expand Up @@ -239,7 +239,7 @@ jobs:
echo "βœ… Package verification complete"

- name: Upload build artifacts
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: dist-packages
path: dist/
Expand Down
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.10.17] - 2026-01-13

### Added

- **TUI State Persistence**: Remember TUI view mode and selection across sessions
- New config option `remember_tui_state` (default: true)
- Persists tree view state (`tui_tree_view`)
- Remembers last selected view item (`tui_last_view_item`)
- Automatically restores state when reopening TUI

- **New Task Management Commands**:
- `tsk add-link`: Add URLs to task links field
- `tsk append`: Append text to task descriptions
- `tsk update`: Update multiple task fields atomically

- **Archive Command Improvements**:
- New `--all-completed` flag to archive all completed tasks at once
- Shows count of tasks to be archived before confirmation

- **Sync Command Non-Interactive Mode**:
- New `--non-interactive` flag for automation and scripting
- Auto-confirms all prompts with safe defaults
- Enables seamless integration with CI/CD pipelines

### Changed

- **Improved Error Handling**: Better resilience for corrupted task files
- Added `silent_errors` parameter to `list_tasks()` and `list_archived_tasks()`
- Detects and reports git conflict markers specifically
- Groups error messages with helpful resolution steps
- Suggests running `git status` when conflicts detected

- **Code Quality**: Fixed all linting and formatting issues
- Removed unused imports across multiple files
- Fixed whitespace and f-string issues
- Moved imports to top of files per PEP 8
- Fixed type error in update command (datetime vs string)

### Dependencies

- Bump actions/checkout from v5 to v6
- Bump actions/cache from v4 to v5
- Bump actions/upload-artifact from v5 to v6
- Update pytest requirement from <9.0 to <10.0

## [0.10.16] - 2025-12-19

### Changed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Issues = "https://github.com/henriqueslab/TaskRepo/issues"

[project.optional-dependencies]
dev = [
"pytest>=7.4,<9.0",
"pytest>=7.4,<10.0",
"pytest-cov>=4.0",
"ruff>=0.12.2",
"mypy>=1.0",
Expand Down
2 changes: 1 addition & 1 deletion src/taskrepo/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.10.16"
__version__ = "0.10.17"
58 changes: 58 additions & 0 deletions src/taskrepo/cli/commands/add_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Add-link command for adding URLs to task links."""

from typing import Optional
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import. The sys module is imported but never used in this file. Remove this unused import.

Copilot uses AI. Check for mistakes.

import click

from taskrepo.core.repository import RepositoryManager
from taskrepo.utils.helpers import find_task_by_title_or_id, select_task_from_result


@click.command(name="add-link")
@click.argument("task_id", required=True)
@click.argument("url", required=True)
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
@click.pass_context
def add_link(ctx, task_id: str, url: str, repo: Optional[str]):
"""Add a link/URL to a task.
Examples:
tsk add-link 5 "https://github.com/org/repo/issues/123"
tsk add-link 10 "https://mail.google.com/..." --repo work
TASK_ID: Task ID, UUID, or title
URL: URL to add to task links
"""
config = ctx.obj["config"]
manager = RepositoryManager(config.parent_dir)

# Validate URL format
if not url.startswith(("http://", "https://")):
click.secho("Error: URL must start with http:// or https://", fg="red", err=True)
ctx.exit(1)

# Find task
result = find_task_by_title_or_id(manager, task_id, repo)

if result[0] is None:
click.secho(f"Error: No task found matching '{task_id}'", fg="red", err=True)
ctx.exit(1)

task, repository = select_task_from_result(ctx, result, task_id)

# Add link if not already present
if task.links is None:
task.links = []

if url in task.links:
click.secho(f"Link already exists in task: {task.title}", fg="yellow")
ctx.exit(0)

task.links.append(url)

# Save task
repository.save_task(task)

click.secho(f"βœ“ Added link to task: {task.title}", fg="green")
click.echo(f"\nLink added: {url}")
click.echo(f"Total links: {len(task.links)}")
48 changes: 48 additions & 0 deletions src/taskrepo/cli/commands/append.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Append command for adding content to task descriptions."""

from typing import Optional
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import. The sys module is imported but never used in this file. Remove this unused import.

Copilot uses AI. Check for mistakes.

import click

from taskrepo.core.repository import RepositoryManager
from taskrepo.utils.helpers import find_task_by_title_or_id, select_task_from_result


@click.command()
@click.argument("task_id", required=True)
@click.option("--text", "-t", required=True, help="Text to append to task description")
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
@click.pass_context
def append(ctx, task_id: str, text: str, repo: Optional[str]):
"""Append text to a task's description.
Examples:
tsk append 5 --text "Additional note from meeting"
tsk append 10 -t "Updated requirements" --repo work
TASK_ID: Task ID, UUID, or title to append to
"""
config = ctx.obj["config"]
manager = RepositoryManager(config.parent_dir)

# Find task
result = find_task_by_title_or_id(manager, task_id, repo)

if result[0] is None:
click.secho(f"Error: No task found matching '{task_id}'", fg="red", err=True)
ctx.exit(1)

task, repository = select_task_from_result(ctx, result, task_id)

# Append text to description
if task.description:
task.description = task.description.rstrip() + "\n\n" + text
else:
task.description = text

# Save task
repository.save_task(task)

click.secho(f"βœ“ Appended text to task: {task.title}", fg="green")
click.echo("\nNew content added:")
click.echo(f" {text}")
41 changes: 40 additions & 1 deletion src/taskrepo/cli/commands/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,54 @@
@click.argument("task_ids", nargs=-1)
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
@click.option("--yes", "-y", is_flag=True, help="Automatically archive subtasks (skip prompt)")
@click.option("--all-completed", is_flag=True, help="Archive all completed tasks")
@click.pass_context
def archive(ctx, task_ids: Tuple[str, ...], repo, yes):
def archive(ctx, task_ids: Tuple[str, ...], repo, yes, all_completed):
"""Archive one or more tasks, or list archived tasks if no task IDs are provided.
TASK_IDS: One or more task IDs to archive (optional - if omitted, lists archived tasks)
Use --all-completed to archive all tasks with status 'completed' in one command.
"""
config = ctx.obj["config"]
manager = RepositoryManager(config.parent_dir)

Comment on lines +28 to 32
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic issue with --all-completed flag. If both --all-completed and task_ids are provided, the flag is ignored (line 34 condition: "if all_completed and not task_ids"). This could be confusing for users. Consider either: 1) rejecting when both are provided with an error message, or 2) documenting that --all-completed is ignored when explicit task IDs are provided.

Suggested change
Use --all-completed to archive all tasks with status 'completed' in one command.
"""
config = ctx.obj["config"]
manager = RepositoryManager(config.parent_dir)
Use --all-completed to archive all tasks with status 'completed' in one command.
Note: --all-completed cannot be used together with explicit TASK_IDS.
"""
config = ctx.obj["config"]
manager = RepositoryManager(config.parent_dir)
# Disallow using --all-completed together with explicit task IDs to avoid confusion
if all_completed and task_ids:
click.secho(
"Error: --all-completed cannot be used together with TASK_IDS. "
"Either specify task IDs, or use --all-completed without IDs.",
fg="red",
err=True,
)
ctx.exit(1)

Copilot uses AI. Check for mistakes.
# Handle --all-completed flag
if all_completed and not task_ids:
# Get all completed tasks
if repo:
repository = manager.get_repository(repo)
if not repository:
click.secho(f"Error: Repository '{repo}' not found", fg="red", err=True)
ctx.exit(1)
all_tasks = repository.list_tasks(include_archived=False)
else:
all_tasks = manager.list_all_tasks(include_archived=False)

# Filter for completed status
completed_tasks = [task for task in all_tasks if task.status == "completed"]

if not completed_tasks:
repo_msg = f" in repository '{repo}'" if repo else ""
click.echo(f"No completed tasks found{repo_msg}.")
return

# Get display IDs from cache for completed tasks
from taskrepo.utils.id_mapping import get_display_id_from_uuid

completed_ids = []
for task in completed_tasks:
display_id = get_display_id_from_uuid(task.id)
if display_id:
completed_ids.append(str(display_id))

if not completed_ids:
click.echo("No completed tasks found with display IDs.")
return

click.echo(f"Found {len(completed_ids)} completed task(s) to archive.")
task_ids = tuple(completed_ids)

# If no task_ids provided, list archived tasks
if not task_ids:
# Get archived tasks from specified repo or all repos
Expand Down
6 changes: 6 additions & 0 deletions src/taskrepo/cli/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ def _display_config(config):
cluster_status = "enabled" if config.cluster_due_dates else "disabled"
click.echo(f" Due date clustering: {cluster_status}")
click.echo(f" TUI view mode: {config.tui_view_mode}")
remember_status = "enabled" if config.remember_tui_state else "disabled"
click.echo(f" Remember TUI state: {remember_status}")
tree_view_status = "enabled" if config.tui_tree_view else "disabled"
click.echo(f" TUI tree view default: {tree_view_status}")
last_item = config.tui_last_view_item or "(none)"
click.echo(f" TUI last view item: {last_item}")
click.secho("-" * 50, fg="green")


Expand Down
11 changes: 11 additions & 0 deletions src/taskrepo/cli/commands/delete.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Delete command for removing tasks."""

import sys
from typing import Tuple

import click
Expand Down Expand Up @@ -37,6 +38,11 @@ def delete(ctx, task_ids: Tuple[str, ...], repo, force):

# Batch confirmation for multiple tasks (unless --force flag is used)
if is_batch and not force:
# Check if we're in a terminal - if not, skip confirmation (auto-cancel for safety)
if not sys.stdin.isatty():
click.echo("Warning: Non-interactive mode detected. Use --force to delete in non-interactive mode.")
ctx.exit(1)

click.echo(f"\nAbout to delete {task_id_count} tasks. This cannot be undone.")

# Create a validator for y/n input
Expand All @@ -60,6 +66,11 @@ def delete_task_handler(task, repository):
"""Handler to delete a task with optional confirmation."""
# Single task confirmation (only if not batch and not force)
if not is_batch and not force:
# Check if we're in a terminal - if not, require --force flag
if not sys.stdin.isatty():
click.echo("Warning: Non-interactive mode detected. Use --force to delete in non-interactive mode.")
ctx.exit(1)

# Format task display with colored UUID and title
assignees_str = f" {', '.join(task.assignees)}" if task.assignees else ""
project_str = f" [{task.project}]" if task.project else ""
Expand Down
Loading