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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## Unreleased

- Add `scheduled` and `deadline` date fields to the task
model across all four backends (org, markdown, sql,
json) and the cloud wire format. Both are date-only ISO
strings; `scheduled` is the snooze (later UI hides the
card until that day arrives) and `deadline` is the due
date (later UI surfaces it as an urgency cue). SQL gets
an idempotent `ALTER TABLE` migration so existing
databases pick up the columns on next open. The
add_task / update_task / MCP / API surfaces all accept
the new kwargs; UI follows in a separate PR.

- Replace the global "Show done" toggle on the kanban
board with per-column collapse. Each column has its own
chevron; collapsed columns shrink to a narrow strip with
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export interface Task {
created: string;
body: string;
github_url: string;
/** Date-only ISO string (``YYYY-MM-DD``) or null.
* Snooze date: when set and in the future, the card is
* hidden from the board until the day arrives. */
scheduled: string | null;
/** Date-only ISO string (``YYYY-MM-DD``) or null. */
deadline: string | null;
state_history?: StateChange[];
}

Expand Down
15 changes: 15 additions & 0 deletions kaisho/api/routers/kanban.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class TaskCreate(BaseModel):
tags: list[str] = []
body: str | None = None
github_url: str | None = None
# Date-only ISO strings (``YYYY-MM-DD``). ``scheduled``
# is the snooze (hidden / subdued until that day),
# ``deadline`` is the due date. Both are optional.
scheduled: str | None = None
deadline: str | None = None


class TaskUpdate(BaseModel):
Expand All @@ -22,6 +27,10 @@ class TaskUpdate(BaseModel):
customer: str | None = None
body: str | None = None
github_url: str | None = None
# ``None`` = leave unchanged (default Pydantic shape);
# ``""`` = clear; ``"YYYY-MM-DD"`` = set.
scheduled: str | None = None
deadline: str | None = None


class TagsUpdate(BaseModel):
Expand Down Expand Up @@ -66,6 +75,8 @@ def create_task(body: TaskCreate):
tags=body.tags,
body=body.body,
github_url=body.github_url,
scheduled=body.scheduled,
deadline=body.deadline,
)


