Build Task Coach with JavaScript
Build an AI-powered to-do list with browser storage and Cloudflare Workers.
Introduction
30 Second Summary
A busy to-do list can show what needs doing. It cannot turn an intimidating task into a clear first move or choose what deserves your attention next.
In this project, you will build Task Coach, a JavaScript to-do list with an AI assistant. You will publish the complete app with Cloudflare Workers so anyone with the link can try it.
What You'll Build
At your public Task Coach link, you can add a workload before asking the AI coach to turn one task into a practical plan or choose your next move.
By the end of this project, you'll have:
- A persistent task list where you can add, complete, or delete work without losing it after a refresh.
- An AI task coach that breaks down a difficult task or recommends one clear priority.
- A public Task Coach link ready to demo from start to finish.
- Secret Mission: Redesign the coaching prompt so vague tasks plus specific tasks receive consistent practical guidance.
Are there any prerequisites?
You'll need a Mac with Node.js plus Visual Studio Code already installed. You'll also need a Cloudflare account.
Before We Start
First, lock in who Task Coach supports and which decisions its AI assistant helps them make. This purpose guides every feature you build later.
Set Up the Task Coach Project
Your Task Coach idea now needs a clear home for its browser files and server code. A small file structure keeps each part easy to find as the app grows.
You will use Visual Studio Code to manage the project. Wrangler provides the local runtime that serves your first page today and supports deployment later.
In this step, get ready to:
- Create the Task Coach folders and files.
- Configure the local Wrangler project.
- Run the placeholder Task Coach page in Safari.
Create the project structure
A project structure gives every file one clear responsibility. The browser files belong in public. The Worker entry point belongs in src.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
Visual Studio Code opens with its editor and Explorer ready for a workspace.
- Click File in the top menu bar.
- Select Open Folder....
- Select Desktop in the folder dialog.
- Select New Folder.
- Enter task-coach as the folder name.
- Press Enter to create the folder.
- Select the new task-coach folder.
- Select Open.
Visual Studio Code may show a Workspace Trust dialog because files inside an opened folder can run code.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
The task-coach folder now appears at the top of the Explorer sidebar.
The Explorer controls are small, so creating the nested folders can feel fiddly. Select the parent folder before using its file or folder control.
- Select task-coach in the Explorer sidebar.
- Select New Folder... from the Explorer controls.
- Enter public as the folder name.
- Press Enter to create the folder.
- Select task-coach in the Explorer sidebar.
- Select New Folder... from the Explorer controls.
- Enter src as the folder name.
- Press Enter to create the folder.
You should now see public and src nested inside task-coach.
- Select task-coach in the Explorer sidebar.
- Select New File... from the Explorer controls.
- Enter package.json as the file name.
- Press Enter to create the file.
- Select task-coach in the Explorer sidebar.
- Select New File... from the Explorer controls.
- Enter wrangler.jsonc as the file name.
- Press Enter to create the file.
The two project configuration files now appear above the folders in the Explorer sidebar.
- Select public in the Explorer sidebar.
- Select New File... from the Explorer controls.
- Enter index.html as the file name.
- Press Enter to create the file.
- Select public in the Explorer sidebar.
- Select New File... from the Explorer controls.
- Enter style.css as the file name.
- Press Enter to create the file.
The public folder now contains the page and its empty stylesheet.
- Select public in the Explorer sidebar.
- Select New File... from the Explorer controls.
- Enter app.js as the file name.
- Press Enter to create the file.
- Select src in the Explorer sidebar.
- Select New File... from the Explorer controls.
- Enter index.js as the file name.
- Press Enter to create the file.
Your Explorer now shows six files across the task-coach workspace. Each file is ready for one part of the app.
Missing a file or folder?
- Expand public in the Explorer sidebar to reveal its three files.
- Expand src in the Explorer sidebar to reveal index.js.
- Check each file name for an extra extension or a spelling difference.
Need another pair of eyes? Help me check my Task Coach file structure.
Configure the local project
The package.json file describes the local project. Its scripts give you short commands for development and deployment.
- Select package.json in the Explorer sidebar.
- Add the project scripts and pinned dependency by pasting this code:
{"name":"task-coach","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"wrangler dev","deploy":"wrangler deploy","delete":"wrangler delete"},"devDependencies":{"wrangler":"4.152.0"}}
What does this configuration do?
- The project name matches the task-coach folder.
- The scripts provide commands for local development and deployment.
- The development dependency pins Wrangler to version 4.152.0.
- Press Cmd+S on macOS or Ctrl+S on Windows to save package.json.
- Confirm the package.json editor tab no longer shows an unsaved-change indicator.
Is package.json showing a problem?
- Check that the file starts with { and ends with }.
- Confirm the Wrangler version is written as 4.152.0.
Still stuck? Help me find the JSON problem in package.json.
The wrangler.jsonc file tells Wrangler where the Worker entry point lives. It also identifies the folder containing browser assets.
- Select wrangler.jsonc in the Explorer sidebar.
- Add the Worker configuration by pasting this code:
{"$schema":"node_modules/wrangler/config-schema.json","name":"task-coach","main":"src/index.js","compatibility_date":"2026-10-25","assets":{"directory":"./public"}}
What does this configuration do?
- The main value points Wrangler to src/index.js.
- The compatibility_date value selects the Worker runtime behavior for 2026-10-25.
- The assets configuration serves files from public.
- Press Cmd+S on macOS or Ctrl+S on Windows to save wrangler.jsonc.
- Confirm the wrangler.jsonc editor tab no longer shows an unsaved-change indicator.
Is Wrangler configuration showing a problem?
- Confirm src/index.js exists at the path named by main.
- Confirm the static directory is written as ./public.
Need help comparing the paths? Help me check my wrangler.jsonc file.
The first browser page only needs a heading. Its stylesheet and script links prepare the page for the interface you build next.
- Select index.html inside the public folder.
- Add the placeholder page by pasting this code:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Task Coach</title>
<link rel="stylesheet" href="/style.css" />
<script src="/app.js" defer></script>
</head>
<body>
<h1>Task Coach</h1>
</body>
</html>
What does this page do?
- The document title names the browser tab Task Coach.
- The stylesheet link points to public/style.css.
- The deferred script points to public/app.js.
- The heading gives the local server a visible result to serve.
- Press Cmd+S on macOS or Ctrl+S on Windows to save public/index.html.
- Confirm the public/index.html editor tab no longer shows an unsaved-change indicator.
Is the HTML showing a problem?
- Confirm every opening HTML tag has its matching closing tag.
- Check that both asset paths begin with /.
Need help spotting the mismatch? Help me compare my placeholder HTML.
✔️ Awesome, I've got everything!
Great. Double-check that you saved all three configured files.
ⓧ I'd like to double check the full code
Compare each configured file with these checkpoint versions.
{"name":"task-coach","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"wrangler dev","deploy":"wrangler deploy","delete":"wrangler delete"},"devDependencies":{"wrangler":"4.152.0"}}
{"$schema":"node_modules/wrangler/config-schema.json","name":"task-coach","main":"src/index.js","compatibility_date":"2026-10-25","assets":{"directory":"./public"}}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Task Coach</title>
<link rel="stylesheet" href="/style.css" />
<script src="/app.js" defer></script>
</head>
<body>
<h1>Task Coach</h1>
</body>
</html>
The files public/style.css, public/app.js, and src/index.js are empty at this checkpoint.
Install Wrangler and run the page
Your configuration names Wrangler as a local development dependency. npm reads that configuration and installs the pinned version inside this project.
The integrated terminal starts inside the open task-coach workspace. This keeps the installation attached to the correct project.
- Click View in the Visual Studio Code menu bar.
- Select Terminal to open the integrated terminal.
- Install the project dependencies by running this command:
npm install
What does this command do?
- npm reads the dependencies listed in package.json.
- It installs Wrangler inside the current project.
- It records the resolved dependency state in package-lock.json.
- Confirm node_modules appears in the Explorer sidebar.
- Confirm package-lock.json appears in the Explorer sidebar.
That installation is complete. Your project now carries its own copy of Wrangler 4.152.0.
Did the installation fail?
- Confirm the terminal path ends with task-coach.
- Check that package.json contains the pinned Wrangler dependency.
- Confirm your internet connection is active before retrying the installation.
Help me diagnose this installation. Ask the NextWork community.
Before you check, do you expect the installed Wrangler version to match the value in package.json?
- Check the local Wrangler version by running this command:
npx wrangler --version
What does this command do?
The command asks the locally installed Wrangler package to print its version. This confirms that the project uses the pinned dependency.
✔️ I see version 4.152.0
Wrangler matches the project pin. Your local runtime is ready to start.
ⓧ I see an older version
An older result means the installed dependency does not match the current project configuration.
- Return to package.json in the Explorer sidebar.
- Confirm the Wrangler value is 4.152.0.
- Switch back to the integrated terminal.
- Reinstall the project dependencies by running this command:
npm install
What does this command do?
npm reads the corrected dependency pin and refreshes the local installation.
- Check the refreshed Wrangler version by running this command:
npx wrangler --version
What does this command do?
The command prints the version from the refreshed local installation.
You should now see Wrangler version 4.152.0.
ⓧ Command not found
This result means the local dependency is unavailable from the current terminal location.
- Confirm the terminal path ends with task-coach.
- Install the local dependency by running this command:
npm install
What does this command do?
npm installs the Wrangler dependency declared by this project.
- Check the installed Wrangler version by running this command:
npx wrangler --version
What does this command do?
The command prints the version from the new local installation.
You should now see Wrangler version 4.152.0.
Wrangler development mode serves the files inside public. The process stays active so browser requests can keep reaching your local project.
Before you start the server, do you expect Wrangler to find the placeholder page through the configured asset directory?
- Start Wrangler development mode by running this command:
npm run dev
What does this command do?
- npm runs the dev script from package.json.
- Wrangler starts a local development server.
- The server exposes the static files from public at the printed local URL.
Wrangler prints a local URL in the terminal and keeps the process running. Leave this terminal active while you test the page.
The terminal can look busy here. The local URL sits in the latest Wrangler output.
Before you load the URL, what heading do you expect Safari to show?
- Select the printed local URL in the terminal.
- Press Cmd+C to copy the URL.
- Press Cmd+Space to open Spotlight.
- Type Safari into Spotlight.
- Press Enter to open Safari.
- Press Cmd+L to focus the address bar.
- Press Cmd+V to paste the local URL.
- Press Enter to load the page.
You will see a page with the heading Task Coach. That first local page proves Wrangler is serving the public directory.
Can't see the Task Coach heading?
- Confirm the Wrangler process is still running in the integrated terminal.
- Use the most recent local URL printed by Wrangler.
- Compare public/index.html with the checkpoint code above.
- Confirm wrangler.jsonc points to ./public.
Still blocked? Help me diagnose why Wrangler is not serving my Task Coach page.
Your Task Coach workspace now runs through its local development server. Next, you will turn the placeholder into an interactive to-do list.
Build a To-Do List That Forgets
The placeholder page proved that Wrangler can serve your local project. The same development session now gives you a fast feedback loop in Safari.
A useful task list needs clear controls plus working interactions. This step builds the first usable version with JavaScript state held in the current page.
In this step, get ready to:
- Build the complete Task Coach page structure.
- Style the responsive task list plus assistant panel.
- Add tasks plus complete and delete interactions.
Build the page structure
The HTML gives every visible control a stable place on the page. Its element IDs give your script precise targets for each interaction.
- In the Visual Studio Code Explorer sidebar from earlier, select public/index.html.
- Find the viewport metadata inside the head element.
- Add the page description directly below the viewport metadata by pasting this code:
<meta
name="description"
content="A persistent to-do list with an AI task coach."
/>
What Does This Metadata Do?
- The description gives browsers a concise summary of Task Coach.
- The content describes the completed experience that this project builds toward.
- Save public/index.html.
- Return to Safari to confirm the Task Coach title still loads.
You should still see the placeholder heading. The metadata now describes the page without changing its visible content.
Page No Longer Loading?
Check that the new metadata remains inside the head element. Confirm that each metadata tag ends with />.
Make sure Wrangler is still running in the integrated terminal. Help me troubleshoot why my Task Coach page stopped loading after I added its description metadata.
- Find the existing <h1>Task Coach</h1> line inside the body element.
- Replace that line with the hero by pasting this code:
<header class="hero">
<p class="eyebrow">AI-powered planning</p>
<h1>Task Coach</h1>
<p>Capture your work, then ask for one clear next move.</p>
</header>
What Does the Hero Do?
- The hero introduces the app before its interactive controls.
- The eyebrow labels the planning theme.
- The final sentence explains the result that Task Coach supports.
- Save public/index.html.
- Return to Safari to check the updated introduction.
You should see the Task Coach heading beneath an AI-powered planning label. A short planning message should appear below the heading.
Still Seeing Only the Old Heading?
Confirm that you replaced the old heading inside public/index.html. Check that the opening and closing header tags surround all three lines.
Save the file before returning to Safari. Help me troubleshoot why Safari still shows the old Task Coach heading.
The task card groups the form with the current list state. Its heading also gives the app a place to show how much work remains.
- Place the cursor directly below the closing header tag.
- Add the task card shell by pasting this code:
<main class="layout">
<section class="card tasks-card" aria-labelledby="tasks-heading">
<div class="section-heading">
<div>
<p class="eyebrow">Your list</p>
<h2 id="tasks-heading">Tasks</h2>
</div>
<p id="task-count" class="task-count">0 tasks left</p>
</div>
</section>
</main>
What Does the Task Card Shell Do?
- The layout class creates the container for both app panels.
- The task heading labels the left panel.
- The task-count element gives JavaScript a place to show the incomplete total.
- Save public/index.html.
- Return to Safari to check the task card shell.
You should see a Your list label above the Tasks heading. The count should read zero tasks left.
Task Heading Missing?
Check that the new main element sits after the closing hero tag. Confirm that its closing tag remains above the closing body tag.
Match each opening tag with its closing tag. Help me find a misplaced tag in my Task Coach layout.
The form captures new task text. The empty state explains the list before the first task exists.
- Place the cursor below the closing section-heading div.
- Add the task form plus list by pasting this code:
<form id="task-form" class="task-form">
<label class="sr-only" for="task-input">New task</label>
<input
id="task-input"
name="task"
type="text"
maxlength="120"
placeholder="What needs to get done?"
autocomplete="off"
required
/>
<button type="submit">Add task</button>
</form>
<p id="empty-state" class="empty-state">
Your list is empty. Add a task to get started.
</p>
<ul id="task-list" class="task-list" aria-live="polite"></ul>
What Does the Task Form Do?
- The input accepts a required task with a maximum length of 120 characters.
- The form groups the input with its submission button.
- The empty-state message explains why no task rows are visible.
- The task list announces updates through its live region.
- Save public/index.html.
- Return to Safari to check the task controls.
You should see a task input beside an Add task button. The empty-list message should appear directly below them.
Form Controls Not Appearing?
Confirm that the form sits inside the task section. Check that the input remains inside the opening and closing form tags.
Look for a missing angle bracket near the input attributes. Help me diagnose why the Task Coach form is missing.
The assistant card reserves space for future coaching actions. Its shared button will eventually choose a priority from the incomplete tasks.
- Place the cursor after the closing task section tag.
- Add the assistant card before the closing main tag by pasting this code:
<aside class="card coach-card" aria-labelledby="coach-heading">
<p class="eyebrow">AI assistant</p>
<h2 id="coach-heading">Task Coach</h2>
<p class="coach-intro">
Break down a difficult task or ask the coach to choose your next move.
</p>
<button id="recommend-button" class="secondary-button" type="button">
What should I do next?
</button>
<div id="assistant-output" class="assistant-output" aria-live="polite">
Add a task, then ask the coach for help.
</div>
</aside>
What Does the Assistant Card Do?
- The aside separates coaching output from task management.
- The recommendation button provides one shared action for the incomplete list.
- The assistant output gives future responses a dedicated live region.
- Save public/index.html.
- Return to Safari to check both page panels.
You should see the task controls first. The Task Coach assistant content should appear below them before styling.
Assistant Card in the Wrong Place?
Confirm that the aside sits inside the main element. It should come after the task section.
Check the spelling of the recommend-button and assistant-output IDs. Help me compare my Task Coach page structure with the expected layout.
✔️ Awesome, I've got everything!
Your complete page structure is ready. Double-check that you saved public/index.html.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="A persistent to-do list with an AI task coach."
/>
<title>Task Coach</title>
<link rel="stylesheet" href="/style.css" />
<script src="/app.js" defer></script>
</head>
<body>
<header class="hero">
<p class="eyebrow">AI-powered planning</p>
<h1>Task Coach</h1>
<p>Capture your work, then ask for one clear next move.</p>
</header>
<main class="layout">
<section class="card tasks-card" aria-labelledby="tasks-heading">
<div class="section-heading">
<div>
<p class="eyebrow">Your list</p>
<h2 id="tasks-heading">Tasks</h2>
</div>
<p id="task-count" class="task-count">0 tasks left</p>
</div>
<form id="task-form" class="task-form">
<label class="sr-only" for="task-input">New task</label>
<input
id="task-input"
name="task"
type="text"
maxlength="120"
placeholder="What needs to get done?"
autocomplete="off"
required
/>
<button type="submit">Add task</button>
</form>
<p id="empty-state" class="empty-state">
Your list is empty. Add a task to get started.
</p>
<ul id="task-list" class="task-list" aria-live="polite"></ul>
</section>
<aside class="card coach-card" aria-labelledby="coach-heading">
<p class="eyebrow">AI assistant</p>
<h2 id="coach-heading">Task Coach</h2>
<p class="coach-intro">
Break down a difficult task or ask the coach to choose your next move.
</p>
<button id="recommend-button" class="secondary-button" type="button">
What should I do next?
</button>
<div id="assistant-output" class="assistant-output" aria-live="polite">
Add a task, then ask the coach for help.
</div>
</aside>
</main>
</body>
</html>
Style the responsive interface
The CSS turns the page structure into a two-panel workspace. Each visual layer gives you an immediate result to check in Safari.
- Select public/style.css in the Explorer sidebar.
- Add the page theme plus base layout by pasting this code:
:root {
color-scheme: light;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
color: #172033;
background: #f4f7fb;
font-synthesis: none;
}
* {
box-sizing: border-box;
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
background:
radial-gradient(circle at top left, #dce8ff 0, transparent 32rem),
#f4f7fb;
}
What Do the Base Styles Do?
- The root rule sets the page colors plus its font stack.
- The universal rule keeps element dimensions predictable.
- The body rule removes the default margin plus paints the pale background.
- Save public/style.css.
- Return to Safari to check the page background.
You should see a pale blue background with a soft gradient in the top-left corner. The page text should use a dark blue color.
Background Still Plain White?
Confirm that the file is named style.css inside public. Check that public/index.html still links to /style.css.
Look for a missing closing brace in the body rule. Help me troubleshoot why the Task Coach base styles are not loading.
- Place the cursor after the closing body rule.
- Add the shared controls plus hero sizing by pasting this code:
button,
input {
font: inherit;
}
button {
border: 0;
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.hero {
width: min(1100px, calc(100% - 2rem));
margin: 0 auto;
padding: 3.5rem 0 2rem;
}
.hero h1,
.section-heading h2,
.coach-card h2 {
margin: 0;
}
.hero h1 {
margin-bottom: 0.5rem;
font-size: clamp(2.4rem, 7vw, 4.5rem);
letter-spacing: -0.06em;
}
What Do the Hero Styles Do?
- The controls inherit the same font as the page.
- Disabled buttons become visually muted.
- The hero gains a centered width plus a responsive heading size.
- Save public/style.css.
- Return to Safari to check the hero.
You should see a large Task Coach title inside a centered hero area. The introduction should have more space around it.
Hero Title Still Small?
Check that the selector reads .hero h1. Confirm that the heading remains inside an element with class="hero".
Look for a missing period before hero. Help me diagnose why the Task Coach hero styles are not applying.
- Place the cursor after the hero heading rule.
- Add the supporting text plus desktop grid by pasting this code:
.hero > p:last-child,
.coach-intro {
color: #596579;
}
.eyebrow {
margin: 0 0 0.45rem;
color: #3b63da;
font-size: 0.76rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(280px, 0.75fr);
gap: 1.25rem;
width: min(1100px, calc(100% - 2rem));
margin: 0 auto;
padding-bottom: 3rem;
}
What Does the Grid Control?
- The eyebrow rule turns each small label into a consistent section marker.
- The layout rule places the task card beside the narrower coach card.
- The shared width aligns both panels with the hero.
- Save public/style.css.
- Return to Safari to check the desktop columns.
You should see the task area on the left. The assistant area should appear in a narrower column on the right.
Panels Still Stacked on a Wide Screen?
Confirm that both panels sit inside the element with class="layout". Check the spelling of grid-template-columns.
Widen the Safari window before checking again. Help me troubleshoot why the Task Coach layout is not forming two columns.
- Place the cursor after the layout rule.
- Style the cards plus task heading by pasting this code:
.card {
border: 1px solid #dce3ef;
border-radius: 1.25rem;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 18px 45px rgba(32, 50, 84, 0.09);
}
.tasks-card,
.coach-card {
padding: 1.35rem;
}
.section-heading {
display: flex;
align-items: end;
justify-content: space-between;
gap: 1rem;
}
.task-count {
margin: 0;
color: #68758a;
font-size: 0.9rem;
}
What Do the Card Styles Do?
- The card rule gives both panels the same border plus white surface.
- The shared padding keeps content away from each card edge.
- The section heading places the task count opposite the Tasks heading.
- Save public/style.css.
- Return to Safari to check both card surfaces.
You should see two rounded white cards with subtle shadows. The task count should sit at the right side of the left card heading.
Cards Missing Their Surfaces?
Check that both panels include the card class in public/index.html. Confirm that the CSS selector begins with a period.
Look for an unclosed rule above .card. Help me find why the Task Coach card styles are being ignored.
- Place the cursor after the task count rule.
- Style the task form plus input focus state by pasting this code:
.task-form {
display: grid;
grid-template-columns: 1fr auto;
gap: 0.7rem;
margin: 1.35rem 0;
}
.task-form input {
min-width: 0;
border: 1px solid #cbd5e5;
border-radius: 0.8rem;
padding: 0.85rem 0.95rem;
color: #172033;
background: #ffffff;
}
.task-form input:focus {
border-color: #4e6ee6;
outline: 3px solid rgba(78, 110, 230, 0.17);
}
What Do the Form Styles Do?
- The form grid gives the input the available width.
- The input gains a rounded border plus comfortable padding.
- The focus outline makes the active field easy to identify.
- Save public/style.css.
- Select the task input in Safari.
You should see the input beside the Add task button. The input should gain a blue outline when selected.
Input Still Looks Unstyled?
Confirm that the form uses class="task-form". Check that the input remains nested inside that form.
Make sure the selector contains a space between .task-form and input. Help me troubleshoot the Task Coach input styles.
- Place the cursor after the input focus rule.
- Style the main buttons plus empty state by pasting this code:
.task-form button,
.secondary-button {
border-radius: 0.8rem;
padding: 0.85rem 1rem;
color: #ffffff;
background: #3457d5;
font-weight: 750;
}
.task-form button:hover,
.secondary-button:hover {
background: #2745b2;
}
.empty-state {
margin: 0;
border: 1px dashed #c8d2e2;
border-radius: 0.9rem;
padding: 1.25rem;
color: #68758a;
text-align: center;
}
What Do These Control Styles Do?
- The main buttons share a blue treatment with strong contrast.
- The hover rule gives the controls visual feedback.
- The empty state becomes a dashed message panel.
- Save public/style.css.
- Return to Safari to check the buttons plus empty state.
You should see blue Add task and recommendation buttons. The empty-list message should sit inside a centered dashed box.
Buttons Not Turning Blue?
Check that the form button sits inside .task-form. Confirm that the recommendation button uses class="secondary-button".
Look for a missing comma between the two selectors. Help me diagnose why the Task Coach buttons are not sharing their styles.
- Place the cursor after the empty-state rule.
- Style the coach card plus response panel by pasting this code:
.coach-card {
align-self: start;
position: sticky;
top: 1rem;
}
.secondary-button {
width: 100%;
margin: 0.5rem 0 1rem;
}
.assistant-output {
min-height: 11rem;
border-radius: 0.9rem;
padding: 1rem;
color: #25334d;
background: #eef3ff;
line-height: 1.6;
white-space: pre-wrap;
}
What Do the Coach Styles Do?
- The coach card stays near the top while a long task list scrolls.
- The recommendation button spans the card width.
- The assistant output gains a pale blue response area.
- Save public/style.css.
- Return to Safari to check the assistant panel.
You should see a full-width recommendation button above a large pale blue output panel. The assistant card should align with the top of the task card.
Assistant Output Still Transparent?
Confirm that the output element uses class="assistant-output" in public/index.html. Check that the CSS selector begins with a period.
Look for an unclosed coach-card rule. Help me troubleshoot why the assistant output panel is not styled.
- Place the cursor after the assistant-output rule.
- Add the accessibility helper plus tablet layout by pasting this code:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
[hidden] {
display: none !important;
}
@media (max-width: 760px) {
.layout {
grid-template-columns: 1fr;
}
.coach-card {
position: static;
}
}
What Changes on Smaller Screens?
- The screen-reader-only label remains available to assistive technology.
- The hidden rule removes elements whose state is hidden.
- The media query stacks both cards below 760 pixels.
- Save public/style.css.
- Narrow the Safari window to check the tablet layout.
You should see the assistant card move below the task card. The coach card should scroll normally in the single-column layout.
Cards Refusing to Stack?
Confirm that the media query closes after the coach-card rule. Check that 760px has no space between the number and unit.
Make sure the Safari window is narrow enough to trigger the rule. Help me troubleshoot the responsive Task Coach grid.
- Place the cursor after the tablet media query.
- Add the narrow-screen controls by pasting this code:
@media (max-width: 520px) {
.task-form,
.task-item {
grid-template-columns: 1fr;
}
.task-actions {
justify-content: flex-end;
}
}
What Changes on Narrow Screens?
- The input and Add task button stack into one column.
- Each future task row also switches to one column.
- Task action buttons align to the right edge of their row.
- Save public/style.css.
- Narrow the Safari window further to check the task form.
You should see the Add task button move below the input. Widen Safari again before adding task behavior.
Form Not Switching to One Column?
Check that the rule targets both .task-form and .task-item. Confirm that the selectors are separated by a comma.
Make sure the window is narrower than 520px. Help me inspect the narrow-screen Task Coach media query.
Add in-memory task behavior
The DOM is JavaScript's representation of the page. Your script reads the form from this representation before rebuilding the task list from an array.
- Select public/app.js in the Explorer sidebar.
- Add the page references plus initial renderer by pasting this code:
const taskForm = document.querySelector("#task-form");
const taskInput = document.querySelector("#task-input");
const taskList = document.querySelector("#task-list");
const taskCount = document.querySelector("#task-count");
const emptyState = document.querySelector("#empty-state");
const recommendButton = document.querySelector("#recommend-button");
const assistantOutput = document.querySelector("#assistant-output");
let tasks = [];
let assistantIsLoading = false;
function createTask(text) {
return {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
text,
completed: false,
};
}
function renderTasks() {
taskList.replaceChildren();
emptyState.hidden = tasks.length > 0;
const incompleteCount = tasks.filter((task) => !task.completed).length;
const noun = incompleteCount === 1 ? "task" : "tasks";
taskCount.textContent = `${incompleteCount} ${noun} left`;
recommendButton.disabled = incompleteCount === 0 || assistantIsLoading;
}
renderTasks();
What Does This Foundation Do?
- The constants store references to page elements that change during task interactions.
- The tasks array holds the current list in page memory.
- The createTask() function gives each task an ID plus an incomplete state.
- The initial renderer updates the empty state plus incomplete count.
- Save public/app.js.
- Return to Safari to check the initial task state.
You should see the empty-state message with zero tasks left. The recommendation button should now look disabled.
Recommendation Button Still Active?
Confirm that renderTasks(); remains at the bottom of public/app.js. Check that recommendButton points to #recommend-button.
Compare the remaining element IDs with public/index.html. Help me diagnose why the initial Task Coach render is not disabling the recommendation button.
Each task needs a checkbox plus visible text. The renderer creates those elements from the current array values.
- Find the blank line above const incompleteCount inside renderTasks().
- Add the first part of the task-row loop by pasting this code:
for (const task of tasks) {
const item = document.createElement("li");
item.className = `task-item${task.completed ? " completed" : ""}`;
item.dataset.taskId = task.id;
const label = document.createElement("label");
label.className = "task-label";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = task.completed;
checkbox.setAttribute("aria-label", `Mark ${task.text} complete`);
const text = document.createElement("span");
text.className = "task-text";
text.textContent = task.text;
label.append(checkbox, text);
}
What Does This Rendering Section Create?
- Each task becomes a list item with its ID stored on the element.
- The checkbox reflects the task's completion value.
- The text span displays the task without interpreting it as HTML.
- Place the cursor directly above the closing brace for the new for loop.
- Complete each task row by pasting this code:
const actions = document.createElement("div");
actions.className = "task-actions";
const planButton = document.createElement("button");
planButton.type = "button";
planButton.className = "plan-button";
planButton.textContent = "Break down";
planButton.disabled = task.completed || assistantIsLoading;
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "delete-button";
deleteButton.textContent = "Delete";
actions.append(planButton, deleteButton);
item.append(label, actions);
taskList.append(item);
How Is Each Task Row Completed?
- The actions container holds the two task buttons.
- Completed tasks disable their Break down button.
- The completed row is appended to the visible task list.
- Save public/app.js.
- Refresh Safari to confirm the renderer still starts successfully.
You should see the empty-state message with zero tasks left. The disabled recommendation button confirms the expanded renderer ran successfully.
Seeing a Script Error?
Confirm that the second rendering block sits inside the for loop. Check that the loop closes before const incompleteCount.
Compare every brace around renderTasks(). Help me diagnose the task-row rendering error in public/app.js.
One update function keeps the array and interface synchronized. The form listener uses that path whenever someone submits a task.
- Place the cursor immediately above the final renderTasks(); line.
- Add the update function plus form listener by pasting this code:
function updateTasks(nextTasks) {
tasks = nextTasks;
renderTasks();
}
taskForm.addEventListener("submit", (event) => {
event.preventDefault();
const text = taskInput.value.trim();
if (!text) {
return;
}
updateTasks([...tasks, createTask(text)]);
taskForm.reset();
taskInput.focus();
});
How Does Adding a Task Work?
- The submit listener prevents the form from reloading the page.
- The trimmed input becomes a task when it contains text.
- The updated array triggers a complete redraw.
- The form resets before returning focus to the input.
- Save public/app.js.
- Enter Draft project outline in the task input.
- Click Add task.
You should see a new task row with a checkbox plus Break down and Delete buttons. The count should change to one task left.
Task Not Appearing?
Check that the form listener appears before the final renderTasks(); line. Confirm that updateTasks() calls renderTasks().
Make sure the form uses id="task-form" and the input uses id="task-input". Help me troubleshoot why submitting the Task Coach form does not add a task row.
The first task gives you a visible row to style. These rules separate its label from its controls.
- Switch back to public/style.css in the Explorer sidebar.
- Find the closing brace for .empty-state.
- Add the task list plus row styles directly below it by pasting this code:
.task-list {
display: grid;
gap: 0.75rem;
margin: 0;
padding: 0;
list-style: none;
}
.task-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.8rem;
align-items: center;
border: 1px solid #e0e6f0;
border-radius: 0.9rem;
padding: 0.85rem;
background: #fbfcfe;
}
How Does the Task Row Take Shape?
- The list stacks task rows with consistent spacing.
- Each row separates the task label from its action controls.
- The pale surface makes every task distinct from the card.
- Save public/style.css.
- Return to Safari to check the Draft project outline row.
You should see the task inside a rounded row. Its controls should sit on the right side.
Task Still Looks Like Plain Text?
Confirm that the renderer assigns task-item to each list item. Check that both new selectors begin with a period.
Make sure the new rules sit before .coach-card. Help me troubleshoot why the Task Coach row styles are not applying.
- Place the cursor after the closing .task-item rule.
- Style the task label plus completion state by pasting this code:
.task-label {
display: flex;
gap: 0.75rem;
align-items: center;
min-width: 0;
}
.task-label input {
width: 1.1rem;
height: 1.1rem;
accent-color: #3457d5;
}
.task-text {
overflow-wrap: anywhere;
}
.task-item.completed .task-text {
color: #7b8799;
text-decoration: line-through;
}
How Will Completion Look?
- The label aligns the checkbox with the task text.
- Long text wraps inside the available row width.
- A completed task becomes muted plus crossed out.
- Save public/style.css.
- Return to Safari to check the task label.
You should see a larger blue checkbox aligned with the task text. The text should remain inside its row.
Checkbox Still Tiny?
Confirm that the label uses class="task-label". Check that the checkbox remains inside that label.
Look for an unclosed rule above .task-label input. Help me inspect the Task Coach label styles.
- Place the cursor after the completed-task rule.
- Style the task actions plus Delete button by pasting this code:
.task-actions {
display: flex;
gap: 0.45rem;
}
.task-actions button {
border-radius: 0.65rem;
padding: 0.55rem 0.7rem;
color: #31405b;
background: #eaf0ff;
font-size: 0.82rem;
font-weight: 700;
}
.task-actions .delete-button {
color: #9b2f3c;
background: #ffebee;
}
How Do the Task Actions Differ?
- The action container keeps both controls together.
- The row buttons use a compact blue treatment.
- The Delete control receives a distinct red treatment.
- Save public/style.css.
- Return to Safari to check both task actions.
You should see a pale blue Break down button beside a pale red Delete button. Both controls should fit neatly inside the row.
Task Buttons Still Unstyled?
Confirm that the renderer assigns task-actions to the controls container. Check that the Delete control receives delete-button.
Compare both selector names with public/app.js. Help me diagnose why the Task Coach action styles are not applying.
✔️ Awesome, I've got everything!
Your responsive Task Coach interface is styled. Double-check that you saved public/style.css.
ⓧ I'd like to double check the full code
:root {
color-scheme: light;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
color: #172033;
background: #f4f7fb;
font-synthesis: none;
}
* {
box-sizing: border-box;
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
background:
radial-gradient(circle at top left, #dce8ff 0, transparent 32rem),
#f4f7fb;
}
button,
input {
font: inherit;
}
button {
border: 0;
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.hero {
width: min(1100px, calc(100% - 2rem));
margin: 0 auto;
padding: 3.5rem 0 2rem;
}
.hero h1,
.section-heading h2,
.coach-card h2 {
margin: 0;
}
.hero h1 {
margin-bottom: 0.5rem;
font-size: clamp(2.4rem, 7vw, 4.5rem);
letter-spacing: -0.06em;
}
.hero > p:last-child,
.coach-intro {
color: #596579;
}
.eyebrow {
margin: 0 0 0.45rem;
color: #3b63da;
font-size: 0.76rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(280px, 0.75fr);
gap: 1.25rem;
width: min(1100px, calc(100% - 2rem));
margin: 0 auto;
padding-bottom: 3rem;
}
.card {
border: 1px solid #dce3ef;
border-radius: 1.25rem;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 18px 45px rgba(32, 50, 84, 0.09);
}
.tasks-card,
.coach-card {
padding: 1.35rem;
}
.section-heading {
display: flex;
align-items: end;
justify-content: space-between;
gap: 1rem;
}
.task-count {
margin: 0;
color: #68758a;
font-size: 0.9rem;
}
.task-form {
display: grid;
grid-template-columns: 1fr auto;
gap: 0.7rem;
margin: 1.35rem 0;
}
.task-form input {
min-width: 0;
border: 1px solid #cbd5e5;
border-radius: 0.8rem;
padding: 0.85rem 0.95rem;
color: #172033;
background: #ffffff;
}
.task-form input:focus {
border-color: #4e6ee6;
outline: 3px solid rgba(78, 110, 230, 0.17);
}
.task-form button,
.secondary-button {
border-radius: 0.8rem;
padding: 0.85rem 1rem;
color: #ffffff;
background: #3457d5;
font-weight: 750;
}
.task-form button:hover,
.secondary-button:hover {
background: #2745b2;
}
.empty-state {
margin: 0;
border: 1px dashed #c8d2e2;
border-radius: 0.9rem;
padding: 1.25rem;
color: #68758a;
text-align: center;
}
.task-list {
display: grid;
gap: 0.75rem;
margin: 0;
padding: 0;
list-style: none;
}
.task-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.8rem;
align-items: center;
border: 1px solid #e0e6f0;
border-radius: 0.9rem;
padding: 0.85rem;
background: #fbfcfe;
}
.task-label {
display: flex;
gap: 0.75rem;
align-items: center;
min-width: 0;
}
.task-label input {
width: 1.1rem;
height: 1.1rem;
accent-color: #3457d5;
}
.task-text {
overflow-wrap: anywhere;
}
.task-item.completed .task-text {
color: #7b8799;
text-decoration: line-through;
}
.task-actions {
display: flex;
gap: 0.45rem;
}
.task-actions button {
border-radius: 0.65rem;
padding: 0.55rem 0.7rem;
color: #31405b;
background: #eaf0ff;
font-size: 0.82rem;
font-weight: 700;
}
.task-actions .delete-button {
color: #9b2f3c;
background: #ffebee;
}
.coach-card {
align-self: start;
position: sticky;
top: 1rem;
}
.secondary-button {
width: 100%;
margin: 0.5rem 0 1rem;
}
.assistant-output {
min-height: 11rem;
border-radius: 0.9rem;
padding: 1rem;
color: #25334d;
background: #eef3ff;
line-height: 1.6;
white-space: pre-wrap;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
[hidden] {
display: none !important;
}
@media (max-width: 760px) {
.layout {
grid-template-columns: 1fr;
}
.coach-card {
position: static;
}
}
@media (max-width: 520px) {
.task-form,
.task-item {
grid-template-columns: 1fr;
}
.task-actions {
justify-content: flex-end;
}
}
A checkbox reports its new value through a change event. The task ID on its row identifies the object that needs updating.
- Switch back to public/app.js in the Explorer sidebar.
- Place the cursor below the form listener.
- Add the completion listener by pasting this code:
taskList.addEventListener("change", (event) => {
if (!event.target.matches('input[type="checkbox"]')) {
return;
}
const item = event.target.closest(".task-item");
updateTasks(
tasks.map((task) =>
task.id === item.dataset.taskId
? { ...task, completed: event.target.checked }
: task,
),
);
});
How Does Completion Work?
- The listener ignores changes that do not come from a task checkbox.
- The nearest task row provides the matching task ID.
- The mapped array changes the completion value for one task.
- Save public/app.js.
- Select the checkbox beside Draft project outline.
You should see the task text become muted plus crossed out. The count should change to zero tasks left.
Checkbox Not Updating the Row?
Confirm that each row receives item.dataset.taskId = task.id; inside renderTasks(). Check that the listener is attached to taskList.
Match the completed property spelling in both places. Help me troubleshoot why a Task Coach checkbox does not update its task.
Deletion uses the same row ID to remove one object from the array. The next render rebuilds the list without that task.
- Place the cursor below the completion listener.
- Add the delete listener by pasting this code:
taskList.addEventListener("click", (event) => {
const item = event.target.closest(".task-item");
if (!item) {
return;
}
if (event.target.matches(".delete-button")) {
updateTasks(tasks.filter((entry) => entry.id !== item.dataset.taskId));
}
});
How Does Deletion Work?
- The click listener finds the row that contains the selected control.
- A Delete click filters that row's task out of the array.
- The update function redraws the remaining tasks.
- Save public/app.js.
- Enter Email project update in the task input.
- Click Add task.
- Click Delete beside Email project update.
You should see the second task disappear. The completed Draft project outline task should remain with zero tasks left.
Delete Button Not Removing Its Task?
Confirm that JavaScript creates the button with className = "delete-button". Check that the click listener uses the matching .delete-button selector.
Make sure the listener appears before the final renderTasks(); call. Help me troubleshoot why the Task Coach Delete button does not remove a task.
✔️ Awesome, I've got everything!
Your add, complete, and delete interactions now work. Double-check that you saved public/app.js.
ⓧ I'd like to double check the full code
const taskForm = document.querySelector("#task-form");
const taskInput = document.querySelector("#task-input");
const taskList = document.querySelector("#task-list");
const taskCount = document.querySelector("#task-count");
const emptyState = document.querySelector("#empty-state");
const recommendButton = document.querySelector("#recommend-button");
const assistantOutput = document.querySelector("#assistant-output");
let tasks = [];
let assistantIsLoading = false;
function createTask(text) {
return {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
text,
completed: false,
};
}
function renderTasks() {
taskList.replaceChildren();
emptyState.hidden = tasks.length > 0;
for (const task of tasks) {
const item = document.createElement("li");
item.className = `task-item${task.completed ? " completed" : ""}`;
item.dataset.taskId = task.id;
const label = document.createElement("label");
label.className = "task-label";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = task.completed;
checkbox.setAttribute("aria-label", `Mark ${task.text} complete`);
const text = document.createElement("span");
text.className = "task-text";
text.textContent = task.text;
label.append(checkbox, text);
const actions = document.createElement("div");
actions.className = "task-actions";
const planButton = document.createElement("button");
planButton.type = "button";
planButton.className = "plan-button";
planButton.textContent = "Break down";
planButton.disabled = task.completed || assistantIsLoading;
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "delete-button";
deleteButton.textContent = "Delete";
actions.append(planButton, deleteButton);
item.append(label, actions);
taskList.append(item);
}
const incompleteCount = tasks.filter((task) => !task.completed).length;
const noun = incompleteCount === 1 ? "task" : "tasks";
taskCount.textContent = `${incompleteCount} ${noun} left`;
recommendButton.disabled = incompleteCount === 0 || assistantIsLoading;
}
function updateTasks(nextTasks) {
tasks = nextTasks;
renderTasks();
}
taskForm.addEventListener("submit", (event) => {
event.preventDefault();
const text = taskInput.value.trim();
if (!text) {
return;
}
updateTasks([...tasks, createTask(text)]);
taskForm.reset();
taskInput.focus();
});
taskList.addEventListener("change", (event) => {
if (!event.target.matches('input[type="checkbox"]')) {
return;
}
const item = event.target.closest(".task-item");
updateTasks(
tasks.map((task) =>
task.id === item.dataset.taskId
? { ...task, completed: event.target.checked }
: task,
),
);
});
taskList.addEventListener("click", (event) => {
const item = event.target.closest(".task-item");
if (!item) {
return;
}
if (event.target.matches(".delete-button")) {
updateTasks(tasks.filter((entry) => entry.id !== item.dataset.taskId));
}
});
renderTasks();
Before you refresh Safari, do you think the completed task will still be there after the page reloads?
- Refresh the local Task Coach page in Safari.
The completed task disappears. The count returns to zero tasks left while the empty-state message returns.
Why Did the Task Disappear?
The tasks array lives in the current page's memory. Refreshing the page runs public/app.js again with a new empty array.
This data loss is the intended obstacle. You now have direct evidence that browser memory cannot preserve task state across a refresh.
Task Still Visible After the Refresh?
Confirm that public/app.js declares let tasks = [];. Check that you refreshed the local URL served by the running project.
Make sure the visible text is a task row instead of the empty-state message. Help me verify why an in-memory Task Coach task appears to survive a Safari refresh.
That is the key limitation exposed: your interactions work until the page reloads. Next, you will preserve the same task text and completion state across refreshes.
Save Tasks in Browser Storage
Your list can already manage tasks inside Safari. A refresh still wipes that work away.
The localStorage API keeps serialized task data inside the browser. This step wires every task change into that storage.
In this step, get ready to:
- Load saved tasks when the page starts.
- Save task changes after each interaction.
- Prove task text and completion states survive a refresh.
Add the storage helpers
Browser storage holds strings. JSON converts your task array into storable text before rebuilding it later.
- In the Visual Studio Code Explorer sidebar, select public/app.js.
- Replace the opening declarations through the assistant loading state by pasting this code:
const STORAGE_KEY = "task-coach-tasks";
const taskForm = document.querySelector("#task-form");
const taskInput = document.querySelector("#task-input");
const taskList = document.querySelector("#task-list");
const taskCount = document.querySelector("#task-count");
const emptyState = document.querySelector("#empty-state");
const recommendButton = document.querySelector("#recommend-button");
const assistantOutput = document.querySelector("#assistant-output");
let tasks = loadTasks();
let assistantIsLoading = false;
function loadTasks() {
try {
const savedTasks = localStorage.getItem(STORAGE_KEY);
const parsedTasks = savedTasks ? JSON.parse(savedTasks) : [];
return Array.isArray(parsedTasks) ? parsedTasks : [];
} catch {
return [];
}
}
function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
What does this code do?
- The STORAGE_KEY constant gives the saved task list a stable browser storage name.
- The loadTasks() function reads the stored text when the page starts.
- The JSON.parse() call rebuilds the stored text as JavaScript data.
- The Array.isArray() check accepts valid task arrays.
- The fallback returns an empty array when the stored data cannot be used.
- The saveTasks() function converts the current task array into text for storage.
- Save public/app.js.
- Refresh the local app in Safari.
You should see the complete two-panel Task Coach interface load without a blank page.
Seeing a blank page?
Check that the replacement starts with STORAGE_KEY. Confirm that all seven DOM references remain above the task state.
Compare the braces around loadTasks() with the code block. Help me find the syntax problem in my browser storage helpers.
Save every task change
Every task change already passes through updateTasks(). Saving inside that function keeps each interaction covered.
- In public/app.js, find the updateTasks() function.
- Replace the complete function by pasting this code:
function updateTasks(nextTasks) {
tasks = nextTasks;
saveTasks();
renderTasks();
}
How does this save each interaction?
- The nextTasks parameter holds the updated array from the current interaction.
- The assignment replaces the previous in-memory task state.
- The saveTasks() call writes that updated array into browser storage.
- The renderTasks() call rebuilds the visible list from the same state.
- Save public/app.js.
- Enter Write launch notes in the task input.
- Click Add task.
You should see Write launch notes in the list.
- Refresh Safari.
The task returns after the refresh. That is the persistence loop working across page loads.
Did the task disappear again?
Confirm that saveTasks() appears between the task assignment and renderTasks() inside updateTasks().
Check that STORAGE_KEY uses the same spelling in both storage functions. Help me troubleshoot why my Task Coach tasks still disappear after a refresh.
✔️ Awesome, I've got everything!
Great. public/app.js now loads saved tasks at startup and saves every task update.
ⓧ I'd like to double check the full code
const STORAGE_KEY = "task-coach-tasks";
const taskForm = document.querySelector("#task-form");
const taskInput = document.querySelector("#task-input");
const taskList = document.querySelector("#task-list");
const taskCount = document.querySelector("#task-count");
const emptyState = document.querySelector("#empty-state");
const recommendButton = document.querySelector("#recommend-button");
const assistantOutput = document.querySelector("#assistant-output");
let tasks = loadTasks();
let assistantIsLoading = false;
function loadTasks() {
try {
const savedTasks = localStorage.getItem(STORAGE_KEY);
const parsedTasks = savedTasks ? JSON.parse(savedTasks) : [];
return Array.isArray(parsedTasks) ? parsedTasks : [];
} catch {
return [];
}
}
function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
function createTask(text) {
return {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
text,
completed: false,
};
}
function renderTasks() {
taskList.replaceChildren();
emptyState.hidden = tasks.length > 0;
for (const task of tasks) {
const item = document.createElement("li");
item.className = `task-item${task.completed ? " completed" : ""}`;
item.dataset.taskId = task.id;
const label = document.createElement("label");
label.className = "task-label";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = task.completed;
checkbox.setAttribute("aria-label", `Mark ${task.text} complete`);
const text = document.createElement("span");
text.className = "task-text";
text.textContent = task.text;
label.append(checkbox, text);
const actions = document.createElement("div");
actions.className = "task-actions";
const planButton = document.createElement("button");
planButton.type = "button";
planButton.className = "plan-button";
planButton.textContent = "Break down";
planButton.disabled = task.completed || assistantIsLoading;
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "delete-button";
deleteButton.textContent = "Delete";
actions.append(planButton, deleteButton);
item.append(label, actions);
taskList.append(item);
}
const incompleteCount = tasks.filter((task) => !task.completed).length;
const noun = incompleteCount === 1 ? "task" : "tasks";
taskCount.textContent = `${incompleteCount} ${noun} left`;
recommendButton.disabled = incompleteCount === 0 || assistantIsLoading;
}
function updateTasks(nextTasks) {
tasks = nextTasks;
saveTasks();
renderTasks();
}
taskForm.addEventListener("submit", (event) => {
event.preventDefault();
const text = taskInput.value.trim();
if (!text) {
return;
}
updateTasks([...tasks, createTask(text)]);
taskForm.reset();
taskInput.focus();
});
taskList.addEventListener("change", (event) => {
if (!event.target.matches('input[type="checkbox"]')) {
return;
}
const item = event.target.closest(".task-item");
updateTasks(
tasks.map((task) =>
task.id === item.dataset.taskId
? { ...task, completed: event.target.checked }
: task,
),
);
});
taskList.addEventListener("click", (event) => {
const item = event.target.closest(".task-item");
if (!item) {
return;
}
if (event.target.matches(".delete-button")) {
updateTasks(tasks.filter((entry) => entry.id !== item.dataset.taskId));
}
});
renderTasks();
Test the restored task state
A reliable persistence test covers an empty list first. It then checks a mixture of active and completed tasks.
- Delete every task from the current list.
You should see the empty-state message with a count of 0 tasks left.
- Add a task named Plan my project.
- Add a task named Email project update.
You should see two active tasks with a count of 2 tasks left.
- Select the checkbox beside Plan my project.
- Add a task named Book focus time.
You should see one completed task and two active tasks.
- Click Delete beside Book focus time.
The list now contains one completed task and one active task. The count shows 1 task left.
Before you refresh, which task text and completion states do you expect Safari to restore?
- Refresh Safari.
You should see Plan my project return as completed. You should see Email project update return as active.
You have fixed the disappearing-task problem. Task Coach now rebuilds its interface from browser storage after every refresh.
Are the completion states incorrect?
Confirm that the checkbox change listener still sends a new task array into updateTasks(). The save only captures changes that reach that function.
Check that loadTasks() returns parsedTasks when the stored value is an array. Help me debug why Task Coach restores task text but loses completion states.
Your task list now keeps its state across browser sessions. Next, you will connect those persistent tasks to the AI coaching endpoint.
Connect the AI Task Coach
Your task list now survives a refresh. The assistant panel still cannot turn those saved tasks into useful guidance.
A Cloudflare Worker gives the browser a server-side endpoint. A Workers AI binding lets that endpoint call the model without placing a provider credential in your frontend code.
In this step, get ready to:
- Configure the AI binding and coaching endpoint.
- Connect the assistant controls to the endpoint.
- Authorize Wrangler and test both coaching actions.
Configure the AI binding and Worker
The binding exposes the account's AI service to server-side code as env.AI. The Worker then accepts coaching requests through /api/coach.
- In the Visual Studio Code Explorer sidebar, select wrangler.jsonc.
- Replace the contents of wrangler.jsonc with this configuration:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "task-coach",
"main": "src/index.js",
"compatibility_date": "2026-10-25",
"assets": {
"directory": "./public"
},
"ai": {
"binding": "AI"
}
}
What does this configuration do?
- The assets section continues serving the interface from public.
- The ai section creates a Workers AI binding named AI.
- Worker code accesses that binding through env.AI.
- Save wrangler.jsonc.
- Check the integrated terminal from earlier for a successful development server reload.
Development server not reloading?
Check that the ai property sits after the closing brace for assets. Confirm that a comma separates the two properties.
Make sure the file name remains wrangler.jsonc. Help me check my Wrangler AI binding configuration.
✔️ Awesome, I've got everything!
Great. Your local Worker now has access to the binding named AI.
ⓧ I'd like to double check the full code
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "task-coach",
"main": "src/index.js",
"compatibility_date": "2026-10-25",
"assets": {
"directory": "./public"
},
"ai": {
"binding": "AI"
}
}
The request handler has two jobs. It validates browser input before asking the model for a breakdown or a priority.
Why use a same-origin endpoint?
The interface and Worker share one origin. The browser can call /api/coach without separate cross-origin configuration.
The binding stays inside the Worker runtime. Your frontend receives only the generated answer.
The handler is split into two paste-sized parts. Keep both parts together before saving the file.
- Select src/index.js in the Explorer sidebar.
- Add the model definition and request validation by pasting this first part:
const MODEL = "@cf/meta/llama-3.2-3b-instruct";
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname !== "/api/coach" || request.method !== "POST") {
return Response.json({ error: "Not found" }, { status: 404 });
}
try {
const body = await request.json();
const action = body.action;
const task = typeof body.task === "string" ? body.task.trim().slice(0, 500) : "";
const tasks = Array.isArray(body.tasks)
? body.tasks
.filter((item) => typeof item === "string")
.map((item) => item.trim().slice(0, 200))
.filter(Boolean)
.slice(0, 20)
: [];
let userMessage;
if (action === "plan" && task) {
userMessage = `Break this task into four small, practical steps: ${JSON.stringify(task)}`;
} else if (action === "prioritize" && tasks.length > 0) {
userMessage = `Choose the single best task to do next from this list and explain the first action: ${JSON.stringify(tasks)}`;
What does this part do?
- The MODEL constant identifies the model used for each coaching request.
- The route guard accepts only POST requests to /api/coach.
- The input checks limit task length and remove invalid list entries.
- The two action paths create a focused message for planning or prioritization.
- Finish the handler by pasting this second part directly below the first part:
} else {
return Response.json(
{ error: "Send a valid coaching action and at least one task." },
{ status: 400 },
);
}
const result = await env.AI.run(MODEL, {
messages: [
{
role: "system",
content: "You are a practical task coach. Be concise, specific, and encouraging.",
},
{
role: "user",
content: userMessage,
},
],
});
return Response.json({ answer: result.response });
} catch (error) {
console.error(error);
return Response.json(
{ error: "Task Coach could not generate a response." },
{ status: 500 },
);
}
},
};
What does this part do?
- The invalid-input branch returns a clear client error before calling the model.
- The messages array combines the coach's role with the selected task request.
- The call to env.AI.run() sends those messages through the configured binding.
- The final branches return the generated answer or a server error as JSON.
- Save src/index.js.
- Return to the local Task Coach tab in Safari.
- Add /api/coach after the local origin in the address bar.
- Press Enter to send a browser request to the endpoint.
You will see a JSON response containing Not found. That response proves the Worker now owns the endpoint and rejects the browser's non-POST request.
Good progress. Your local runtime can now distinguish the coaching API from the static interface.
Not seeing the JSON response?
Confirm that the path is exactly /api/coach. Check that src/index.js was saved after both parts were added.
Look in the integrated terminal for a JavaScript syntax problem. Help me debug my Task Coach Worker handler.
✔️ Awesome, I've got everything!
Your endpoint is active. The route guard is returning the expected response.
ⓧ I'd like to double check the full code
const MODEL = "@cf/meta/llama-3.2-3b-instruct";
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname !== "/api/coach" || request.method !== "POST") {
return Response.json({ error: "Not found" }, { status: 404 });
}
try {
const body = await request.json();
const action = body.action;
const task = typeof body.task === "string" ? body.task.trim().slice(0, 500) : "";
const tasks = Array.isArray(body.tasks)
? body.tasks
.filter((item) => typeof item === "string")
.map((item) => item.trim().slice(0, 200))
.filter(Boolean)
.slice(0, 20)
: [];
let userMessage;
if (action === "plan" && task) {
userMessage = `Break this task into four small, practical steps: ${JSON.stringify(task)}`;
} else if (action === "prioritize" && tasks.length > 0) {
userMessage = `Choose the single best task to do next from this list and explain the first action: ${JSON.stringify(tasks)}`;
} else {
return Response.json(
{ error: "Send a valid coaching action and at least one task." },
{ status: 400 },
);
}
const result = await env.AI.run(MODEL, {
messages: [
{
role: "system",
content: "You are a practical task coach. Be concise, specific, and encouraging.",
},
{
role: "user",
content: userMessage,
},
],
});
return Response.json({ answer: result.response });
} catch (error) {
console.error(error);
return Response.json(
{ error: "Task Coach could not generate a response." },
{ status: 500 },
);
}
},
};
Connect the browser to the coach
The frontend sends an HTTP request containing JSON. It also updates the assistant panel while the request is running.
The request function is split into two parts. Paste both parts together before saving public/app.js.
- Select public/app.js in the Explorer sidebar.
- Find the closing brace of updateTasks(nextTasks).
- Start the assistant request function directly below it by pasting this first part:
async function askAssistant(action, task = "") {
const incompleteTasks = tasks.filter((item) => !item.completed);
if (action === "prioritize" && incompleteTasks.length === 0) {
assistantOutput.textContent = "Add an incomplete task before asking for a priority.";
return;
}
assistantIsLoading = true;
assistantOutput.textContent = "Task Coach is thinking...";
renderTasks();
try {
const response = await fetch("/api/coach", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
action,
task,
tasks: incompleteTasks.map((item) => item.text),
}),
});
What does this part do?
- The function filters out completed tasks before preparing the request.
- The loading state disables coaching controls through the existing render logic.
- The request sends the action and task text to the same-origin endpoint.
- The content-type header tells the Worker to read the body as JSON.
- Finish askAssistant() by pasting this second part directly below the first part:
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "The coach could not answer.");
}
assistantOutput.textContent = data.answer;
} catch (error) {
assistantOutput.textContent = `Something went wrong: ${error.message}`;
} finally {
assistantIsLoading = false;
renderTasks();
}
}
How are responses handled?
- A successful response places data.answer in the assistant panel.
- An unsuccessful response turns the Worker's error into a visible message.
- The finally block restores the controls after every outcome.
- Save public/app.js.
- Check the integrated terminal for a successful reload without a JavaScript syntax problem.
The request function now loads successfully. The next two event handlers connect it to the visible controls.
- Find the delete-button condition inside the task list click listener.
- Add the planning condition directly below that delete condition:
if (event.target.matches(".plan-button") && task) {
askAssistant("plan", task.text);
}
What does this condition do?
A click on Break down passes the selected task text to askAssistant() with the plan action.
- Find the final renderTasks(); call at the bottom of public/app.js.
- Add the recommendation listener directly above that final call:
recommendButton.addEventListener("click", () => {
askAssistant("prioritize");
});
What does this listener do?
The recommendation button requests the prioritize action. The request function automatically includes every incomplete task.
- Save public/app.js.
- Check the integrated terminal for another successful development server reload.
Seeing a client-side syntax problem?
Check that both halves of askAssistant() appear between updateTasks() and the form submit listener. Confirm that the final closing brace is present.
Make sure the recommendation listener remains above the final renderTasks(); call. Help me find the syntax problem in public/app.js.
✔️ Awesome, I've got everything!
Your two assistant controls now send the appropriate coaching action.
ⓧ I'd like to double check the full code
const STORAGE_KEY = "task-coach-tasks";
const taskForm = document.querySelector("#task-form");
const taskInput = document.querySelector("#task-input");
const taskList = document.querySelector("#task-list");
const taskCount = document.querySelector("#task-count");
const emptyState = document.querySelector("#empty-state");
const recommendButton = document.querySelector("#recommend-button");
const assistantOutput = document.querySelector("#assistant-output");
let tasks = loadTasks();
let assistantIsLoading = false;
function loadTasks() {
try {
const savedTasks = localStorage.getItem(STORAGE_KEY);
const parsedTasks = savedTasks ? JSON.parse(savedTasks) : [];
return Array.isArray(parsedTasks) ? parsedTasks : [];
} catch {
return [];
}
}
function saveTasks() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
function createTask(text) {
return {
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
text,
completed: false,
};
}
function renderTasks() {
taskList.replaceChildren();
emptyState.hidden = tasks.length > 0;
for (const task of tasks) {
const item = document.createElement("li");
item.className = `task-item${task.completed ? " completed" : ""}`;
item.dataset.taskId = task.id;
const label = document.createElement("label");
label.className = "task-label";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = task.completed;
checkbox.setAttribute("aria-label", `Mark ${task.text} complete`);
const text = document.createElement("span");
text.className = "task-text";
text.textContent = task.text;
label.append(checkbox, text);
const actions = document.createElement("div");
actions.className = "task-actions";
const planButton = document.createElement("button");
planButton.type = "button";
planButton.className = "plan-button";
planButton.textContent = "Break down";
planButton.disabled = task.completed || assistantIsLoading;
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "delete-button";
deleteButton.textContent = "Delete";
actions.append(planButton, deleteButton);
item.append(label, actions);
taskList.append(item);
}
const incompleteCount = tasks.filter((task) => !task.completed).length;
const noun = incompleteCount === 1 ? "task" : "tasks";
taskCount.textContent = `${incompleteCount} ${noun} left`;
recommendButton.disabled = incompleteCount === 0 || assistantIsLoading;
}
function updateTasks(nextTasks) {
tasks = nextTasks;
saveTasks();
renderTasks();
}
async function askAssistant(action, task = "") {
const incompleteTasks = tasks.filter((item) => !item.completed);
if (action === "prioritize" && incompleteTasks.length === 0) {
assistantOutput.textContent = "Add an incomplete task before asking for a priority.";
return;
}
assistantIsLoading = true;
assistantOutput.textContent = "Task Coach is thinking...";
renderTasks();
try {
const response = await fetch("/api/coach", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
action,
task,
tasks: incompleteTasks.map((item) => item.text),
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "The coach could not answer.");
}
assistantOutput.textContent = data.answer;
} catch (error) {
assistantOutput.textContent = `Something went wrong: ${error.message}`;
} finally {
assistantIsLoading = false;
renderTasks();
}
}
taskForm.addEventListener("submit", (event) => {
event.preventDefault();
const text = taskInput.value.trim();
if (!text) {
return;
}
updateTasks([...tasks, createTask(text)]);
taskForm.reset();
taskInput.focus();
});
taskList.addEventListener("change", (event) => {
if (!event.target.matches('input[type="checkbox"]')) {
return;
}
const item = event.target.closest(".task-item");
updateTasks(
tasks.map((task) =>
task.id === item.dataset.taskId
? { ...task, completed: event.target.checked }
: task,
),
);
});
taskList.addEventListener("click", (event) => {
const item = event.target.closest(".task-item");
if (!item) {
return;
}
const task = tasks.find((entry) => entry.id === item.dataset.taskId);
if (event.target.matches(".delete-button")) {
updateTasks(tasks.filter((entry) => entry.id !== item.dataset.taskId));
}
if (event.target.matches(".plan-button") && task) {
askAssistant("plan", task.text);
}
});
recommendButton.addEventListener("click", () => {
askAssistant("prioritize");
});
renderTasks();
Authorize and test the coach
Wrangler uses browser authorization to connect local development to your Cloudflare account. Restarting development mode after sign-in makes the AI binding available to local requests.
- Return to the integrated terminal running development mode.
- Stop the current development server by pressing Ctrl+C.
- Start Wrangler authorization by running this command:
npx wrangler login
- Complete the Cloudflare authorization in the browser window Wrangler opens.
- Return to the integrated terminal after the authorization completes.
The terminal confirms that Wrangler can now use your Cloudflare account.
Authorization not completing?
Check whether the authorization page opened behind Safari's current window. Complete the account authorization before returning to Visual Studio Code.
If the terminal reports a login problem, run the same command again after confirming your internet connection. Help me troubleshoot Wrangler authorization.
- Restart the local development server by running this command:
npm run dev
Wrangler prints the local development URL when the server is ready.
- Open the printed local URL in Safari.
- Add tasks until the list contains at least two incomplete tasks.
Free allocation limit
Local Workers AI requests use the same daily allocation as deployed requests. Workers Free includes 10,000 Neurons per day.
You will not receive Workers Free overage charges because further AI operations fail after the daily limit is exhausted. Stop testing if that happens.
Before you test the first action, predict how the selected task will change when the coach receives it.
- Click Break down beside one incomplete task.
You will briefly see Task Coach is thinking.... The assistant panel then shows an AI-generated action plan for that task.
Before you test the second action, predict whether the coach will use completed tasks in its recommendation.
- Click What should I do next?.
The assistant panel shows one recommendation based on the incomplete tasks. Completed tasks are excluded from the request.
That is the full local loop working. Your browser state now reaches a server-side AI model and returns useful guidance to the same panel.
Coach not returning an answer?
Confirm that the development server was restarted after Wrangler authorization. Check the integrated terminal for a Worker error after clicking an assistant control.
Make sure at least one task remains incomplete. Help me debug the local Task Coach AI request.
Your persistent task list now has a working AI coach in local development. Next, you will deploy the interface and endpoint together at one public URL.
Deploy Task Coach
Your local Task Coach can now break down an incomplete task and choose a clear next priority.
That success still lives on your Mac. This step deploys the complete app to one public Cloudflare Workers URL that anyone can open.
In this step, get ready to:
- Review the Worker configuration that controls the public deployment.
- Deploy the complete Task Coach project.
- Verify every task and coaching action on the public origin.
Review the deployment configuration
A deployment follows wrangler.jsonc as its map. Each value must still point at the files and binding that already work locally.
- Select wrangler.jsonc in the Explorer sidebar of the existing task-coach project.
- Confirm the name value is task-coach.
- Confirm the main value points to src/index.js.
- Confirm the compatibility_date value is 2026-10-25.
- Confirm the assets section points to ./public.
- Confirm the ai section exposes the binding as AI.
Together, these settings describe the current local architecture. The public deployment can reproduce it without changing any code.
✔️ Awesome, I've got everything!
Great. Every deployment field matches the project that already works locally.
ⓧ I'd like to double check the full code
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "task-coach",
"main": "src/index.js",
"compatibility_date": "2026-10-25",
"assets": {
"directory": "./public"
},
"ai": {
"binding": "AI"
}
}
How does this file shape the deployment?
- The $schema value describes the supported configuration structure.
- The name value gives the Worker its task-coach name.
- The main value identifies the Worker entry point.
- The compatibility_date value selects the runtime behavior used by the Worker.
- The assets section publishes the files inside ./public.
- The ai section exposes the server-side binding as env.AI.
Deploy the Worker
Deployment sends the existing project to Cloudflare. The same origin serves the browser files plus /api/coach.
- Create a second integrated terminal with the new-terminal control in the Visual Studio Code terminal panel.
Before you deploy, predict whether one Worker can serve the page plus its coaching endpoint.
- Publish the current project by running this command:
npm run deploy
What does this command do?
- The npm command selects the deploy script from package.json.
- That script runs wrangler deploy with the local Wrangler dependency.
- Wrangler reads wrangler.jsonc before publishing the Worker.
- The deployment includes the files from ./public plus the Worker in src/index.js.
The terminal confirms that task-coach was deployed. It also prints the public workers.dev address.
- Record the printed public address here: your workers.dev URL.
You have made the local-to-public leap. Task Coach now has an address that other people can open.
Deployment not completing?
- Confirm the second terminal is inside the existing task-coach folder.
- Complete the browser authentication flow if the previous Wrangler session has expired.
- Run the deployment command again after authentication finishes.
- Ask for help with the deployment output: Help me diagnose why my Task Coach Worker deployment is not completing.
Test the public app
Safari keeps localStorage data separate for each browser origin. Your deployed app therefore has its own task list.
Before you load the public address, predict whether tasks from local development will follow you.
- Create a new tab in the Safari window from earlier.
- Paste your workers.dev URL into the address bar.
- Press Enter to load the deployed app.
The deployed Task Coach interface loads from its public origin. Tasks from the local development address do not appear here.
- Enter Plan the weekly update in the new-task field.
- Click Add task.
- Enter Review launch notes in the new-task field.
- Click Add task.
Both tasks appear in the list. The counter shows 2 tasks left.
- Reload the deployed page using Safari's toolbar.
Both task names return after the reload. The deployed origin now has its own persistent browser state.
- Select the checkbox beside Plan the weekly update.
The completed task becomes crossed out. The counter changes to 1 task left.
A short wait is normal while Workers AI generates each response.
Before you ask for coaching, predict whether the deployed endpoint can answer from the same public origin.
- Click Break down beside Review launch notes.
The assistant panel first shows Task Coach is thinking... The panel then displays a breakdown for the selected task.
- Click What should I do next?.
The assistant recommends the remaining incomplete task. Its response includes a first action you can take.
- Click Delete beside Review launch notes.
The selected task disappears from the deployed list. You have now verified add, refresh, complete, delete, breakdown, and recommendation behavior on the public app.
That is the full deployment working. Your browser interface and server-side coach now operate together at one public address.
Public coaching response not appearing?
- Confirm the address bar contains your workers.dev URL.
- Confirm the selected task is still incomplete before choosing Break down.
- Confirm at least one incomplete task remains before requesting a recommendation.
- Ask for help with the public request: Help me diagnose why my deployed Task Coach page loads but its AI coaching request does not return a response.
Secret mission
Design a Better Coaching Prompt
Redesign the coaching prompt so vague and specific tasks receive consistent guidance. You will separate task data from instructions before requiring testable response formats.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep Task Coach online, pause your activity, or delete every project resource. Task Coach stays at $0 on the Cloudflare Workers Free plan while usage remains within the included limits.
How the Free Limits Work
Workers Free includes 100,000 Worker requests per day. Workers AI includes 10,000 Workers AI Neurons per day at no charge.
Further operations fail when a free allocation is exhausted. Static asset requests remain free and unlimited.
Resources you used:
- The deployed Cloudflare Worker named task-coach, including its static assets and AI binding.
- The local Node.js project in the task-coach folder, including its installed npm packages.
- The browser-local task data stored through localStorage for the local and deployed origins in Safari.
Keep everything running
No action is needed while you are actively using Task Coach. The public app remains available for future demonstrations.
- Leave the task-coach Worker deployed.
- Keep the local task-coach folder on your Mac.
- Keep Safari's browser-local task data for both origins.
- Monitor Workers and Workers AI usage in the Cloudflare dashboard.
Pause - I'll come back to this later
Pausing activity leaves the Worker deployed for your return. Avoiding new visits prevents the app from making more Worker or AI requests.
- Stop sharing the public workers.dev URL.
- Avoid visiting the deployed Task Coach app.
- Keep the local task-coach folder for future updates.
- Leave Safari's browser-local task data in place.
Delete - I don't want to use this again
Deletion is permanent. The cloud deployment disappears before you remove the local source files.
Delete the deployed Worker:
- Switch back to the integrated terminal in Visual Studio Code.
- Confirm the terminal prompt is inside the task-coach folder.
- Delete the deployed task-coach Worker by running this command:
npm run delete
What Does This Command Do?
- The delete script runs the deletion command configured through package.json.
- Wrangler targets the Worker defined in wrangler.jsonc.
- The deletion removes the deployed Worker plus its associated Cloudflare platform resources.
- Complete the deletion confirmation when Wrangler requests it.
- Return to the former public workers.dev URL in Safari.
You should no longer see Task Coach at that public address. This confirms that the Worker deployment is gone.
Worker Still Available?
- Confirm that the terminal prompt was inside the task-coach folder when you ran the deletion command.
- Reconnect Wrangler through its browser authentication flow if your session has expired.
- Help me troubleshoot the Worker deletion.
Clear the browser-local task data:
Deleting the Worker leaves each origin's saved task list on your Mac. Clearing the website data removes those remaining browser records.
- Use Safari's website data controls to clear the local development origin from earlier.
- Use Safari's website data controls to clear the public workers.dev origin from earlier.
The task lists saved for both origins are now removed from Safari.
Delete the local project:
Removing the local folder cannot be undone. Your deployed Worker is already gone before this command touches the source files.
- Return to the integrated terminal from the Worker deletion.
- Remove the local task-coach folder by running these commands:
cd ..
rm -rf task-coach
ls
What Do These Commands Do?
- The cd .. command moves the terminal to the folder containing task-coach.
- The rm -rf task-coach command permanently removes the local project folder.
- The ls command lists the remaining items so you can confirm that the folder is absent.
You should not see task-coach in the ls output. Your local project files and installed dependencies are now gone.
Folder Still Listed?
- Confirm that the terminal moved to the folder containing task-coach before removal.
- Check the folder permissions if macOS blocks the deletion.
- Help me safely remove the local folder.
Nice Work!
Nice Work!
You did it! Task Coach is live at a public URL through Cloudflare Workers. Your app now turns a task list into practical guidance with Workers AI.
You've learned how to:
- Build a responsive to-do list that adds tasks. Complete tasks. Delete tasks. Preserve task state with localStorage.
- Connect browser JavaScript to a same-origin serverless endpoint. Use an AI binding without placing a provider key in frontend code.
- Deploy a full-stack Task Coach app to a public URL. Verify that its persistent task list works with both AI coaching actions.
- Secret Mission: Design a stricter coaching prompt that separates task data from instructions. Test its response format with vague and specific tasks.
Ready to quiz yourself?