Skip to content

Commit 955f2c1

Browse files
paxcalptclaude
andauthored
feat: TUI state persistence, new commands, and non-interactive improvements (#11)
* feat: TUI state persistence, new commands, and non-interactive improvements ## TUI State Persistence - Add configuration properties for remembering TUI state across sessions - remember_tui_state: master toggle (default: true) - tui_tree_view: persist tree/flat view preference - tui_last_view_item: persist selected repo/project/assignee - Restore view state on TUI startup - Save state on navigation (arrow keys, tree toggle) - Reset saved item when switching view modes (Tab key) - Fix bug where --repo flag always overwrote restored state - Update config display to show TUI persistence settings - Change tree toggle key from 'r' to 't' in help text ## New Commands - add-link: Add URLs to task links - Usage: tsk add-link <task_id> <url> - Validates HTTP/HTTPS URLs - append: Append text to task descriptions - Usage: tsk append <task_id> --text "content" - update: Batch update task fields - Usage: tsk update <task_ids> [--priority/--status/--project/etc] - Supports: priority, status, project, tags, assignees, due date, title ## Non-Interactive Terminal Detection - Add sys.stdin.isatty() checks across commands - delete: require --force flag in non-interactive mode - done: auto-confirm subtask marking in non-interactive mode - unarchive: auto-confirm subtask operations in non-interactive mode - sync: add --non-interactive flag to skip unexpected file prompts ## Archive Command Enhancement - Add --all-completed flag to archive all completed tasks at once - Usage: tsk archive --all-completed [--repo <name>] ## Sync Command Improvements - Add run_git_verbose() for interactive git operations - Implement SimpleSyncProgress for safer terminal output - Better handling of prompts and credential helpers - Improved display of unexpected files during sync 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: improve error handling for corrupted task files and git conflicts - Add silent_errors parameter to list_tasks() and list_archived_tasks() - Detect and report git conflict markers specifically - Collect all failed files and show helpful summary with resolution steps - Improve error messages to show filename only (not full path) - Suggest running 'git status' when conflicts are detected This makes the TUI more resilient to YAML parsing errors caused by unresolved git merge conflicts, preventing crashes and providing better guidance to users. * fix: resolve linting errors and type issues for CI - Fix all ruff linting errors (unused imports, whitespace, f-strings) - Move rich.console import to top of file_validation.py - Fix type error in update.py: use datetime instead of string for task.due - Format code with ruff formatter All must-fix issues from robot review addressed: - Type error in update.py:128 (datetime vs string) ✓ - detect_conflicts signature verified (skip_fetch parameter exists) ✓ - All linting errors fixed ✓ * ci(deps): bump GitHub Actions and pytest dependencies Integrate all dependabot dependency updates: - 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 This consolidates PRs #7, #8, #9, and #10. * chore: bump version to 0.10.17 --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 73fd0e9 commit 955f2c1

19 files changed

Lines changed: 820 additions & 212 deletions

File tree

.github/workflows/ci.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252

5353
steps:
5454
- name: Checkout code
55-
uses: actions/checkout@v5
55+
uses: actions/checkout@v6
5656

5757
- name: Set up Python
5858
uses: actions/setup-python@v6
@@ -66,7 +66,7 @@ jobs:
6666
enable-cache: true
6767

6868
- name: Cache dependencies
69-
uses: actions/cache@v4
69+
uses: actions/cache@v5
7070
with:
7171
path: |
7272
~/.cache/uv
@@ -111,7 +111,7 @@ jobs:
111111

112112
steps:
113113
- name: Checkout code
114-
uses: actions/checkout@v5
114+
uses: actions/checkout@v6
115115

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

127127
- name: Cache dependencies
128-
uses: actions/cache@v4
128+
uses: actions/cache@v5
129129
with:
130130
path: |
131131
~/.cache/uv
@@ -159,7 +159,7 @@ jobs:
159159

160160
steps:
161161
- name: Checkout code
162-
uses: actions/checkout@v5
162+
uses: actions/checkout@v6
163163

164164
- name: Set up Python
165165
uses: actions/setup-python@v6
@@ -173,7 +173,7 @@ jobs:
173173
enable-cache: true
174174

175175
- name: Cache dependencies
176-
uses: actions/cache@v4
176+
uses: actions/cache@v5
177177
with:
178178
path: |
179179
~/.cache/uv
@@ -193,7 +193,7 @@ jobs:
193193
echo "✅ Coverage report generated"
194194
195195
- name: Upload coverage artifact
196-
uses: actions/upload-artifact@v5
196+
uses: actions/upload-artifact@v6
197197
with:
198198
name: coverage-report
199199
path: coverage.xml
@@ -208,7 +208,7 @@ jobs:
208208

209209
steps:
210210
- name: Checkout code
211-
uses: actions/checkout@v5
211+
uses: actions/checkout@v6
212212

213213
- name: Set up Python
214214
uses: actions/setup-python@v6
@@ -239,7 +239,7 @@ jobs:
239239
echo "✅ Package verification complete"
240240
241241
- name: Upload build artifacts
242-
uses: actions/upload-artifact@v5
242+
uses: actions/upload-artifact@v6
243243
with:
244244
name: dist-packages
245245
path: dist/

CHANGELOG.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.10.17] - 2026-01-13
11+
12+
### Added
13+
14+
- **TUI State Persistence**: Remember TUI view mode and selection across sessions
15+
- New config option `remember_tui_state` (default: true)
16+
- Persists tree view state (`tui_tree_view`)
17+
- Remembers last selected view item (`tui_last_view_item`)
18+
- Automatically restores state when reopening TUI
19+
20+
- **New Task Management Commands**:
21+
- `tsk add-link`: Add URLs to task links field
22+
- `tsk append`: Append text to task descriptions
23+
- `tsk update`: Update multiple task fields atomically
24+
25+
- **Archive Command Improvements**:
26+
- New `--all-completed` flag to archive all completed tasks at once
27+
- Shows count of tasks to be archived before confirmation
28+
29+
- **Sync Command Non-Interactive Mode**:
30+
- New `--non-interactive` flag for automation and scripting
31+
- Auto-confirms all prompts with safe defaults
32+
- Enables seamless integration with CI/CD pipelines
33+
34+
### Changed
35+
36+
- **Improved Error Handling**: Better resilience for corrupted task files
37+
- Added `silent_errors` parameter to `list_tasks()` and `list_archived_tasks()`
38+
- Detects and reports git conflict markers specifically
39+
- Groups error messages with helpful resolution steps
40+
- Suggests running `git status` when conflicts detected
41+
42+
- **Code Quality**: Fixed all linting and formatting issues
43+
- Removed unused imports across multiple files
44+
- Fixed whitespace and f-string issues
45+
- Moved imports to top of files per PEP 8
46+
- Fixed type error in update command (datetime vs string)
47+
48+
### Dependencies
49+
50+
- Bump actions/checkout from v5 to v6
51+
- Bump actions/cache from v4 to v5
52+
- Bump actions/upload-artifact from v5 to v6
53+
- Update pytest requirement from <9.0 to <10.0
54+
1055
## [0.10.16] - 2025-12-19
1156

