From 1514d94ce994072a761ac03b7d7a86f181bbe711 Mon Sep 17 00:00:00 2001 From: Mani Chandra Dulam Date: Tue, 28 Apr 2026 15:08:58 +0530 Subject: [PATCH 1/5] docs(proposal): add 0001-task-management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds proposal 0001 — a thin opt-in layer above terminals so a developer's intent ("fix the auth bug", "learn about CRDTs") can persist independently of any process. Three layers — Project, Task, Terminal — with user-defined kanban boards per project, persisted as plain markdown that mirrors the obsidian-kanban format. The principle ("Kolu has agency over IO; Kolu has no agency over semantics") is what keeps the feature small: Kolu reads, parses, and writes the user's task data on user action — never autonomously, never inferred from agent state, never derived from terminal exit. Includes a sibling HTML mockup at docs/proposals/0001-task-management/ showing four proposed Kolu surfaces (per-project board, cross-project list, terminal canvas with task tags, creation UI with palette modal + per-lane inline add) and one explanation-only view (raw markdown side-by-side with the rendered board, included to make the file format concrete — not a proposed Kolu surface). Status: draft. Several open questions (default lanes on first project, "No Project" UX in the project picker, completion-timestamp visual treatment) call for directional feedback before promoting to accepted. _Generated by [\`/do\`](https://github.com/srid/agency) on Claude Code (model \`claude-opus-4-7\`)._ --- docs/proposals/0001-task-management.md | 221 +++ .../0001-task-management/mockup.html | 1189 +++++++++++++++++ 2 files changed, 1410 insertions(+) create mode 100644 docs/proposals/0001-task-management.md create mode 100644 docs/proposals/0001-task-management/mockup.html diff --git a/docs/proposals/0001-task-management.md b/docs/proposals/0001-task-management.md new file mode 100644 index 00000000..4c9d5364 --- /dev/null +++ b/docs/proposals/0001-task-management.md @@ -0,0 +1,221 @@ +--- +title: Task Management +number: 0001 +status: draft +author: ThisIsMani +created: 2026-04-27 +--- + +# 0001 — Task Management + +## Summary + +Add a thin, opt-in layer above terminals so a developer's *intent* — "fix the auth bug", "learn about CRDTs", "investigate the slow startup" — can persist independently of any process. Projects group related work; tasks represent intent inside a project; terminals optionally tag themselves to a task. State lives in plain markdown (one file per project, format mirroring [obsidian-kanban](https://github.com/obsidian-community/obsidian-kanban)) which Kolu reads, parses, and writes on user action — never autonomously. Nothing about today's terminal-centric flow changes; users who don't opt in see no difference. + +## Motivation + +Kolu today is excellent at terminals — spawn one, focus it, split it, switch between them. There is nothing above the terminal. Once a terminal is closed, the *intent* it served is gone. Concrete scenarios where that falls short: + +- *"What was I doing yesterday?"* — A developer closed a session that involved three terminals investigating a flaky test. Today, that effort is unrecoverable except from shell history. Wanted: a labeled bookmark — "investigating flaky test X" — that survives the terminals and tells them where they were. +- *"I want to learn about Y."* — The intent has no repo. Today, the developer either spawns a terminal at `~/` and gives the agent instructions verbally, or doesn't capture the intent at all. Wanted: a place to write down "learn about CRDTs" and attach whichever terminals end up serving it. +- *"These three terminals are the same effort."* — A debugging session that grew to several panes shares one underlying purpose. Today there is no way to express that grouping. Wanted: a parent the user can name. +- *"Where did I park that task?"* — Coming back after a context switch, the developer wants a list of unfinished work — not a list of recent repos, not a list of zombie terminals. + +The thread connecting all four: a unit of intent that exists *above* terminals, scoped per project, fully under user control. + +## Core principle + +**Kolu has agency over IO. Kolu has no agency over semantics.** + +Kolu reads, parses, and writes the user's task data when the user acts. Kolu does *not* infer, autonomously update, observe agents and decide things, or generally do anything "smart" with the data on its own. The user is the brain; Kolu is the hands. + +Concretely: + +- A task is "done" when the user says it is. Not when a terminal exits, not when an agent's `taskProgress` reaches `n/n`, not when the agent says "I think I'm done." +- A task moves between lanes because the user moves it. Never because Kolu inferred something. +- Kolu writes the persisted form (markdown) on user action; it never writes spontaneously. + +This principle is what keeps the feature small. Every time something feels like it wants to be "smart", check it against this principle and the answer is usually "no, the user does that themselves." + +## User-facing behavior + +Three layers, each opt-in: + +``` +Project + └── Tasks (live in user-defined lanes) + └── Terminals (optionally tagged with taskId) +``` + +### Projects + +A user-curated, named, optionally path-bound container. + +```ts +type Project = { + id: string; // kolu-managed UUID + name: string; // user-chosen + repoPath?: string; // optional; missing for topic projects and the synthetic "No Project" + archived?: boolean; +}; +``` + +`repoPath` is **optional**. Most projects have a path (a real repo on disk); some don't (topic-only projects like *"Learn CRDTs"*, or the synthetic "No Project" container described below). Kolu's `strict` TypeScript enforces a null branch at every consumer that needs the path — terminal-cwd seeding, git operations, file browser — so the absence is type-checked, not silently ignored. + +A reserved synthetic project — displayed as **"No Project"** — exists to hold *floating* tasks (tasks the user created without picking a project). It is identified by a single reserved UUID constant kept in code, not by its display name and not by `repoPath` being absent. A user who creates a real project and names it "No Project" does not collide with the synthetic one — identity is structural, not textual. + +### Tasks + +Pure intent metadata. A task is a labeled bookmark of "what I was working on." + +```ts +type Task = { + id: string; // kolu-managed; encoded as a hidden block-id in markdown + projectId: string; // mandatory; floating tasks live under the synthetic "No Project" + name: string; // user-typed + description?: string; // user-typed; freeform markdown + createdAt: number; + updatedAt: number; + completedAt?: number; // set on first entry into a `**Complete**` lane; immutable once set +}; +``` + +Notably **absent**: a `status` field. The lane a task lives in *is* its status. Carrying `status` on the task object would be a denormalization of lane membership and would require two writes for every state change. Status is computed at read time from "which lane currently contains this task." + +`completedAt` semantics: **first-completion, immutable**. Set once when a task first lands in a lane flagged as completion. Moving it back out does not clear it. Moving back in does not update it. Re-opening a "completed" task is a separate concept that this proposal deliberately does not model. + +### Lanes + +User-defined per project. A lane is just a heading in the project's markdown file; the lane name is whatever the user typed. Two projects can both have lanes named "Done", or one called "🔥 fires" and another "Reading" — the system imposes no vocabulary. + +A lane can carry a `complete` *role* (zero or one role today; the schema leaves room for more — see Out of scope). A lane with the `complete` role is treated as the "done" column: tasks landing there get a `completedAt` timestamp. + +### Creating a task + +There is **one underlying operation** — `createTask(input)` — invoked through two affordances: + +- **Per-lane "+ add task"** — an inline input at the bottom of each lane on the project board. Project and lane are inferred from context; the user types only a title. Fast path for "drop a thought into the right column." +- **Command palette → "New task"** — a modal asking project, lane, title, description. Lane defaults to the project's first lane; project defaults to the last-active project (or "No Project"). Slower, more deliberate path; works from anywhere, including with no project open. + +Both affordances pre-fill different subsets of the same input shape. They share the write path. There is no `createTaskInline` and `createTaskModal` — only `createTask`. + +### The project board (per project) + +The primary view for working *inside* a project. Lanes laid out left-to-right; tasks shown as cards within. Cards expose: + +- Title +- Optional description preview +- Linked terminal pills (if any terminals are tagged to this task) +- A check + completion timestamp when the lane carries the `complete` role + +Cards are dragged between lanes to change status; the `+ add task` affordance lives at the bottom of each lane. + +### Cross-project view + +A separate view — **not a board**. A time-ordered list/table over all tasks across all projects: + +| Updated | Project | Lane | Task | +|---------|---------|------|------| +| 2h ago | kolu-board-spec | Doing | Learn CRDTs | +| 5h ago | kolu | Todo | Refactor session module | +| yesterday | kolu-board-spec | Done ✓ | Set up dev env | +| 3d ago | No Project | Reading | Read CRDT paper X | + +`Lane` is just a string per project — no aggregation, no normalization. Project A's "Doing" and Project B's "In progress" are unrelated strings. Filter by project, lane name, or recency; sort however. + +### Terminals: optional task tagging + +A terminal can be tagged with a `taskId`. Tagged terminals show a "▸ task name" line in their title bar; clicking it jumps to the task in its project board. Untagged terminals look exactly like Kolu's terminals today. + +Tagging is a user act. Kolu does not infer task-terminal relationships from `cwd`, agent kind, or any heuristic. + +### Storage + +Each project has one markdown file. Format mirrors [obsidian-kanban](https://github.com/obsidian-community/obsidian-kanban) so the file is parser-compatible if a user ever wants to view or edit it in Obsidian: + +```markdown +--- +kolu-board: v1 +projectId: 7f3e... +--- + +## Todo + +- [ ] Fix login bug +- [ ] Refactor session module + Splits the auth and session storage modules. + +## Doing + +- [ ] Learn CRDTs + +## Done + +**Complete** + +- [x] Set up dev env @{2026-04-20} +``` + +Conventions: + +- `## LaneName` — heading is the lane. +- `**Complete**` immediately under a heading flags the lane with the `complete` role (matches obsidian-kanban exactly). +- `- [ ]` / `- [x]` items are tasks. Checkbox state is decorative; lane membership is canonical. +- `` HTML comments hold task block-ids. Hidden in rendered markdown so users don't accidentally clobber them while editing. +- `@{2026-04-20}` is a completion timestamp annotation written by Kolu when a task enters a `complete` lane. + +The file is **plain markdown**. Kolu is the primary editor, but users can hand-edit in any tool. Kolu watches for filesystem changes and reparses on update. Last-write-wins conflict resolution; reasonable for a personal-use feature. + +**Default location**: `~/.config/kolu/projects//board.md` — alongside Kolu's other state. Survives Kolu uninstall/reinstall as a single directory copy; doesn't pollute the user's repo. Two distinct user populations are concretely known to exist: + +1. *Tool-managed* — the default. Kolu owns storage; user doesn't version it. +2. *Git-versioned* — board lives at `/.kolu/board.md`, committed alongside code. + +Both populations are real today. The default covers (1); the implementation should leave the path-resolution layer pluggable so (2) can be added without schema reshaping. + +## Prototype + +See [`./0001-task-management/mockup.html`](./0001-task-management/mockup.html) — open in a browser. Five views: + +1. **Per-project board** — lanes, cards, terminal pills, completion check. +2. **What's on disk** *(explanation only)* — the raw markdown that produces view 1, rendered side-by-side. Not a proposed Kolu surface; included to make the file format concrete. +3. **Cross-project view** — time-ordered table. +4. **Terminal canvas with task tags** — existing canvas; one new title-bar affordance. +5. **Creation UI** *(proposed)* — per-lane inline `+ add` and the command-palette `New task` modal. + +## Implementation notes + +Optional pointers, not prescriptions: + +- Block-IDs (`^abc123`) are kolu-managed and must be invisible to the user — store them in HTML comments so hand-edits don't clobber them. +- The path-resolution layer that maps `(projectId) → board.md path` should be small and replaceable. The default implementation returns `~/.config/kolu/projects//board.md`; a future per-project `boardPath` override (for git-versioned boards) plugs into the same seam. +- The synthetic "No Project" container is identified by a single reserved UUID constant. Display rendering substitutes the label "No Project"; storage and code paths treat it like any other project. + +## Alternatives considered + +- **`status` as a field on Task.** Rejected — denormalizes lane membership, requires two writes per state change. Lane is canonical; status is computed at read time. +- **Predefined status enum (`todo / doing / done`)** with user-defined sub-lanes. Rejected — collapsing different per-project meanings (e.g. `Blocked` shown under `Todo` in a unified view) is misleading. Cross-project alignment is solved by switching to a *time-ordered list* rather than a unified board. +- **Hybrid: predefined enum + freeform tags.** Rejected — gives up the "I name my own lifecycle" win, which is the whole reason for adopting an obsidian-kanban-style model. +- **Agent-driven completion** (Kolu observing `taskProgress` from `agent-provider.ts` and writing back). Rejected by the core principle — agents may *suggest*, but the user *decides*. The UI may surface "agent thinks this is done" as a passive affordance; it never writes. +- **Inferring task done from terminal exit.** Rejected — process lifecycle and intent lifecycle are independent clocks. A terminal can exit because the user closed it; the task is nowhere near done. +- **Gantt views.** Rejected — Gantt needs `startsAt` *and* `endsAt`. Picking a "start" lane introduces a second designation problem and edges into project-management territory the proposal explicitly excludes (see Out of scope). +- **`Project` and `recentRepos` unified into one store.** Rejected — auto-MRU and user curation have different change rates and serve different purposes (*"where was I?"* vs *"what am I working on?"*). They stay parallel. +- **`Board` as a domain entity** distinct from `Project`. Rejected — every project has exactly one board. "Board" was a phantom concept; the project *is* its board. +- **Persistent JSON index alongside the markdown.** Rejected — same data in two stores invites drift. The in-memory parsed representation is enough; the markdown file is canonical. + +## Open questions + +- **Default lanes on first project creation.** Probably `Todo`, `Doing`, `Done` (with `Done` flagged `**Complete**`). Worth confirming on review or shipping a default the user can immediately edit. +- **"No Project" UX.** Pin to top of the project picker? Hide until it has tasks? Always show with a distinct treatment? Open. +- **Visual treatment of completion timestamp annotations** (`@{2026-04-20}`) inside the markdown. Inline, sidebar, hover-only? Open. + +## Out of scope + +These are deliberate exclusions: + +- **Project-management features.** No assignees, due dates, priorities, dependencies, blockers, sprints, milestones, recurring tasks. This is a personal kanban, not a tracker. +- **Gantt charts and time-bound planning views.** Adding `startsAt` / `endsAt` would expand the task object well beyond "intent metadata". +- **Agent-driven autonomous mutation.** Agents observing terminal/agent state and writing to tasks without user action remains banned. Per-agent completion volatility (Claude Code's TaskCreate, OpenCode's todo SQLite) stays inside `packages/integrations/anyagent/src/agent-provider.ts`. The UI may show "agent thinks this is done" as a passive read-only affordance; it never writes. +- **Programmatic mutation surface for user-invoked clients.** A way for the user to mutate tasks from outside the Kolu UI — via an MCP server, a CLI, an agent tool, a plugin host, or some other shape — is anticipated as a future addition. The form, trust model, and auth design are all undecided. Out of scope for this proposal; deserves its own. +- **Replacing `recentRepos`.** That auto-MRU stays untouched. A user opening a recent repo without ever creating a Project for it should keep working exactly as today. +- **Board format interop with non-Obsidian tools** beyond what falls out of mirroring obsidian-kanban's syntax. No adapters, no bidirectional sync to other task systems. diff --git a/docs/proposals/0001-task-management/mockup.html b/docs/proposals/0001-task-management/mockup.html new file mode 100644 index 00000000..f0e70a22 --- /dev/null +++ b/docs/proposals/0001-task-management/mockup.html @@ -0,0 +1,1189 @@ + + + + + + Kolu · Task Management Mockups + + + +
+ + + +
+ Proposed views — Kolu surfaces this proposal asks to add +
+ + +
+ + +
+
+ +
+ kolu + + kolu-board-spec +
+ ⌘K +
+ +
+
+
+ +
+
+ Todo + 2 +
+
+
Refactor session module
+
Splits the auth and session storage modules.
+
+
+
Fix login bug
+
+ term-1 + term-2 +
+
+
add task…
+
+ + +
+
+ Doing + 1 +
+
+
Learn CRDTs
+
Read paper; prototype a tiny LWW register; write up.
+
+ term-3 +
+
+
add task…
+
+ + +
+
+ Done + 1 + **Complete** +
+
+
+ Set up dev env + ✓ 2d ago +
+
+
add task…
+
+
+
+ +
+
+ + Explanation only — what's on disk +
+
~/.config/kolu/projects/<id>/board.md
+
---
+kolu-board: v1
+projectId: 7f3e...
+---
+
+## Todo
+
+- [ ] Refactor session module <!-- ^def456 -->
+  Splits the auth and session storage modules.
+- [ ] Fix login bug <!-- ^abc123 -->
+
+## Doing
+
+- [ ] Learn CRDTs <!-- ^ghi789 -->
+  Read paper; prototype a tiny LWW register; write up.
+
+## Done
+
+**Complete**
+
+- [x] Set up dev env <!-- ^jkl012 --> @{2026-04-25}
+
+
+
+ +
+ What's shown (left, proposed): Lanes are user-named. The + **Complete** badge marks a lane as carrying the complete role; tasks + landing there get a completion timestamp. Term-pills under cards are linked terminals; clicking one + focuses that terminal on the canvas. The + + add task… at the bottom of each lane is one of the two creation affordances (see + view 04). +
+
+ What's shown (right, explanation only): The raw markdown that produces the board + on the left. This is not a proposed Kolu surface — Kolu is not proposing to render + a side-by-side raw-source panel. The panel exists in this mockup so reviewers can see the file + format concretely. Block-id anchors (<!-- ^abc123 -->) are kolu-managed and + intentionally hidden in HTML comments. +
+
+ + +
+ + +
+
+ +
+ Tasks · all projects +
+
+ +
+
+ Filter + project: any ⌄ + lane: any ⌄ + order: updated ⌄ + + 4 tasks +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
UpdatedProjectLaneTask
2h agokolu-board-specDoingLearn CRDTs
5h agokoluTodoRefactor session module
yesterdaykolu-board-specDone ✓Set up dev env
3d agoNo ProjectReadingRead CRDT paper X
+
+
+ +
+ What's shown: One row per task across every project. Lane is a string + per project — note that No Project has a lane called Reading that no other + project has, and that's fine. The view does not align lane names across projects. Floating tasks + live under the synthetic project displayed as "No Project". That display label is + rendered by the UI when a project's id matches the reserved sentinel constant — never + by name match — so a user who creates a real project named "No Project" doesn't collide with the + synthetic one. +
+
+ + +
+ + +
+
+ +
+ kolu + + 2 terminals +
+ ⌘K +
+ +
+
+
+
+ + claude + ~/kolu +
+ +
+
+
$ claude "investigate the login 500"
+
⎯ thinking …
+
Looking at auth/session.ts
+
$
+
+
+ +
+
+
+ zsh + ~/kolu +
+ +
+
+
$ just test-quick
+
120 passed · 0 failed
+
$
+
+
+
+
+ +
+ What's shown: The left tile (agent running) is tagged with the + Fix login bug task — clicking the link jumps to that task in its project board. The right + tile is untagged and shows a hint affordance. Untagged is the default; tagging is opt-in. Nothing + else about the canvas changes — the title bar gains one line, that's it. +
+
+ + +
+ + +
+
+ +
+ creating a task +
+
+ +
+ +
+ Per-lane inline +

+ Click + add task… at the + bottom of any lane. Project + lane are inferred; you only type the title. +

+
+
+ Todo + 2 +
+
+
Refactor session module
+
+
+
Fix login bug
+
+
+ Investigate flaky test +
+
+

+ ↵ to create · esc to cancel · ⇥ to expand into the modal +

+
+ + + +
+
+ +
+ What's shown: Two affordances calling one underlying + createTask(input) operation. The per-lane inline (left) pre-fills project + lane, asks + only for title — fast path. The command palette (right) elicits all fields with sensible defaults + (last-active project, project's first lane) — works from anywhere, including with no project open. + They share the write path; affordances differ only in which fields they ask for. +
+
+
+ + From afb590fa70b5381e0c11dbec49b5349ae4d2acdd Mon Sep 17 00:00:00 2001 From: Mani Chandra Dulam Date: Tue, 28 Apr 2026 15:55:56 +0530 Subject: [PATCH 2/5] =?UTF-8?q?docs(proposal):=200001=20=E2=80=94=20naviga?= =?UTF-8?q?tion,=20card=20interactions,=20project=20+=20task=20lifecycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds in the architectural and UX decisions from a follow-up talk-mode session structurally reviewed by both Lowy and Hickey lenses. **Navigation.** Adds an explicit Navigation subsection: two top-level routes (`/` for the workspace, `/projects/` for a project board). Project addressing uses the existing UUID `id` directly — no URL-friendly `slug` field; rejected because slug-collisions force fallback to UUID, meaning UUID was always the identity. Project navigation lives entirely in the URL; `preferences.activeProject` is explicitly rejected to prevent two stores claiming the same fact. Mobile swipe (per #622) is preserved as a gesture layer that calls `navigate()` rather than a parallel navigation stack. **Card interactions.** Cards become self-sufficient — five affordances on each card (drag, click-title, click-description, click-pill, click-✕) with no detail view, modal-on-click, or drawer. Long descriptions collapse by heuristic with session-only state — never persisted to markdown or preferences. **Project lifecycle.** Palette → CreateProjectModal / EditProjectModal (deliberately separate components sharing only a stateless ProjectForm). Archive is the in-app affordance; project deletion stays a manual filesystem operation. **Task lifecycle.** Inline edit primary, palette EditTaskModal as a keyboard-only secondary path. Delete is anchored to the card's ✕ because Cmd+K has no notion of "this task right here." **Implementation notes** add the router prerequisite, the card-as-layout-shell pattern, and the session-only collapse-state guidance. **Alternatives considered** records six rejected shapes (slug, preferences-driven navigation, card detail view, single create+edit modal, persisted collapse, right-click menu). **Out of scope** records persisted collapse state, in-app project deletion, and drag-to-trash zones. Proposal grows 222 → 288 lines. Status remains *draft* — the open questions on default lanes, "No Project" UX, and completion-timestamp visual treatment are unchanged. _Generated by [\`/do\`](https://github.com/srid/agency) on Claude Code (model \`claude-opus-4-7\`)._ --- docs/proposals/0001-task-management.md | 91 ++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/docs/proposals/0001-task-management.md b/docs/proposals/0001-task-management.md index 4c9d5364..412eb85f 100644 --- a/docs/proposals/0001-task-management.md +++ b/docs/proposals/0001-task-management.md @@ -47,6 +47,21 @@ Project └── Terminals (optionally tagged with taskId) ``` +### Navigation + +Two top-level routes: + +- `/` — the workspace (terminal canvas, exactly as today). +- `/projects/` — that project's kanban board. + +Browser back/forward and bookmarkable URLs come for free. Project navigation lives entirely in the URL — there is no `preferences.activeProject`. Right-panel state (Inspector vs. Code, collapsed/expanded) stays in preferences as today; that's pane-level chrome state, orthogonal to which project the user is looking at. + +Project addressing uses the existing `Project.id` (UUID) directly: `/projects/7f3e1d2a-…`. There is no separate URL-friendly slug field — a project rename only changes the displayed name, the URL stays stable, no bookmarks break. + +Mobile swipe navigation (per [#622](https://github.com/juspay/kolu/issues/622)) is preserved as a *gesture layer* that calls `navigate()` rather than a parallel navigation stack. Swipe-left, swipe-right, browser back/forward all push and pop the same route history. One stack, one source of truth. + +Switching projects from inside the workspace: command palette → `Projects` group → list of curated projects → selection navigates to `/projects/`. + ### Projects A user-curated, named, optionally path-bound container. @@ -64,6 +79,20 @@ type Project = { A reserved synthetic project — displayed as **"No Project"** — exists to hold *floating* tasks (tasks the user created without picking a project). It is identified by a single reserved UUID constant kept in code, not by its display name and not by `repoPath` being absent. A user who creates a real project and names it "No Project" does not collide with the synthetic one — identity is structural, not textual. +### Project lifecycle: creating, editing, archiving + +**Create.** Command palette → `New project` → opens `CreateProjectModal`: + +- *Name* (required, text input). +- *Repo path* (optional, text input). Pre-filled from the active terminal's `cwd` if any. Autocompletes from `recentRepos`. Leave blank for a topic-only project (e.g. *"Learn CRDTs"*). +- `[Create]` → adds the project, navigates to `/projects/`. + +**Edit.** Command palette → `Edit project` → pick from list → opens `EditProjectModal` pre-filled with the existing values. Same field set, different semantics around side-effects (renaming may trigger a board-file move when the path-resolution layer is aware of `name`). Create and edit are kept as **separate modal components** sharing only a stateless `` for the field layout — so their validation rules, side-effects, and future divergence don't tangle. + +**Archive.** Command palette → `Archive project`. Sets `archived: true` on the project record. Archived projects are hidden from the default `Projects` list but reachable via `Show archived projects`. The synthetic "No Project" cannot be archived. + +**Delete.** Out of scope for this proposal. If the user wants to permanently remove a project, they delete the project's directory under `~/.config/kolu/projects/` manually. Archive is the only in-app affordance. + ### Tasks Pure intent metadata. A task is a labeled bookmark of "what I was working on." @@ -90,25 +119,50 @@ User-defined per project. A lane is just a heading in the project's markdown fil A lane can carry a `complete` *role* (zero or one role today; the schema leaves room for more — see Out of scope). A lane with the `complete` role is treated as the "done" column: tasks landing there get a `completedAt` timestamp. -### Creating a task +### The project board (per project) -There is **one underlying operation** — `createTask(input)` — invoked through two affordances: +The primary view for working *inside* a project. Lanes laid out left-to-right; tasks shown as cards within. A card displays: -- **Per-lane "+ add task"** — an inline input at the bottom of each lane on the project board. Project and lane are inferred from context; the user types only a title. Fast path for "drop a thought into the right column." -- **Command palette → "New task"** — a modal asking project, lane, title, description. Lane defaults to the project's first lane; project defaults to the last-active project (or "No Project"). Slower, more deliberate path; works from anywhere, including with no project open. +- Title. +- Description preview (collapsed past ~3 lines — see [Card interactions](#card-interactions)). +- Linked terminal pills (if any terminals are tagged to this task). +- A check + completion timestamp when the lane carries the `complete` role. -Both affordances pre-fill different subsets of the same input shape. They share the write path. There is no `createTaskInline` and `createTaskModal` — only `createTask`. +The `+ add task` affordance lives at the bottom of each lane. -### The project board (per project) +### Card interactions + +A task card is a self-sufficient surface — there is no detail view, modal-on-click, or drawer. Everything important about a task is visible on the card; everything you can do *to* a task is done from the card directly. Five distinct affordances: + +| Surface | Action | +|---|---| +| Click + hold + drag the card body | Move the task between lanes | +| Click on the title text | Enter inline edit (text input replaces the title) | +| Click on the description text | Enter inline edit (textarea) | +| Click a terminal pill on the card | Navigate to `/` and focus that terminal | +| Click ✕ in the top-right corner | Open `DeleteTaskConfirm` modal → confirm to delete | + +Click on empty card area is a no-op. Inline edit commits on Enter (title) or blur (both); Escape cancels. + +**Long descriptions: collapse-by-heuristic.** When a task's description exceeds ~3 lines, the card starts collapsed with a `▸` chevron exposing only the title and meta. Click the chevron to expand to `▾` and reveal the description. The expand/collapse state is **session-only** — not persisted to markdown, not stored in preferences. On reload or project switch, every card returns to its heuristic default. Markdown stays free of UI chrome; preferences don't grow an unbounded `Record`. + +Terminal-pill click is the path the proposal owes a word about: clicking a pill navigates to `/` (workspace) and focuses the corresponding terminal. The browser back-button returns the user to the project board. This works because navigation is route-driven (see [Navigation](#navigation)) — no separate "go back to where I was" affordance needs to exist. + +### Task lifecycle: creating, editing, deleting + +**Create.** One underlying operation — `createTask(input)` — invoked through two affordances: + +- **Per-lane "+ add task"** — an inline input at the bottom of each lane. Project and lane are inferred from context; the user types only a title. Fast path for *"drop a thought into the right column."* +- **Command palette → `New task`** — opens `CreateTaskModal` asking project, lane, title, description. Lane defaults to the project's first lane; project defaults to the last-active project (or "No Project"). Slower, more deliberate path; works from anywhere, including with no project open. + +Both affordances pre-fill different subsets of the same input shape and share the write path. There is no `createTaskInline` and `createTaskModal` — only `createTask`. -The primary view for working *inside* a project. Lanes laid out left-to-right; tasks shown as cards within. Cards expose: +**Edit.** Two paths: -- Title -- Optional description preview -- Linked terminal pills (if any terminals are tagged to this task) -- A check + completion timestamp when the lane carries the `complete` role +- **Inline** — click the title or description on the card (see [Card interactions](#card-interactions)). The card stays in place; the field becomes editable. Primary path. +- **Command palette → `Edit task`** — list → pick → opens `EditTaskModal` pre-filled with current values. Useful when the card isn't visible (cross-project view, or you forgot which project a task lives in). -Cards are dragged between lanes to change status; the `+ add task` affordance lives at the bottom of each lane. +**Delete.** Click ✕ on the card → `DeleteTaskConfirm` modal (mirrors kolu's existing `CloseConfirm` pattern at `packages/client/src/CloseConfirm.tsx:61-69` for risky actions). The palette has no `Delete task` command — `Cmd+K` has no notion of *"this task right here"*, only the card does. Deletion is anchored to the card surface, not the palette. ### Cross-project view @@ -190,6 +244,10 @@ Optional pointers, not prescriptions: - Block-IDs (`^abc123`) are kolu-managed and must be invisible to the user — store them in HTML comments so hand-edits don't clobber them. - The path-resolution layer that maps `(projectId) → board.md path` should be small and replaceable. The default implementation returns `~/.config/kolu/projects//board.md`; a future per-project `boardPath` override (for git-versioned boards) plugs into the same seam. - The synthetic "No Project" container is identified by a single reserved UUID constant. Display rendering substitutes the label "No Project"; storage and code paths treat it like any other project. +- **Routing.** Adopting a SolidJS-compatible router (`@solidjs/router` or similar) is a structural prerequisite. Today kolu has no router (`packages/client/src/App.tsx` is mode-less per [#622](https://github.com/juspay/kolu/issues/622)) — this proposal explicitly revisits that stance. The implementer should treat routing as a workspace-wide change, not a feature-local one. +- **Task card is a layout shell, not a god-component.** Each affordance (drag, inline-edit-title, inline-edit-description, terminal-pill nav, delete ✕) should isolate into its own hook or sub-component (`useDragTaskCard`, ``, ``, ``, etc.). The card itself orchestrates layout; behavior lives in the parts. +- **Create and edit are separate modals.** `CreateProjectModal` / `EditProjectModal` (and the same shape for tasks) share a stateless `` / `` for field rendering, but each modal owns its own validation, side-effect, and lifecycle semantics. Avoid a single `` that branches internally. +- **Card expand/collapse state is session-only.** Hold it in a per-board UI-state hook (e.g. `useProjectBoardUIState()`) keyed by task id; never persist to markdown or preferences. The default value is computed from the description length. ## Alternatives considered @@ -202,6 +260,12 @@ Optional pointers, not prescriptions: - **`Project` and `recentRepos` unified into one store.** Rejected — auto-MRU and user curation have different change rates and serve different purposes (*"where was I?"* vs *"what am I working on?"*). They stay parallel. - **`Board` as a domain entity** distinct from `Project`. Rejected — every project has exactly one board. "Board" was a phantom concept; the project *is* its board. - **Persistent JSON index alongside the markdown.** Rejected — same data in two stores invites drift. The in-memory parsed representation is enough; the markdown file is canonical. +- **URL-friendly `slug` field on `Project` (e.g. `/projects/kolu`).** Rejected — slug-collisions force fallback to UUID, meaning UUID was always the identity and slug was a display label wearing identity clothes. Three failure modes vanish at once: rename-breaks-bookmarks, URL-drifts-from-name, slug-collision-fallback. URLs use `Project.id` directly; the display name lives in the page title and breadcrumb separately. +- **Project navigation tracked in `preferences.activeProject`.** Rejected — the URL is the only canonical answer to "which project am I on." A second store invites drift on reload, deep-link, and palette-driven selection. Preferences keeps orthogonal pane-chrome state (right-panel collapse, tab); navigation lives in the route. +- **Card detail view (modal/page/drawer opened on click).** Rejected — the card itself shows everything important, and inline edit covers field changes. Adding a detail view duplicates information already on the card and invents a new presentation pattern kolu doesn't have today. +- **Single `ProjectModal` (or `TaskModal`) handling create + edit via an `isEdit` flag.** Rejected — create and edit have different validation rules (name-uniqueness across all vs. all-but-self), different side-effects (rename-triggers-board-move), and will diverge further. Two modals sharing a stateless field component avoids the internal branching. +- **Persisting card expand/collapse state.** Rejected — markdown placement causes layout twitches on hand-edit; preferences placement creates an unbounded `Record` that leaks entries for deleted tasks. Session-only state with a "long descriptions start collapsed" heuristic answers the user need without storing anything. +- **Right-click context menu replacing the card affordances.** Rejected — kolu has no right-click context menu pattern today (the pill tree is read-only, terminal tiles use direct-click affordances). Inline-edit on click is well-precedented (Notion, Obsidian, Linear) and keeps the kanban interaction model close to obsidian-kanban's. ## Open questions @@ -219,3 +283,6 @@ These are deliberate exclusions: - **Programmatic mutation surface for user-invoked clients.** A way for the user to mutate tasks from outside the Kolu UI — via an MCP server, a CLI, an agent tool, a plugin host, or some other shape — is anticipated as a future addition. The form, trust model, and auth design are all undecided. Out of scope for this proposal; deserves its own. - **Replacing `recentRepos`.** That auto-MRU stays untouched. A user opening a recent repo without ever creating a Project for it should keep working exactly as today. - **Board format interop with non-Obsidian tools** beyond what falls out of mirroring obsidian-kanban's syntax. No adapters, no bidirectional sync to other task systems. +- **Persisting card expand/collapse state.** Heuristic (description length) determines the default; the user's toggle lives in session memory only. Reload is a clean slate by design. +- **Project deletion from inside Kolu.** Archive (via `archived: true`) is the in-app affordance. Permanent deletion is a manual filesystem operation under `~/.config/kolu/projects/`. Adding an in-app delete affordance with confirmation flows is a separate proposal. +- **Drag-to-trash zone for task deletion.** Considered, deferred. It's discoverable on mobile but eats real estate on the small fullscreen viewport. The card-corner ✕ works on both desktop and mobile; revisit drag-to-trash if usage data shows people prefer dragging. From 8da513f8fc5c7d41a5c6c379b02eaf76beb9ba90 Mon Sep 17 00:00:00 2001 From: Mani Chandra Dulam Date: Tue, 28 Apr 2026 16:00:02 +0530 Subject: [PATCH 3/5] =?UTF-8?q?docs(proposal):=200001=20mockup=20=E2=80=94?= =?UTF-8?q?=20show=20=E2=9C=95=20delete=20and=20collapse=20chevron?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Catches the visual prototype up to the prose. Card affordances added in afb590f (✕ delete in card top-right, ▸ collapse chevron for long descriptions) are now visible in the per-project board mockup. - Each card on the kanban view gains a small ✕ in the top-right (low opacity by default; full-strength on hover; turns red on focus). On click in the live UI this opens DeleteTaskConfirm. - One new card in the Todo lane ("Investigate dropped frames in scrollback") carries a long description and renders in collapsed state with a ▸ show more chevron — demonstrates the collapse-by-heuristic, session-only state. - The on-disk markdown panel grows to match (the new task appears as a `- [ ]` entry with the same long description). - Mini-cards in the Creation UI view (section 04) get the ✕ for consistency. - The "What's shown" legend explains both new affordances. The Prototype section of the proposal markdown gains a one-line note on view 1 calling out the ✕ and chevron. _Generated by [\`/do\`](https://github.com/srid/agency) on Claude Code (model \`claude-opus-4-7\`)._ --- docs/proposals/0001-task-management.md | 2 +- .../0001-task-management/mockup.html | 90 +++++++++++++++++-- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/docs/proposals/0001-task-management.md b/docs/proposals/0001-task-management.md index 412eb85f..7f41bdb8 100644 --- a/docs/proposals/0001-task-management.md +++ b/docs/proposals/0001-task-management.md @@ -231,7 +231,7 @@ Both populations are real today. The default covers (1); the implementation shou See [`./0001-task-management/mockup.html`](./0001-task-management/mockup.html) — open in a browser. Five views: -1. **Per-project board** — lanes, cards, terminal pills, completion check. +1. **Per-project board** — lanes, cards (with ✕ delete on hover, ▸ collapse chevron on long descriptions), terminal pills, completion check. 2. **What's on disk** *(explanation only)* — the raw markdown that produces view 1, rendered side-by-side. Not a proposed Kolu surface; included to make the file format concrete. 3. **Cross-project view** — time-ordered table. 4. **Terminal canvas with task tags** — existing canvas; one new title-bar affordance. diff --git a/docs/proposals/0001-task-management/mockup.html b/docs/proposals/0001-task-management/mockup.html index f0e70a22..3704f4e2 100644 --- a/docs/proposals/0001-task-management/mockup.html +++ b/docs/proposals/0001-task-management/mockup.html @@ -311,14 +311,66 @@ background: var(--surface-2); border: 1px solid var(--border-soft); border-radius: 6px; - padding: 10px 12px; + padding: 10px 28px 10px 12px; margin-bottom: 10px; cursor: grab; transition: border-color 120ms; + position: relative; } .card:hover { border-color: var(--border); } + .card:hover .card-x { + opacity: 1; + } + + .card-x { + position: absolute; + top: 5px; + right: 6px; + width: 18px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + color: var(--text-faint); + font-size: 13px; + line-height: 1; + cursor: pointer; + border-radius: 3px; + opacity: 0.55; + transition: color 100ms, background 100ms, opacity 100ms; + } + .card-x:hover { + color: var(--red); + background: var(--bg); + opacity: 1; + } + + .card-chevron { + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 8px; + padding: 3px 8px; + font-family: var(--mono); + font-size: 11.5px; + color: var(--text-faint); + background: var(--bg); + border: 1px solid var(--border-soft); + border-radius: 3px; + cursor: pointer; + } + .card-chevron:hover { + color: var(--text); + border-color: var(--border); + } + .card.collapsed .desc, + .card.collapsed .meta { + display: none; + } .card .title { font-size: 13.5px; @@ -853,19 +905,37 @@

Per-project board

Todo - 2 + 3
+
Refactor session module
Splits the auth and session storage modules.
+
Fix login bug
term-1 term-2
+
add task…
@@ -876,6 +946,7 @@

Per-project board

1
+
Learn CRDTs
Read paper; prototype a tiny LWW register; write up.
@@ -893,6 +964,7 @@

Per-project board

**Complete**
+
Set up dev env ✓ 2d ago @@ -919,6 +991,9 @@

Per-project board

- [ ] Refactor session module <!-- ^def456 --> Splits the auth and session storage modules. - [ ] Fix login bug <!-- ^abc123 --> +- [ ] Investigate dropped frames in scrollback <!-- ^mno345 --> + Long description spanning several lines about the WebGL texture atlas + growing unbounded in long-running terminals, with reproduction steps… ## Doing @@ -938,9 +1013,12 @@

Per-project board

What's shown (left, proposed): Lanes are user-named. The **Complete** badge marks a lane as carrying the complete role; tasks landing there get a completion timestamp. Term-pills under cards are linked terminals; clicking one - focuses that terminal on the canvas. The - + add task… at the bottom of each lane is one of the two creation affordances (see - view 04). + navigates to / (workspace) and focuses that terminal — browser back returns here. + Each card carries a small × in the top-right (visible on hover) that opens a + delete-confirm modal. The third Todo card has a long description and starts collapsed with a + ▸ show more chevron — clicking the chevron expands the card; the state is + session-only and not persisted. The + add task… at the bottom of each lane is one of + the two creation affordances (see view 04).
What's shown (right, explanation only): The raw markdown that produces the board @@ -1119,9 +1197,11 @@

Creating a task

2
+
Refactor session module
+
Fix login bug
From c79a54d646c2271734a3a201132724e3f5e5b90b Mon Sep 17 00:00:00 2001 From: Mani Chandra Dulam Date: Tue, 28 Apr 2026 16:28:11 +0530 Subject: [PATCH 4/5] =?UTF-8?q?docs(proposal):=200001=20=E2=80=94=20single?= =?UTF-8?q?=20updatedAt=20timestamp=20+=20/tasks=20route=20+=20schema/migr?= =?UTF-8?q?ations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds in the design decisions from a third lowy/hickey-reviewed discussion. The Task schema collapses; routes get a third entry; Implementation notes pick up a Schema and migrations note. **Single timestamp.** `Task` shrinks to `{ id, projectId, name, description?, updatedAt }`. `createdAt` and `completedAt` are gone as stored fields: - `createdAt` isn't recoverable from markdown after lane moves — we'd be lying about precision the source can't back up. - `completedAt` is a derivation: *if the task's current lane has the `complete` role, `updatedAt` is the completion time; otherwise the task isn't done.* Storing it independently invited drift. First-completion-immutable semantics are deliberately dropped — moving a task back through Done re-stamps `updatedAt` rather than preserving the original completion date. `updatedAt` is named "last meaningful interaction": bumped on lane move, title edit, or description edit. *Not* bumped on drag-to-reorder within the same lane (reordering is presentation, not content). **`/tasks` route.** Added as the third top-level route alongside `/` and `/projects/`. The cross-project view's URL is no longer implicit. **Storage.** The on-disk markdown sample now shows `@{YYYY-MM-DD}` on every task line (not just the Done ones). The annotation IS `updatedAt`. Conventions explain auto-heal on hand-edit deletion. **Schema and migrations.** New Implementation-notes bullet: parsed boards validated through Zod schemas (`TaskSchema`, `LaneSchema`, `BoardSchema`) following the discipline of `PersistedStateSchema` at packages/server/src/state.ts:34-39. `kolu-board: v1` is the version handle; bumping to v2 means a `migrateBoardFromV1ToV2` runner. Schema home (client vs. kolu-common) is left to the implementer following the `PersistedStateSchema` rule of own-where-owned. **Out of scope** picks up first-completion-immutable timestamps (deliberate drop, with the trade explained), cycle-time analytics, and per-task version fields. **Alternatives considered** records two more rejected shapes — stored `createdAt`/`completedAt` fields, and the `` triplet that an earlier round considered before the single-`updatedAt` model landed. **Mockup catches up.** Each frame's chrome gains a faux URL bar showing the route it lives at (`/projects/`, `/tasks`, `/`). Each kanban card carries an `@{YYYY-MM-DD}` chip. The on-disk markdown panel shows the annotation on every task. _Generated by [\`/do\`](https://github.com/srid/agency) on Claude Code (model \`claude-opus-4-7\`)._ --- docs/proposals/0001-task-management.md | 46 ++++--- .../0001-task-management/mockup.html | 116 +++++++++++++++--- 2 files changed, 132 insertions(+), 30 deletions(-) diff --git a/docs/proposals/0001-task-management.md b/docs/proposals/0001-task-management.md index 7f41bdb8..ecafeeb2 100644 --- a/docs/proposals/0001-task-management.md +++ b/docs/proposals/0001-task-management.md @@ -49,10 +49,11 @@ Project ### Navigation -Two top-level routes: +Three top-level routes: - `/` — the workspace (terminal canvas, exactly as today). - `/projects/` — that project's kanban board. +- `/tasks` — the cross-project view (time-ordered table over every task in every project; see [Cross-project view](#cross-project-view)). Browser back/forward and bookmarkable URLs come for free. Project navigation lives entirely in the URL — there is no `preferences.activeProject`. Right-panel state (Inspector vs. Code, collapsed/expanded) stays in preferences as today; that's pane-level chrome state, orthogonal to which project the user is looking at. @@ -103,21 +104,29 @@ type Task = { projectId: string; // mandatory; floating tasks live under the synthetic "No Project" name: string; // user-typed description?: string; // user-typed; freeform markdown - createdAt: number; - updatedAt: number; - completedAt?: number; // set on first entry into a `**Complete**` lane; immutable once set + updatedAt: number; // last meaningful interaction (see below) }; ``` -Notably **absent**: a `status` field. The lane a task lives in *is* its status. Carrying `status` on the task object would be a denormalization of lane membership and would require two writes for every state change. Status is computed at read time from "which lane currently contains this task." +Notably **absent**: -`completedAt` semantics: **first-completion, immutable**. Set once when a task first lands in a lane flagged as completion. Moving it back out does not clear it. Moving back in does not update it. Re-opening a "completed" task is a separate concept that this proposal deliberately does not model. +- A `status` field. The lane a task lives in *is* its status. Carrying `status` on the task object would denormalize lane membership and require two writes for every state change. Status is computed at read time from "which lane currently contains this task." +- A `createdAt` field. Once a task has moved between lanes a few times, "when did this first exist" is no longer recoverable from the markdown — and we'd rather not pretend otherwise. For a personal kanban, "when did I last interact with this" is the question that matters; "when did I first jot it down" is not. +- A `completedAt` field. Completion is purely a derived fact: *if the task's current lane has the `complete` role, the task is done at `updatedAt`; otherwise it isn't done.* Moving a task back out of Done and re-completing it re-stamps `updatedAt` — the original completion date is not preserved. See [Out of scope](#out-of-scope) for the deliberate trade. + +`updatedAt` semantics: **last meaningful interaction**. Bumped to `now` whenever: + +- The task moves between lanes (drag-to-different-lane). +- The title is edited inline. +- The description is edited inline. + +**Not** bumped on drag-to-reorder *within the same lane* — reordering is presentation, not content. The user expects "this task was last touched yesterday" to mean *the task's content or location changed*, not *I dragged it up two slots in the same column.* ### Lanes User-defined per project. A lane is just a heading in the project's markdown file; the lane name is whatever the user typed. Two projects can both have lanes named "Done", or one called "🔥 fires" and another "Reading" — the system imposes no vocabulary. -A lane can carry a `complete` *role* (zero or one role today; the schema leaves room for more — see Out of scope). A lane with the `complete` role is treated as the "done" column: tasks landing there get a `completedAt` timestamp. +A lane can carry a `complete` *role* (zero or one role today; the schema leaves room for more — see Out of scope). A lane with the `complete` role is treated as the "done" column: tasks living there are *currently completed* (with their completion time being the same `updatedAt` annotation every other lane carries). ### The project board (per project) @@ -195,19 +204,19 @@ projectId: 7f3e... ## Todo -- [ ] Fix login bug -- [ ] Refactor session module +- [ ] Fix login bug @{2026-04-20} +- [ ] Refactor session module @{2026-04-19} Splits the auth and session storage modules. ## Doing -- [ ] Learn CRDTs +- [ ] Learn CRDTs @{2026-04-22} ## Done **Complete** -- [x] Set up dev env @{2026-04-20} +- [x] Set up dev env @{2026-04-25} ``` Conventions: @@ -216,7 +225,7 @@ Conventions: - `**Complete**` immediately under a heading flags the lane with the `complete` role (matches obsidian-kanban exactly). - `- [ ]` / `- [x]` items are tasks. Checkbox state is decorative; lane membership is canonical. - `` HTML comments hold task block-ids. Hidden in rendered markdown so users don't accidentally clobber them while editing. -- `@{2026-04-20}` is a completion timestamp annotation written by Kolu when a task enters a `complete` lane. +- `@{YYYY-MM-DD}` on each task line is the task's `updatedAt` — the date of its last meaningful interaction (see [Tasks](#tasks) for what counts). Kolu writes this on every task, in every lane (not just `complete`-role lanes). Hand-edits in another editor that delete the annotation are auto-healed on the next Kolu save. The file is **plain markdown**. Kolu is the primary editor, but users can hand-edit in any tool. Kolu watches for filesystem changes and reparses on update. Last-write-wins conflict resolution; reasonable for a personal-use feature. @@ -229,12 +238,12 @@ Both populations are real today. The default covers (1); the implementation shou ## Prototype -See [`./0001-task-management/mockup.html`](./0001-task-management/mockup.html) — open in a browser. Five views: +See [`./0001-task-management/mockup.html`](./0001-task-management/mockup.html) — open in a browser. Each frame's chrome carries a faux URL bar showing the route it lives at. Five views: -1. **Per-project board** — lanes, cards (with ✕ delete on hover, ▸ collapse chevron on long descriptions), terminal pills, completion check. +1. **Per-project board** at `/projects/` — lanes, cards (with `@{YYYY-MM-DD}` chip, ✕ delete on hover, ▸ collapse chevron on long descriptions), terminal pills, completion check. 2. **What's on disk** *(explanation only)* — the raw markdown that produces view 1, rendered side-by-side. Not a proposed Kolu surface; included to make the file format concrete. -3. **Cross-project view** — time-ordered table. -4. **Terminal canvas with task tags** — existing canvas; one new title-bar affordance. +3. **Cross-project view** at `/tasks` — time-ordered table. +4. **Terminal canvas with task tags** at `/` — existing canvas; one new title-bar affordance. 5. **Creation UI** *(proposed)* — per-lane inline `+ add` and the command-palette `New task` modal. ## Implementation notes @@ -248,6 +257,7 @@ Optional pointers, not prescriptions: - **Task card is a layout shell, not a god-component.** Each affordance (drag, inline-edit-title, inline-edit-description, terminal-pill nav, delete ✕) should isolate into its own hook or sub-component (`useDragTaskCard`, ``, ``, ``, etc.). The card itself orchestrates layout; behavior lives in the parts. - **Create and edit are separate modals.** `CreateProjectModal` / `EditProjectModal` (and the same shape for tasks) share a stateless `` / `` for field rendering, but each modal owns its own validation, side-effect, and lifecycle semantics. Avoid a single `` that branches internally. - **Card expand/collapse state is session-only.** Hold it in a per-board UI-state hook (e.g. `useProjectBoardUIState()`) keyed by task id; never persist to markdown or preferences. The default value is computed from the description length. +- **Schema and migrations.** Parsed boards are validated through Zod schemas — `TaskSchema`, `LaneSchema`, `BoardSchema` — that mirror the discipline of the existing `PersistedStateSchema` at `packages/server/src/state.ts:34-39`. The `kolu-board: v1` frontmatter is the canonical version handle for the on-disk format; bumping to `v2` means writing a `migrateBoardFromV1ToV2` function that runs on parse, same migration-ladder pattern as `state.json`. The exact home for the schema files (client package vs. `kolu-common`) is left to the implementer — `PersistedStateSchema` lives where its data is owned, and these schemas should follow the same rule. ## Alternatives considered @@ -266,6 +276,8 @@ Optional pointers, not prescriptions: - **Single `ProjectModal` (or `TaskModal`) handling create + edit via an `isEdit` flag.** Rejected — create and edit have different validation rules (name-uniqueness across all vs. all-but-self), different side-effects (rename-triggers-board-move), and will diverge further. Two modals sharing a stateless field component avoids the internal branching. - **Persisting card expand/collapse state.** Rejected — markdown placement causes layout twitches on hand-edit; preferences placement creates an unbounded `Record` that leaks entries for deleted tasks. Session-only state with a "long descriptions start collapsed" heuristic answers the user need without storing anything. - **Right-click context menu replacing the card affordances.** Rejected — kolu has no right-click context menu pattern today (the pill tree is read-only, terminal tiles use direct-click affordances). Inline-edit on click is well-precedented (Notion, Obsidian, Linear) and keeps the kanban interaction model close to obsidian-kanban's. +- **Stored `createdAt` and `completedAt` fields on `Task`.** Rejected. `createdAt` isn't recoverable from the markdown after a few lane moves — the file is canonical, and we'd be lying about the precision if we stored a value the source couldn't back up. `completedAt` is a derivation from `(task, currentLane)` — *if the lane has the `complete` role, `updatedAt` is the completion time; otherwise it's not done.* Storing it as an independent field invites drift between the lane and the field, and "first-completion-immutable" semantics cost us the honesty that re-completing a task re-stamps the date. Single timestamp; lane-membership encodes the rest. +- **Hidden `c=` / `u=` / `d=` timestamps inside the block-id comment.** Considered — would have stored all three timestamps invisibly per task. Rejected once the single-`updatedAt` model landed, since two of the three storage slots had nothing to fill them with. The visible `@{YYYY-MM-DD}` annotation on each task line is the simpler outcome. ## Open questions @@ -286,3 +298,5 @@ These are deliberate exclusions: - **Persisting card expand/collapse state.** Heuristic (description length) determines the default; the user's toggle lives in session memory only. Reload is a clean slate by design. - **Project deletion from inside Kolu.** Archive (via `archived: true`) is the in-app affordance. Permanent deletion is a manual filesystem operation under `~/.config/kolu/projects/`. Adding an in-app delete affordance with confirmation flows is a separate proposal. - **Drag-to-trash zone for task deletion.** Considered, deferred. It's discoverable on mobile but eats real estate on the small fullscreen viewport. The card-corner ✕ works on both desktop and mobile; revisit drag-to-trash if usage data shows people prefer dragging. +- **First-completion-immutable timestamps and cycle-time analytics.** Moving a task out of Done and back in re-stamps `updatedAt`; the original completion date is not preserved. "How long did task X take?" / "what's our average cycle time?" / "completed-on history" are project-management questions and stay out of scope. The deliberate trade is honesty in the schema: storing a timestamp we'd silently overwrite would lie to users about what the field means. +- **Per-task version field.** The on-disk format carries `kolu-board: vN` once at the top of the file; individual tasks have no version of their own. Adding optional fields later is handled by the migration ladder, not by per-record versioning. diff --git a/docs/proposals/0001-task-management/mockup.html b/docs/proposals/0001-task-management/mockup.html index 3704f4e2..52df342f 100644 --- a/docs/proposals/0001-task-management/mockup.html +++ b/docs/proposals/0001-task-management/mockup.html @@ -184,6 +184,62 @@ color: var(--text-faint); } + .url-bar { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 14px; + background: #0a0c10; + border-bottom: 1px solid var(--border-soft); + font-family: var(--mono); + font-size: 12px; + color: var(--text-dim); + } + .url-bar .nav { + display: inline-flex; + gap: 4px; + color: var(--text-faint); + } + .url-bar .nav span { + cursor: pointer; + } + .url-bar .nav span.active { + color: var(--text); + } + .url-bar .lock { + font-size: 10px; + color: var(--green); + } + .url-bar .url { + flex: 1; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .url-bar .url .host { + color: var(--text-faint); + } + .url-bar .url .path { + color: var(--accent); + } + + .updated-chip { + display: inline-block; + margin-top: 6px; + padding: 1px 7px; + font-family: var(--mono); + font-size: 10.5px; + color: var(--text-faint); + background: var(--bg); + border: 1px solid var(--border-soft); + border-radius: 3px; + } + .card .meta + .updated-chip, + .card .desc + .updated-chip { + margin-top: 8px; + } + /* Split layout: kanban + on-disk panel ------------------------------- */ .split-body { display: grid; @@ -898,6 +954,12 @@

Per-project board

⌘K
+
+ + 🔒 + kolu.local/projects/7f3e1d2a-9b04-42c1-bc83-5e3d10d0b4a1 +
+
@@ -911,6 +973,7 @@

Per-project board

Refactor session module
Splits the auth and session storage modules.
+ @{2026-04-19}
@@ -919,6 +982,7 @@

Per-project board

term-1 term-2
+ @{2026-04-20}
▸ show more + @{2026-04-23}
add task…
@@ -952,6 +1017,7 @@

Per-project board

term-3
+ @{2026-04-22}
add task…
@@ -967,8 +1033,9 @@

Per-project board

Set up dev env - ✓ 2d ago +
+ @{2026-04-25}
add task…
@@ -988,16 +1055,16 @@

Per-project board

## Todo -- [ ] Refactor session module <!-- ^def456 --> +- [ ] Refactor session module <!-- ^def456 --> @{2026-04-19} Splits the auth and session storage modules. -- [ ] Fix login bug <!-- ^abc123 --> -- [ ] Investigate dropped frames in scrollback <!-- ^mno345 --> +- [ ] Fix login bug <!-- ^abc123 --> @{2026-04-20} +- [ ] Investigate dropped frames in scrollback <!-- ^mno345 --> @{2026-04-23} Long description spanning several lines about the WebGL texture atlas growing unbounded in long-running terminals, with reproduction steps… ## Doing -- [ ] Learn CRDTs <!-- ^ghi789 --> +- [ ] Learn CRDTs <!-- ^ghi789 --> @{2026-04-22} Read paper; prototype a tiny LWW register; write up. ## Done @@ -1010,15 +1077,18 @@

Per-project board

- What's shown (left, proposed): Lanes are user-named. The - **Complete** badge marks a lane as carrying the complete role; tasks - landing there get a completion timestamp. Term-pills under cards are linked terminals; clicking one - navigates to / (workspace) and focuses that terminal — browser back returns here. - Each card carries a small × in the top-right (visible on hover) that opens a - delete-confirm modal. The third Todo card has a long description and starts collapsed with a - ▸ show more chevron — clicking the chevron expands the card; the state is - session-only and not persisted. The + add task… at the bottom of each lane is one of - the two creation affordances (see view 04). + What's shown (left, proposed): The URL bar shows the route the board lives at — + /projects/<projectId>. Lanes are user-named. The **Complete** badge + marks a lane as carrying the complete role; tasks living there are *currently + completed*. Each card carries an @{YYYY-MM-DD} chip — that's the task's + updatedAt, bumped on lane move or inline edit (not on drag-to-reorder within the same + lane). Term-pills under cards are linked terminals; clicking one navigates to / + (workspace) and focuses that terminal — browser back returns here. Each card carries a small + × in the top-right (visible on hover) that opens a delete-confirm modal. The third + Todo card has a long description and starts collapsed with a ▸ show more chevron — + clicking the chevron expands the card; the state is session-only and not persisted. The + + add task… at the bottom of each lane is one of the two creation affordances (see + view 04).
What's shown (right, explanation only): The raw markdown that produces the board @@ -1045,6 +1115,12 @@

Cross-project view

+
+ + 🔒 + kolu.local/tasks +
+
Filter @@ -1124,6 +1200,12 @@

Terminal canvas with task tags

⌘K
+
+ + 🔒 + kolu.local/ +
+
@@ -1183,6 +1265,12 @@

Creating a task

+
+ + 🔒 + kolu.local/projects/7f3e1d2a-9b04-42c1-bc83-5e3d10d0b4a1 +
+
From 35effcf99399c86818bb93f264ef1ffa6d432729 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Thu, 30 Apr 2026 15:07:54 -0400 Subject: [PATCH 5/5] =?UTF-8?q?docs(proposal):=200001=20=E2=80=94=20refine?= =?UTF-8?q?ment:=20terminal=20task=20kanban=20(alt=20design)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sibling refinement under docs/proposals/0001-task-management/ proposing a smaller surface for the same user need: pill-tree-on-hover becomes a two-lane kanban (Backlog + Active) backed by one Task concept persisted in the JSON state file. Cuts the Project layer, the markdown substrate, and user-defined lanes; keeps "queued + live work in one surface" and the IO-not-semantics principle. --- .../refinement-terminal-kanban.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/proposals/0001-task-management/refinement-terminal-kanban.md diff --git a/docs/proposals/0001-task-management/refinement-terminal-kanban.md b/docs/proposals/0001-task-management/refinement-terminal-kanban.md new file mode 100644 index 00000000..0e7fe019 --- /dev/null +++ b/docs/proposals/0001-task-management/refinement-terminal-kanban.md @@ -0,0 +1,132 @@ +# Refinement — Terminal Task Kanban + +A simpler shape for the same user need, surfaced for discussion under the +parent proposal. Cuts the **Project** layer, the **markdown-on-disk** +substrate, and **user-defined lanes**; keeps the user motivation +("queued work + live work in one surface") and the core principle +("Kolu has agency over IO; Kolu has no agency over semantics"). + +## Summary + +Replace the pill-tree-on-hover with a kanban that shows live terminals +plus a backlog of yet-to-be-spawned ones. Two lanes — **Backlog** (a +queued task with no terminal yet) and **Active** (a task bound to a +live terminal, sub-grouped by agent state). Closing a terminal removes +its task. One concept (Task), no project layer, no markdown files. + +## What this changes vs. the parent proposal + +- **No Project layer.** Per-repo grouping derives from the bound + terminal's main repo. The repo sidebar's facets come from + `Set(tasks.map(t => t.mainRepoRoot))` — no `Project` records to + create, name, archive, or keep in sync with `recentRepos`. +- **No markdown substrate.** Tasks live in the same JSON state file + as everything else Kolu persists. There is no obsidian-kanban file + format, no per-project board, no filesystem watcher, no last-write-wins + merge layer. +- **Two derived lanes, not user-defined.** `terminalId` absent → + Backlog; present → Active. The kanban exists to replace the pill + tree, and the pill tree only ever surfaced live terminals — two + derived lanes match that scope without inventing vocabulary the + user has to maintain. +- **No "Done" lane.** Closing the terminal removes the task. Tasks are + ephemeral: queued intent, then a live terminal, then gone. +- **No new route.** The board *is* the pill tree, expanded on hover. + Same widget at two density levels — no `/projects/`, no + `/tasks` page, no router prerequisite. + +## User-facing behavior + +**Resting state.** Pill tree as today (compact strip of live terminals). + +**On hover.** Expands into a kanban with three regions: + +- **Repo sidebar** — `All` plus per-repo facets with task counts. Click + a repo to filter the lanes. +- **Backlog lane** — cards for queued tasks not yet spawned. `+ add` + row at the bottom takes a title, scoped to the selected repo (prompts + for one when `All`). +- **Active lane** — cards for tasks bound to a live terminal, + sub-grouped top to bottom: **⏸ Awaiting** (agent waiting on the + user) → **⏵ Working** (agent thinking or running tools, animated + dot) → **⌨ No agent** (terminal alive, no AI agent observed). The + user-eyes-needed group surfaces first. + +**Card actions.** + +- Backlog `+ worktree` → opens the existing new-worktree dialog + pre-filled with this task's repo and name. On dialog success the + spawned terminal binds to this task (no fresh task is auto-created). + This is the only "promote" path; the rest of the card is read-only + metadata. +- Backlog `+ add` → inline title input. +- Active click → focuses that terminal in the canvas. +- Title click → inline rename. +- ✕ → delete. Backlog only. An Active task cannot be deleted from + the kanban — the only way to remove it is to close its bound + terminal (the task disappears with the terminal). + +## Prototype + +``` +┌─ pill tree (default) ────────────────────────────────────────────┐ +│ ⚡ silver-video ⏸ stout-proof ⌨ vira │ +└──────────────────────────────────────────────────────────────────┘ + ↓ hover +┌─ kanban (all repos) ─────────────────────────────────────────────┐ +│ Repos │ Backlog Active │ +│ ────────── │ │ +│ ● All 12 │ ┌──────────┐ ⏸ Awaiting (1) │ +│ kolu 5 │ │ fix auth │ ┌──────────┐ │ +│ eman 4 │ │ kolu │ │ stout-… │ │ +│ vira 3 │ │+ worktree│ │ emanote │ │ +│ │ └──────────┘ │ ⚡ codex │ │ +│ ────────── │ ┌──────────┐ └──────────┘ │ +│ + new repo │ │ + add │ │ +│ │ └──────────┘ ⏵ Working (1) │ +│ │ ┌──────────┐ ●● pulse │ +│ │ │ silver-… │ │ +│ │ │ kolu │ │ +│ │ │ ⚡ claude │ │ +│ │ └──────────┘ │ +│ │ │ +│ │ ⌨ No agent (1) │ +│ │ ┌──────────┐ │ +│ │ │ vira │ │ +│ │ │ $ shell │ │ +│ │ └──────────┘ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +## What this gives up vs. the parent proposal + +Naming these explicitly so the trade is visible: + +- **Topic-only intent without a repo.** Parent supports *"Learn CRDTs"* + with no `repoPath`. This refinement does not — every task binds to a + real `mainRepoRoot` (a git repo on disk). A user who wants a topic- + only task must first create a git repo for it (`git init` somewhere) + and bind the task to that. `$HOME` is not a repo and is not a + workaround. +- **"Done" as a visible record.** Parent shows completed tasks in a + `complete`-role lane with timestamps. This refinement removes the + task on terminal close; there is no per-task history. +- **Per-project lane vocabulary.** Parent lets the user define + `🔥 fires`, `Reading`, `In review`, etc. This refinement has Backlog + + Active only; the user does not name lanes. +- **Cross-project view.** Parent's `/tasks` table aggregates across + projects. This refinement has the repo-sidebar facet on the same + surface but no separate route or table. +- **Markdown interop.** Parent's files are obsidian-kanban-readable. + This refinement keeps tasks inside the JSON state file. + +## Decisions + +- **Backlog tasks stay queued across restarts.** They are persisted + state; the server brings them back as Backlog cards on next launch. + No auto-promote. +- **An Active task cannot be deleted.** Removing it requires closing + the bound terminal; the task disappears with the terminal. Delete + (✕) is a Backlog-only affordance. +- **The kanban defaults to All repos.** The repo sidebar starts on + `All`; per-repo facets are an opt-in filter.