TaskCanvas: Visualizing task.md with Sanity
AnTaskCanvas This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange. AnTaskCanvas started with a fairly ordinary problem. I tried a lot of task management tools: Linear, Trello, GitHub P
AnTaskCanvas
This is a submission for the Sanity Challenge, Path Two: Vibe-Code Something Strange.
AnTaskCanvas started with a fairly ordinary problem.
I tried a lot of task management tools: Linear, Trello, GitHub Projects, Notion, Microsoft To Do, Google Tasks, and quite a few others. They all solve parts of the problem well, but in my case I always ended up with the same friction: planning and the actual repository lived in different places.
I would create tasks for one project, move on to another, then another one, and after a while that planning would be disconnected from its original context. I had to remember which workspace belonged to which repository, which notes belonged to which idea, and where I had written down that task I quickly captured so I would not forget it.
The end result was much less sophisticated than all those tools promised: a folder full of paper notes, random reminders, Google Keep, and scattered ideas.
The idea for AnTaskCanvas had actually been in my head for quite a while.
Originally, I imagined it as a VS Code extension: something that would let me open a project and see its tasks directly from the same place where I was writing code.
The problem was that I never really had the time to build all the infrastructure around it.
And with the rise of vibe coding, the problem became even more obvious to me.
You can build an application very quickly now, but you can also end up with bugs, half-finished screens, and decisions you made in a hurry and barely remember a week later. The faster you move, the easier it becomes to lose track of what you were doing and what was still pending.
The idea still made sense, but building it meant solving persistence, data modeling, users, state, and a whole infrastructure layer before getting to the part I actually cared about:
turning tasks into something visual without falling back to keeping everything locally β and quietly hoping my computer never decides to explode.
Then Sanity entered the picture.
Suddenly, a large part of that infrastructure no longer had to be the project itself. Sanity gave me a foundation for modeling and persisting the information without first having to build an API, a database, and all the infrastructure around it.
For me, that was hard to ignore.
The boring part already had a solid foundation.
What was left was the visual part.
And an idea I had been postponing for a long time could finally become something real.
The idea behind AnTaskCanvas is deliberately simple: each project can carry its own planning in a TASKS.md file, keep that file versionable and portable, and still turn it into something visual.
That file can be imported into the application, viewed and organized through a Canvas or Kanban view, and exported back to Markdown.
The application also tries to sanitize the file, because real TASKS.md files are rarely pristine. Tasks get written quickly, structures become inconsistent, and small formatting mistakes appear when the only thing that matters at that moment is not forgetting an idea.
What matters to me is not turning Markdown into another closed database.
It is keeping the planning directly connected to the project it belongs to.
Sanity sits just before that planning becomes part of the repository's official history.
An idea can appear at any time: during a commute, during a sleepless night, or while working on a completely different project. You do not necessarily want to turn it into a commit immediately. Sometimes you just want to keep it, see it in context, and decide later whether it deserves to make it into production.
That is where Sanity gets a second role.
GitHub preserves what we finally decided to build.
Sanity also preserves what we are still thinking about.
AnTaskCanvas uses that persistence to keep working state and project context even before everything has to become part of the Git history.
That creates a fairly natural separation:
Sanity is the living state of the project.
TASKS.md is its portable representation.
GitHub is its versioned history.
AnTaskCanvas sits between those three layers.
It is not trying to replace Linear, Trello, Notion, or GitHub Projects. It also does not require AI to manage tasks.
Its goal is much more specific: take something as flat and simple as a TASKS.md file and turn it into a visual, persistent, understandable workspace without breaking its relationship with the repository.
Because the problem I am trying to solve is not that we lack task managers.
It is that too often our ideas about a project live in one place, while the actual project lives somewhere else.
What I Built
I built AnTaskCanvas, an application that turns a TASKS.md file into a more visual way of working with the tasks inside a project.
The idea is simple: TASKS.md remains the portable source of truth, but instead of working only with a text file, AnTaskCanvas can import it, interpret it, and organize its tasks through Canvas and Kanban views.
The application also tries to sanitize the file during import.
In real projects, these documents are often written in a hurry, with inconsistent structures or small formatting errors. Before representing them visually, AnTaskCanvas analyzes and normalizes that information so it can work with it more predictably.
Once inside the application, tasks can be organized visually and persisted using Sanity.
Sanity stores the structured representation of the tasks as well as information that does not belong inside the Markdown itself, such as positions, dimensions, and the visual state of groups inside the Canvas.
When we want to return to the portable format, the planning can be exported back to TASKS.md.
The current main flow is:
TASKS.md
β
import
β
analyze / sanitize
β
task model
β
Canvas / Kanban
β
persist with Sanity
β
export TASKS.md
Direct GitHub integration β authentication, importing directly from a repository, and automatic synchronization β belongs to the next phase and is not part of the current demo.
AnTaskCanvas is mainly aimed at individual developers, indie hackers, and small teams working across multiple projects who want to keep planning close to the repository without turning it into yet another workspace that has to be remembered and maintained separately.
Demo
Live demo:
Screenshots:
Experience the primary interactive modes of AnTaskCanvas:
1. Infinite Spatial Canvas (tldraw v5)
Visual freeform canvas with custom
TaskCardnodes, section groups, and live Directed Acyclic Graph (DAG) dependency connections (blockedBy).
2. Interactive Kanban Board
High-density column workflow (
Todo,In Progress,Done,Blocked) with instant drag-and-drop state updates and inline editing.
3. Live Split Markdown Editor
Side-by-side CodeMirror Markdown editor with bi-directional AST synchronizationβtype in Markdown or edit visually with zero data loss.
4. Monorepo Document Explorer & Context-Aware Filters
Manage multi-scope task files (
frontend/TASKS.md,backend/TASKS.md) with filter dropdowns that reactively reconcile with active document tags.
The main demo flow shows:
real TASKS.md
β
import
β
normalization
β
Canvas
β
Kanban
β
editing
β
persistence
β
reload project
β
restore visual context
β
export TASKS.md
The goal of the demo is to show the real workflow, not just a collection of polished screens with controlled sample data.
Code
The source code for AnTaskCanvas is available on GitHub:
Repository:
https://github.com/anappwilos/AnCanvasTask_END
Branch:
main
The application is built with React, Vite, and TypeScript, using Tailwind CSS v4 for the interface and tldraw as the Canvas engine.
To work with TASKS.md, AnTaskCanvas uses a normalizer built on unified, remark-parse, and remark-stringify.
Its job is not simply to read Markdown.
It first interprets and normalizes the document into a structure that can be used by Canvas or Kanban, while still allowing the original Markdown workflow to remain intact.
At a high level, the flow looks like this:
TASKS.md
β
markdownNormalizer.ts
β
task model
β
Canvas / Kanban
β
sanityService.ts
β
Sanity
The Sanity integration is kept separate from the logic responsible for interpreting Markdown.
That way, the file, its visual representation, and its persistent state do not depend directly on one another.
Main stack
- React
- Vite
- TypeScript
- Tailwind CSS v4
- tldraw
- unified / remark
- Sanity
@sanity/client
Main pieces
src/utils/markdownNormalizer.ts
β TASKS.md parsing and normalization
src/services/sanityService.ts
β Sanity integration
src/services/workspaceService.ts
β workspace and persistence operations
src/sanity/schemas/
β Sanity content model
The main architectural decision was to keep three responsibilities separate:
TASKS.md
β portable content
AnTaskCanvas
β editing and visual representation
Sanity
β persistent structure and visual memory
This allows AnTaskCanvas to store information that only makes sense to the application β such as the position and dimensions of a card inside the Canvas β without polluting TASKS.md with metadata that would be meaningless when opening the file directly in an editor.
My Build Process
AnTaskCanvas did not start with all of this architecture already figured out.
The first idea was much smaller: open a TASKS.md file and stop looking at it as a flat list of text.
A large part of the project started through vibe coding.
At first I mainly worked with Google AI Studio / Gemini and Antigravity to get the early versions up quickly. As the application grew, I also started using agents such as Codex for more focused reviews, debugging, audits, and changes where I wanted to avoid overly aggressive edits.
That shift ended up teaching me a lot about how I wanted to use AI during the project.
01 β Make it visual first
I started with React, TypeScript, and tldraw.
The first version had little more than a top bar, an infinite canvas, and a few fake task cards.
Before thinking about persistence, synchronization, or GitHub, I wanted to answer one question:
Does it actually make sense to look at a TASKS.md this way?
Then I added a second view in Kanban format.
That is where the core of AnTaskCanvas started to appear:
the same set of tasks could be seen as Markdown, as a Canvas, or as a board.
02 β Turning TASKS.md into more than plain text
The next step was reading a real file.
I started with the most basic Markdown tasks:
- [ ] Pending task
- [x] Completed task
Then came headings:
## Backend
## Frontend
## Bugs
which became visual groups.
From there I started adding metadata:
ID
Priority
Blocked by
Priorities from P0 to P3 could be represented visually, and Blocked by made it possible to draw dependencies between tasks as connections inside the Canvas.
At that point it stopped being just a Markdown viewer.
The document was starting to behave like a structure.
03 β Editing without breaking the source of truth
This introduced one of the more important problems.
I did not want the Canvas to become another database completely disconnected from the original file.
If I changed a task visually, that change had to be able to return to Markdown.
So I added editing operations such as:
- changing a title;
- completing a task;
- changing priority;
- creating tasks;
- deleting tasks;
- moving tasks between groups.
Those changes needed to make their way back into the document.
The rule was simple:
TASKS.md had to remain portable and understandable outside AnTaskCanvas.
04 β Sanity started much smaller
The first Sanity integration was fairly limited.
The initial idea was to use it only for information that did not really belong inside TASKS.md:
- card positions;
- dimensions;
- visual groups;
- Canvas state;
- representation-specific configuration.
The content itself would still belong to Markdown.
But once persistence was working, Sanity started to occupy a fairly natural place inside the project.
There was information I did not yet want to turn into repository history, but I also did not want to lose:
- working state;
- visual context;
- positions;
- grouping;
- structured information around tasks.
The architecture started to split in a way I had not originally planned:
Sanity as living state.
TASKS.md as the portable representation.
Git as versioned history.
Sanity eventually grew into a deeper integration, with structured persistence, visual state, and an embedded Studio inside the application.
05 β Auto organize
Once groups, priorities, and dependencies existed, manually placing every task stopped being fun.
With five cards, it is manageable.
With fifty, not so much.
That is how Auto organize appeared.
The idea was to use a layout engine to automatically distribute Canvas elements and create a reasonable starting arrangement.
After that, the user could still move things manually.
This also revealed something that happened repeatedly throughout the project: a feature can be technically correct and still be annoying in real use.
We later had to adjust behaviors such as automatic centering and Canvas movement to avoid camera jumps and interactions that interrupted the workflow.
06 β Real Markdown is much messier than demo Markdown
Up to this point, the examples worked beautifully.
Then real files arrived.
Duplicate IDs.
Broken dependencies.
Invalid priorities.
Inconsistent indentation.
Unknown metadata.
Checkboxes written in slightly different ways.
And tasks written quickly because the only important thing at that moment was not forgetting them.
That ended up creating a part of the project that was not in the original plan at all: the safe Markdown normalizer.
Instead of silently modifying the document, AnTaskCanvas can analyze the file and generate a normalization proposal.
Before accepting the changes, the user can review a diff and decide what to apply.
The original stays untouched until the operation is confirmed.
It is probably one of the features that best represents how AnTaskCanvas evolved:
it exists because real usage broke assumptions that worked perfectly in the prototype.
07 β Real files
Then came the less exciting but necessary part:
- opening
.mdfiles; - drag and drop;
- importing;
- exporting;
- saving;
- detecting changes;
- handling different documents.
The application started to move away from being a demo built around controlled data and toward something capable of working with real documents.
Over time this also evolved into the idea of workspaces and multiple-document handling.
08 β AI-generated design became a problem
This is where the process changed quite a bit.
For a large part of the development I worked with Google AI Studio and Antigravity.
The models were very good at adding things.
That was exactly the problem.
Every request like:
improve the interface
could turn into another card, another button, another gradient, another shadow, another badge, or another section.
In some iterations I ended up with more decoration than actual improvement.
The application started to develop that very recognizable AI-generated interface look:
- too many cards;
- too much empty space;
- gradients;
- glassmorphism;
- shadows;
- huge buttons;
- excessive rounding;
- repeated information;
- landing-page aesthetics applied to a work tool.
The more I asked:
βMake it more modern.β
the worse it got.
The model interpreted βimproveβ as βadd.β
So I started doing the opposite:
remove.
Gradients, glassmorphism, shadows, and unnecessary decoration were stripped away.
Border radii were reduced, information density increased, and functional hierarchy became more important than ornament.
That process also changed my prompts.
I went from asking:
make it more modern
to defining concrete rules around density, typography, spacing, hierarchy, responsive behavior, accessibility, and especially when not to create anything new.
I also started adding documentation and rules for agents.
It was no longer enough to say what I wanted.
I had to explain what I did not want.
09 β Internationalization: when the prompt had to mature too
One of the later major phases was i18n.
I wanted developers to keep writing readable source text while message extraction happened automatically, without manually maintaining keys like:
t("tasks.actions.delete")
The goal was a compiler-first architecture.
By this point the prompt itself looked very different from the prompts I used at the beginning.
Instead of simply saying βadd translations,β I started by defining the architecture I wanted and the things the agent should not do.
One of the real prompts was:
Implement a modern, automatic, compiler-first internationalization architecture across the entire project.
Developers should work with readable source text.
I do not want manual semantic keys such as
t("tasks.delete")or manually maintained translation dictionaries.Before modifying code, detect the framework, bundler, TypeScript setup, existing i18n system, and component structure.
Do not replace a valid existing solution unless necessary.
Prioritize a small, typed solution with no external translation APIs at runtime.
The full prompt eventually covered automatic extraction, ID generation, pluralization, accessibility, regional formatting, CI, and even which strings should not be translated.
The implementation ended up using Lingui.
And, as often happens, the plan was simpler than the implementation.
At one point:
lingui extract
returned:
0 messages
even though the application was full of text.
I had to review configuration, macros, and versions.
After fixing the issue, extraction found:
193 messages
and the project compiled correctly again.
That episode captures fairly well how my use of AI changed:
less
βbuild this for meβ
and more
βwork inside these constraints and inspect what already exists first.β
10 β Bugs that do not appear in the nice screenshot
There were also plenty of less photogenic moments.
One deployed version ended up as a completely blank white screen.
I had to review dependencies, package management, Vite configuration, and runtime errors.
I eventually added a RootErrorBoundary so that a startup failure would no longer leave the user with nothing but a blank page.
There were also problems inside tldraw.
For example, an early attempt to prevent certain shapes from being created intercepted the process too early and broke the internal store.
The solution was not to add another abstraction.
It was to let tldraw complete its operation correctly and intervene afterward.
These are not the most attractive parts of the project.
But they probably explain the build process better than the polished screenshots do.
11 β What changed in the way I use AI
I started by using AI mostly to generate.
I ended up using it much more to:
- inspect;
- compare;
- look for regressions;
- apply small changes;
- validate;
- work within constraints.
One pattern I ended up reusing was:
Before modifying anything, inspect the project and understand how it currently works.
Do not rebuild the architecture.
Do not add unnecessary dependencies.
Do not modify components unrelated to the problem.
Identify the root cause first.
Then apply the smallest necessary change.
Verify the result with build, TypeScript, lint, and tests if available.
If something cannot be verified, say so explicitly.
inspect β understand β fix β verify
Do not add code βjust in case.β
It may sound obvious.
In practice, it produced better results than many huge prompts.
The prompts changed too.
At the beginning they mostly described what I wanted to appear.
By the end they mostly described the boundaries within which the agent was allowed to work.
I think that was one of the most interesting lessons from the project.
With vibe coding, generating code is often not the hardest part.
The hard part is making sure every new generation does not destroy the intention of the previous one.
Sanity Project Details
Project ID: or19faat
Dataset: production
API Version: 2024-03-01
Public Dataset URL:
https://or19faat.api.sanity.io/v2024-03-01/data/query/production?query=%2A%5B0...20%5D%7B_id%2C_type%7D
Sanity is the layer that allows AnTaskCanvas to go beyond simply turning a Markdown file into a visual interface.
The starting point is still TASKS.md: a flat, portable format that is easy to keep next to the project.
But Markdown has a fairly obvious limitation once you try to turn it into something visual.
It can describe a task, its status, or its subtasks, but it does not know that a card was placed at a particular position on the Canvas, inside a certain group, or with specific dimensions.
That is where Sanity comes in.
The model
AnTaskCanvas currently uses three main document types.
workspace
Represents the general context of a project.
It stores workspace information and relates the tasks that belong to it.
task
Represents the structured version of a task found in TASKS.md.
It can store information such as:
- title;
- description;
- status;
- priority;
- tags;
- subtasks;
- group or section;
- dependencies.
This means that a line of Markdown stops being only text and becomes structured content that the application can query and represent in different ways.
canvasVisualState
This is probably the most important piece for the AnTaskCanvas idea.
The content of a task and the way we choose to organize it visually are two different things.
That is why Canvas state is stored separately.
For each task, it can keep information such as:
taskId
x
y
width
height
and for each group:
groupTitle
x
y
width
height
isCollapsed
This makes it possible to move a card, resize it, or reorganize a group without artificially modifying the content of TASKS.md.
In other words:
Markdown stores what a task is.
Sanity can store how we had organized it visually.
Why separate task and canvasVisualState
I could have stored everything inside the task document itself.
I did not, because that would have mixed two different concepts.
A task can move many times without its content changing.
Likewise, I can edit the title, priority, or a subtask without wanting to modify its position inside the Canvas.
Keeping them separate allows each part to evolve for a different reason:
task
β changes when the content changes
canvasVisualState
β changes when the representation changes
It also keeps view-specific information out of the task model.
Kanban, for example, needs to know the state of a task.
It does not need to know its X/Y coordinates inside the Canvas.
From flat file to Canvas
The current flow can be summarized like this:
TASKS.md
β
parser + normalizer
β
structured task model
β
Canvas / Kanban
β
Sanity
Semantic changes made from Canvas or Kanban can be reflected back into the Markdown content, while Sanity stores the structured representation and the spatial state of the project.
That keeps the separation fairly clear:
TASKS.md
β portable and interoperable content
AnTaskCanvas
β editing and visual representation
Sanity
β structured persistence and visual state
Persistence between sessions
AnTaskCanvas combines Sanity with a small local cache layer.
When restoring the visual state of a project, the application tries to retrieve it from Sanity using its projectId.
It can also keep a local copy of the visual state as a fallback if Sanity is unavailable or the connection takes too long.
That means that when reopening a project, we can recover more than just its tasks.
We can also recover how the workspace had been organized.
That was an important part of the original problem: coming back to a project months later and finding more than a list of pending tasks.
I wanted to recover some of the visual context I had been working with as well.
What is persisted and what is not
Not everything needs to live in Sanity.
Sanity stores:
- workspaces;
- structured tasks;
- relationships between workspace and tasks;
- visual state of cards;
- group position and dimensions;
- expanded or collapsed state of groups.
On the other hand, temporary interface state such as the current selection, zoom level, camera position, active filters, temporary searches, or open modals does not need to become persistent data.
Those are momentary working states, not part of the project itself.
Recovering context, not just data
This distinction became more important than I expected.
When I return to an old project, I do not only want to know that ten tasks are still pending.
It also helps to remember how I had grouped them.
Which tasks were close together.
Which group I had left open.
Which part of the project I had mentally separated from the rest.
That kind of information is difficult to express inside Markdown without polluting the file with purely visual metadata.
Sanity lets AnTaskCanvas preserve that context without turning TASKS.md into a proprietary format.
Sanity Studio inside AnTaskCanvas
The project also integrates Sanity Studio inside the application itself.
This makes it possible to inspect and manage structured documents without completely leaving the AnTaskCanvas interface.
During development, this was particularly useful because I could inspect what the application was actually generating instead of relying only on what I could see on the Canvas.
No realtime yet
One of the things I simplified during the latest refactor was real-time synchronization.
The current version does not maintain a permanent subscription through client.listen().
Persistence happens explicitly when the user modifies or saves state.
That was a deliberate decision for this version.
AnTaskCanvas is currently aimed mainly at individual developers, so there was no need yet to introduce the complexity of multi-user collaboration, conflicts, or simultaneous change resolution.
That can come later if the product actually needs collaborative editing.
The relationship with TASKS.md
An important decision was not to use Sanity as a replacement for Markdown.
If AnTaskCanvas disappeared tomorrow, the file should still be useful.
TASKS.md does not need AnTaskCanvas in order to exist.
The user can export it and continue working with it from an editor, a repository, or any other Markdown-compatible tool.
Sanity adds persistence and structure.
But it does not try to trap the project's content inside the application.
A deliberate decision
I also deliberately avoided storing the entire TASKS.md file as one big text block inside Sanity.
That would have been easier.
But it would also have removed much of the benefit of structured content.
AnTaskCanvas separates:
content,
structure,
visual representation.
Markdown remains portable and readable outside the application.
Sanity stores the parts that actually benefit from being structured and persistent.
And the Canvas uses both to turn something fundamentally flat into a visual workspace.
The same content, multiple representations
The same task can appear in different views without being duplicated:
task
βββ Canvas
βββ Kanban
βββ TASKS.md
All three representations refer to the same task.
But each one needs different information.
Markdown needs content.
Kanban mainly needs status and grouping.
Canvas also needs position and dimensions.
The Sanity schema keeps those differences separate instead of forcing one structure to solve every use case at once.
In one sentence
If I had to summarize Sanity's role in AnTaskCanvas:
TASKS.md remembers the tasks. Sanity remembers the structured and visual context around them.
That is what allows AnTaskCanvas to be more than a Markdown viewer.
It becomes a workspace you can return to and continue from where you left off.
Agent Session
I do not have a public Agent Session associated with the project.
A large part of AnTaskCanvas was developed using Google AI Studio / Gemini and Antigravity, and later I used other agents for more focused review, debugging, and auditing tasks.
Instead of linking to a session that does not exist, I want to include one of the real prompts that best represents how my way of working with agents changed during the project.
At the beginning, my instructions were much more open-ended.
As AnTaskCanvas grew, I found that I got better results by defining the constraints first, the expected architecture, and especially what the agent should not do.
This was one of the prompts used while implementing the internationalization system:
Implement a modern, automatic, compiler-first internationalization architecture across the entire project.
Developers should work with readable source text.
I do not want manual semantic keys such as:
t("tasks.actions.delete")or manually maintained translation dictionaries.
Before modifying code:
- detect the framework;
- detect the bundler;
- detect TypeScript;
- detect the existing i18n system;
- detect the component structure;
- detect the build system.
Do not replace a valid existing solution unnecessarily.
The final solution must be:
- compiler-first;
- typed;
- automatically extracted;
- without external translation APIs at runtime;
- without manually created semantic keys;
- with as little architectural noise as possible.
The full prompt was considerably longer.
It eventually covered pluralization, interpolation, regional formats, accessibility, message extraction, CI, automatic ID generation, and which types of strings should not be translated.
The result did not work correctly on the first attempt either.
During implementation:
lingui extract
at one point found:
0 messages
The configuration and macro imports had to be reviewed before the issue was found.
After fixing it:
193 messages
were detected and the project compiled correctly again.
This part of the development summarizes my experience with vibe coding in AnTaskCanvas fairly well:
the prompt stopped being just a description of what I wanted to build and started becoming a set of boundaries within which the agent was allowed to work.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.