1257
### Changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Issues = "https://github.com/henriqueslab/TaskRepo/issues"
4343

4444
[project.optional-dependencies]
4545
dev = [
46-
"pytest>=7.4,<9.0",
46+
"pytest>=7.4,<10.0",
4747
"pytest-cov>=4.0",
4848
"ruff>=0.12.2",
4949
"mypy>=1.0",

src/taskrepo/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.10.16"
1+
__version__ = "0.10.17"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Add-link command for adding URLs to task links."""
2+
3+
from typing import Optional
4+
5+
import click
6+
7+
from taskrepo.core.repository import RepositoryManager
8+
from taskrepo.utils.helpers import find_task_by_title_or_id, select_task_from_result
9+
10+
11+
@click.command(name="add-link")
12+
@click.argument("task_id", required=True)
13+
@click.argument("url", required=True)
14+
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
15+
@click.pass_context
16+
def add_link(ctx, task_id: str, url: str, repo: Optional[str]):
17+
"""Add a link/URL to a task.
18+
19+
Examples:
20+
tsk add-link 5 "https://github.com/org/repo/issues/123"
21+
tsk add-link 10 "https://mail.google.com/..." --repo work
22+
23+
TASK_ID: Task ID, UUID, or title
24+
URL: URL to add to task links
25+
"""
26+
config = ctx.obj["config"]
27+
manager = RepositoryManager(config.parent_dir)
28+
29+
# Validate URL format
30+
if not url.startswith(("http://", "https://")):
31+
click.secho("Error: URL must start with http:// or https://", fg="red", err=True)
32+
ctx.exit(1)
33+
34+
# Find task
35+
result = find_task_by_title_or_id(manager, task_id, repo)
36+
37+
if result[0] is None:
38+
click.secho(f"Error: No task found matching '{task_id}'", fg="red", err=True)
39+
ctx.exit(1)
40+
41+
task, repository = select_task_from_result(ctx, result, task_id)
42+
43+
# Add link if not already present
44+
if task.links is None:
45+
task.links = []
46+
47+
if url in task.links:
48+
click.secho(f"Link already exists in task: {task.title}", fg="yellow")
49+
ctx.exit(0)
50+
51+
task.links.append(url)
52+
53+
# Save task
54+
repository.save_task(task)
55+
56+
click.secho(f"✓ Added link to task: {task.title}", fg="green")
57+
click.echo(f"\nLink added: {url}")
58+
click.echo(f"Total links: {len(task.links)}")
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Append command for adding content to task descriptions."""
2+
3+
from typing import Optional
4+
5+
import click
6+
7+
from taskrepo.core.repository import RepositoryManager
8+
from taskrepo.utils.helpers import find_task_by_title_or_id, select_task_from_result
9+
10+
11+
@click.command()
12+
@click.argument("task_id", required=True)
13+
@click.option("--text", "-t", required=True, help="Text to append to task description")
14+
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
15+
@click.pass_context
16+
def append(ctx, task_id: str, text: str, repo: Optional[str]):
17+
"""Append text to a task's description.
18+
19+
Examples:
20+
tsk append 5 --text "Additional note from meeting"
21+
tsk append 10 -t "Updated requirements" --repo work
22+
23+
TASK_ID: Task ID, UUID, or title to append to
24+
"""
25+
config = ctx.obj["config"]
26+
manager = RepositoryManager(config.parent_dir)
27+
28+
# Find task
29+
result = find_task_by_title_or_id(manager, task_id, repo)
30+
31+
if result[0] is None:
32+
click.secho(f"Error: No task found matching '{task_id}'", fg="red", err=True)
33+
ctx.exit(1)
34+
35+
task, repository = select_task_from_result(ctx, result, task_id)
36+
37+
# Append text to description
38+
if task.description:
39+
task.description = task.description.rstrip() + "\n\n" + text
40+
else:
41+
task.description = text
42+
43+
# Save task
44+
repository.save_task(task)
45+
46+
click.secho(f"✓ Appended text to task: {task.title}", fg="green")
47+
click.echo("\nNew content added:")
48+
click.echo(f" {text}")

src/taskrepo/cli/commands/archive.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,54 @@
1818
@click.argument("task_ids", nargs=-1)
1919
@click.option("--repo", "-r", help="Repository name (will search all repos if not specified)")
2020
@click.option("--yes", "-y", is_flag=True, help="Automatically archive subtasks (skip prompt)")
21+
@click.option("--all-completed", is_flag=True, help="Archive all completed tasks")
2122
@click.pass_context
22-
def archive(ctx, task_ids: Tuple[str, ...], repo, yes):
23+
def archive(ctx, task_ids: Tuple[str, ...], repo, yes, all_completed):
2324
"""Archive one or more tasks, or list archived tasks if no task IDs are provided.
2425
2526
TASK_IDS: One or more task IDs to archive (optional - if omitted, lists archived tasks)
27+
28+
Use --all-completed to archive all tasks with status 'completed' in one command.
2629
"""
2730
config = ctx.obj["config"]
2831
manager = RepositoryManager(config.parent_dir)
2932

33+
# Handle --all-completed flag
34+
if all_completed and not task_ids:
35+
# Get all completed tasks
36+
if repo:
37+
repository = manager.get_repository(repo)
38+
if not repository:
39+
click.secho(f"Error: Repository '{repo}' not found", fg="red", err=True)
40+
ctx.exit(1)
41+
all_tasks = repository.list_tasks(include_archived=False)
42+
else:
43+
all_tasks = manager.list_all_tasks(include_archived=False)
44+
45+
# Filter for completed status
46+
completed_tasks = [task for task in all_tasks if task.status == "completed"]
47+
48+
if not completed_tasks:
49+
repo_msg = f" in repository '{repo}'" if repo else ""
50+
click.echo(f"No completed tasks found{repo_msg}.")
51+
return
52+
53+
# Get display IDs from cache for completed tasks
54+
from taskrepo.utils.id_mapping import get_display_id_from_uuid
55+
56+
completed_ids = []
57+
for task in completed_tasks:
58+
display_id = get_display_id_from_uuid(task.id)
59+
if display_id:
60+
completed_ids.append(str(display_id))
61+
62+
if not completed_ids:
63+
click.echo("No completed tasks found with display IDs.")
64+
return
65+
66+
click.echo(f"Found {len(completed_ids)} completed task(s) to archive.")
67+
task_ids = tuple(completed_ids)
68+
3069
# If no task_ids provided, list archived tasks
3170
if not task_ids:
3271
# Get archived tasks from specified repo or all repos

src/taskrepo/cli/commands/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ def _display_config(config):
3737
cluster_status = "enabled" if config.cluster_due_dates else "disabled"
3838
click.echo(f" Due date clustering: {cluster_status}")
3939
click.echo(f" TUI view mode: {config.tui_view_mode}")
40+
remember_status = "enabled" if config.remember_tui_state else "disabled"
41+
click.echo(f" Remember TUI state: {remember_status}")
42+
tree_view_status = "enabled" if config.tui_tree_view else "disabled"
43+
click.echo(f" TUI tree view default: {tree_view_status}")
44+
last_item = config.tui_last_view_item or "(none)"
45+
click.echo(f" TUI last view item: {last_item}")
4046
click.secho("-" * 50, fg="green")
4147

4248

src/taskrepo/cli/commands/delete.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Delete command for removing tasks."""
22

3+
import sys
34
from typing import Tuple
45

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

3839
# Batch confirmation for multiple tasks (unless --force flag is used)
3940
if is_batch and not force:
41+
# Check if we're in a terminal - if not, skip confirmation (auto-cancel for safety)
42+
if not sys.stdin.isatty():
43+
click.echo("Warning: Non-interactive mode detected. Use --force to delete in non-interactive mode.")
44+
ctx.exit(1)
45+
4046
click.echo(f"\nAbout to delete {task_id_count} tasks. This cannot be undone.")
4147

4248
# Create a validator for y/n input
@@ -60,6 +66,11 @@ def delete_task_handler(task, repository):
6066
"""Handler to delete a task with optional confirmation."""
6167
# Single task confirmation (only if not batch and not force)
6268
if not is_batch and not force:
69+
# Check if we're in a terminal - if not, require --force flag
70+
if not sys.stdin.isatty():
71+
click.echo("Warning: Non-interactive mode detected. Use --force to delete in non-interactive mode.")
72+
ctx.exit(1)
73+
6374
# Format task display with colored UUID and title
6475
assignees_str = f" {', '.join(task.assignees)}" if task.assignees else ""
6576
project_str = f" [{task.project}]" if task.project else ""

0 commit comments

Comments
 (0)