Expand All @@ -85,13 +96,17 @@ def update_task(task_id: str, body: TaskUpdate):
or body.customer is not None
or body.body is not None
or body.github_url is not None
or body.scheduled is not None
or body.deadline is not None
):
result = tasks.update_task(
task_id,
title=body.title,
customer=body.customer,
body=body.body,
github_url=body.github_url,
scheduled=body.scheduled,
deadline=body.deadline,
)
if result is None:
raise HTTPException(
Expand Down
10 changes: 10 additions & 0 deletions kaisho/backends/json_backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ def add_task(
github_url=None,
sync_id=None,
task_id=None,
scheduled=None,
deadline=None,
) -> dict:
"""Create a new task and return its dict."""
tasks = _read_json(self._tasks_file)
Expand All @@ -225,6 +227,8 @@ def add_task(
"github_url": github_url or "",
"properties": {},
"created": datetime.now().isoformat(),
"scheduled": scheduled or None,
"deadline": deadline or None,
}
tasks.insert(0, task)
_write_json(self._tasks_file, tasks)
Expand Down Expand Up @@ -274,6 +278,8 @@ def update_task(
customer=None,
body=None,
github_url=None,
scheduled=None,
deadline=None,
) -> dict:
"""Update a task's fields and return updated dict."""
tasks = _read_json(self._tasks_file)
Expand All @@ -287,6 +293,10 @@ def update_task(
t["body"] = body
if github_url is not None:
t["github_url"] = github_url
if scheduled is not None:
t["scheduled"] = scheduled or None
if deadline is not None:
t["deadline"] = deadline or None
_write_json(self._tasks_file, tasks)
return t
raise ValueError(f"Task not found: {task_id}")
Expand Down
20 changes: 20 additions & 0 deletions kaisho/backends/markdown/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ def _section_to_task(sec: dict) -> dict:
"archive_status": meta.get(
"archive_status", ""
),
"scheduled": meta.get("scheduled") or None,
"deadline": meta.get("deadline") or None,
}


Expand All @@ -460,6 +462,14 @@ def _task_to_section(task: dict) -> dict:
meta["archived_at"] = task["archived_at"]
if task.get("archive_status"):
meta["archive_status"] = task["archive_status"]
# Only emit ``scheduled`` / ``deadline`` when set, so
# the absence of a value reads as a clean missing key
# in the YAML / TOML-style metadata header rather than
# an explicit empty.
if task.get("scheduled"):
meta["scheduled"] = task["scheduled"]
if task.get("deadline"):
meta["deadline"] = task["deadline"]
props = _task_meta_to_props({
**meta,
"properties": task.get("properties", {}),
Expand Down Expand Up @@ -588,6 +598,8 @@ def add_task(
github_url=None,
sync_id=None,
task_id=None,
scheduled=None,
deadline=None,
) -> dict:
"""Create a new task and return its dict."""
tasks = self._load_tasks()
Expand All @@ -604,6 +616,8 @@ def add_task(
"properties": {},
"created": now.isoformat(),
"updated_at": now.isoformat(),
"scheduled": scheduled or None,
"deadline": deadline or None,
}
tasks.insert(0, task)
self._save_tasks(tasks)
Expand Down Expand Up @@ -659,6 +673,8 @@ def update_task(
customer=None,
body=None,
github_url=None,
scheduled=None,
deadline=None,
) -> dict:
"""Update a task's fields and return updated dict."""
tasks = self._load_tasks()
Expand All @@ -672,6 +688,10 @@ def update_task(
t["body"] = body
if github_url is not None:
t["github_url"] = github_url
if scheduled is not None:
t["scheduled"] = scheduled or None
if deadline is not None:
t["deadline"] = deadline or None
t["updated_at"] = (
_local_now().isoformat()
)
Expand Down
8 changes: 8 additions & 0 deletions kaisho/backends/org/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def add_task(
github_url: str | None = None,
sync_id: str | None = None,
task_id: str | None = None,
scheduled: str | None = None,
deadline: str | None = None,
) -> dict:
"""Create a new task and return its dict."""
return kanban.add_task(
Expand All @@ -64,6 +66,8 @@ def add_task(
github_url=github_url,
sync_id=sync_id,
task_id=task_id,
scheduled=scheduled,
deadline=deadline,
)

def move_task(self, task_id: str, new_status: str) -> dict:
Expand Down Expand Up @@ -100,6 +104,8 @@ def update_task(
customer: str | None = None,
body: str | None = None,
github_url: str | None = None,
scheduled: str | None = None,
deadline: str | None = None,
) -> dict:
"""Update a task's fields and return updated dict."""
return kanban.update_task(
Expand All @@ -110,6 +116,8 @@ def update_task(
customer=customer,
body=body,
github_url=github_url,
scheduled=scheduled,
deadline=deadline,
)

def archive_task(self, task_id: str) -> bool:
Expand Down
54 changes: 53 additions & 1 deletion kaisho/backends/sql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ class TaskRow(Base):
created = Column(String, nullable=False)
archived_at = Column(String, nullable=True)
archive_status = Column(String, nullable=True)
# Date-only ISO strings (``YYYY-MM-DD``) or NULL.
# ``scheduled`` is the snooze: the task is hidden /
# subdued until that day arrives. ``deadline`` is the
# due date: shown but flagged when close or past. Both
# cross the wire so PWA / iOS see the same dates.
scheduled = Column(String, nullable=True)
deadline = Column(String, nullable=True)


class ClockRow(Base): # noqa: E302
Expand Down Expand Up @@ -168,6 +175,7 @@ def __init__(self, dsn: str):
self.engine = create_engine(dsn)
Base.metadata.create_all(self.engine)
_ensure_paused_column(self.engine)
_ensure_task_date_columns(self.engine)
_ensure_customer_used_offset_column(self.engine)
self._Session = sessionmaker(bind=self.engine)

Expand Down Expand Up @@ -199,6 +207,29 @@ def _ensure_paused_column(engine) -> None:
))


def _ensure_task_date_columns(engine) -> None:
"""Add ``tasks.scheduled`` and ``tasks.deadline`` columns
on legacy databases.

Same shape as ``_ensure_paused_column``: idempotent
per-column check, silent skip when already present.
"""
from sqlalchemy import inspect, text
inspector = inspect(engine)
if "tasks" not in inspector.get_table_names():
return
cols = {c["name"] for c in inspector.get_columns("tasks")}
with engine.begin() as conn:
if "scheduled" not in cols:
conn.execute(text(
"ALTER TABLE tasks ADD COLUMN scheduled VARCHAR"
))
if "deadline" not in cols:
conn.execute(text(
"ALTER TABLE tasks ADD COLUMN deadline VARCHAR"
))


def _ensure_customer_used_offset_column(engine) -> None:
"""Add the ``customers.used_offset`` column on legacy
databases.
Expand Down Expand Up @@ -383,6 +414,8 @@ def _task_row_to_dict(row: TaskRow) -> dict:
"created": row.created,
"archived_at": row.archived_at,
"archive_status": row.archive_status,
"scheduled": row.scheduled or None,
"deadline": row.deadline or None,
}


Expand Down Expand Up @@ -618,6 +651,8 @@ def add_task(
github_url=None,
sync_id=None,
task_id=None,
scheduled=None,
deadline=None,
) -> dict:
"""Create a new task and return its dict."""
# ``task_id`` is honoured when the caller wants to
Expand All @@ -636,6 +671,8 @@ def add_task(
github_url=github_url or "",
properties="{}",
created=now,
scheduled=scheduled or None,
deadline=deadline or None,
)
session = self._eng.session()
try:
Expand All @@ -653,6 +690,8 @@ def add_task(
"github_url": github_url or "",
"properties": {},
"created": now,
"scheduled": scheduled or None,
"deadline": deadline or None,
}

def move_task(self, task_id, new_status) -> dict:
Expand Down Expand Up @@ -723,8 +762,17 @@ def update_task(
customer=None,
body=None,
github_url=None,
scheduled=None,
deadline=None,
) -> dict:
"""Update a task's fields and return updated dict."""
"""Update a task's fields and return updated dict.

``scheduled`` / ``deadline`` follow the same
sentinel rules as every other field on this
helper: ``None`` leaves the existing value alone,
an empty string clears the column, a date string
(``YYYY-MM-DD``) sets it.
"""
session = self._eng.session()
try:
row = session.get(TaskRow, task_id)
Expand All @@ -740,6 +788,10 @@ def update_task(
row.body = body
if github_url is not None:
row.github_url = github_url
if scheduled is not None:
row.scheduled = scheduled or None
if deadline is not None:
row.deadline = deadline or None
session.commit()
result = _task_row_to_dict(row)
finally:
Expand Down
32 changes: 31 additions & 1 deletion kaisho/cron/tool_defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@
"link to this task"
),
},
"scheduled": {
"type": "string",
"description": (
"Snooze date (YYYY-MM-DD). Task is "
"hidden until that day arrives."
),
},
"deadline": {
"type": "string",
"description": (
"Due date (YYYY-MM-DD). Surfaced "
"as an urgency cue on the card."
),
},
},
"required": ["title"],
},
Expand Down Expand Up @@ -380,7 +394,7 @@
"tier": "write",
"description": (
"Update a task's title, customer, body, "
"or GitHub URL."
"GitHub URL, scheduled date, or deadline."
),
"input_schema": {
"type": "object",
Expand Down Expand Up @@ -411,6 +425,22 @@
"(optional)"
),
},
"scheduled": {
"type": "string",
"description": (
"Snooze date (YYYY-MM-DD), empty "
"string to clear, omit to leave "
"unchanged."
),
},
"deadline": {
"type": "string",
"description": (
"Due date (YYYY-MM-DD), empty "
"string to clear, omit to leave "
"unchanged."
),
},
},
"required": ["task_id"],
},
Expand Down
Loading
Loading