From 28a7e9a8aaddb4d1fdbec56fec66374ce26bd0d2 Mon Sep 17 00:00:00 2001 From: Michael Gilardi <11632652+mgilardi@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:41:33 -0700 Subject: [PATCH] Add an Impact view next to List and Markdown The owner asked for project buckets ordered by impact and effort, top five each, without losing the current view. One bucket per category; effort comes from the size estimates and impact is a plain field on the item's line (impact:high, or impact:high* when the user set it). A Focus column holds each category's top item. Finishing an item leaves it in place and the next joins only when asked. Dragging reorders the view only, or moves an item to another category. The phone note hides the new field. Decisions are in docs/DECISIONS.md. Tested: 37 unit tests, 134 engine tests, ci-checks.sh and the e2e run (81 checks, 21 of them for this view) pass locally. Not built: Claude rating unrated items during /morning and /weekly, which needs the owner's yes for the RULES.md change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CwdHcRvts6qgU3EZBfNtvY --- CHANGELOG.md | 4 + apps/desktop/e2e/ui.mjs | 46 +++++++ apps/desktop/src/App.tsx | 26 ++-- apps/desktop/src/ImpactUI.tsx | 219 +++++++++++++++++++++++++++++++++ apps/desktop/src/ListUI.tsx | 4 + apps/desktop/src/app.css | 18 +++ apps/desktop/src/focus.test.ts | 119 ++++++++++++++++++ apps/desktop/src/focus.ts | 131 ++++++++++++++++++++ apps/desktop/src/master.ts | 33 ++++- apps/desktop/tsconfig.json | 1 + docs/DECISIONS.md | 1 + docs/test-audit.md | 15 +++ engine/tools/notes.py | 2 +- tests/test_engine.py | 4 +- 14 files changed, 611 insertions(+), 12 deletions(-) create mode 100644 apps/desktop/src/ImpactUI.tsx create mode 100644 apps/desktop/src/focus.test.ts create mode 100644 apps/desktop/src/focus.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 92556bf..9571092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased +- **An Impact view in the app, next to List and Markdown.** One bucket per category, each showing its best five: low effort and high impact first, then high/high, low/low and high effort with low impact. Effort comes from the size estimate, and you rate impact with one click. A Focus column holds the top item from each category, most imperative first. Something you finish there stays in place, marked completed, and the next one joins only when you ask. Drag to reorder a bucket, or drag (or use Move…) to put an item in another category. The List view hasn't changed. +- **Other tools can hand Theseus items for your yes.** `engine/tools/intake.py` reads what they leave in `data/intake/`; nothing joins your list until you accept it. + ## 0.6.1 - **Fewer calls to connected services.** ClickUp's connector allows 100 calls a day, and syncing every 15 minutes used most of them. Connected services are now read at set times (`reread_times`), plus the morning and nightly runs, and a ClickUp read takes two calls. - **A first wall display.** `theseus wall setup ` puts your list on a TV or monitor from a Raspberry Pi, grouped by category, with no Claude. Your Mac sends only what the screen's views use, and never messages, email or your Today note. Task titles stay off until you choose, and `theseus wall hours 6am 12am` sets when the screen is on. It hasn't run on a real Pi yet. diff --git a/apps/desktop/e2e/ui.mjs b/apps/desktop/e2e/ui.mjs index ffdd538..af04f4b 100644 --- a/apps/desktop/e2e/ui.mjs +++ b/apps/desktop/e2e/ui.mjs @@ -115,5 +115,51 @@ st.engine_version='0.4.0';await sleep(3300);ok('warns when the engine is out of await p.click('nav button:has-text("Activity")');await p.waitForSelector('text=Sam Rivera');ok('Activity lists thread notes with both marks',await p.isVisible('text=checked 2026-11-05 10:00:00'));await p.click('button:has-text("Sam Rivera")');await p.waitForSelector('text=Short replies.');ok('a thread note opens',await p.isVisible('text=Short replies.')); await p.click('nav button:has-text("Setup")');await p.click('button:has-text("Commands")');await p.waitForSelector('text=THESEUS COMMANDS');ok('Setup shows the command list',await p.isVisible('text=THESEUS COMMANDS')); await sleep(100);ok('still no page errors',errs.length===0);if(errs.length)console.log(errs.slice(0,3)); +// the impact view ------------------------------------------------------------ +{const IMPACT=`# ★ Master List + +## 🔥 NOW +- [ ] Big important · #Biz · ~3h · impact:high · id:t1 +- [ ] Quick important · #Biz · ~15m · impact:high · id:t2 +- [ ] Quick minor · #Biz · ~10m · impact:low · id:t3 +- [ ] Unrated · #Biz · id:t4 +- [ ] Home thing · #Home · due 11/4 · ~20m · impact:low · id:t5 + +## ⏭ NEXT +- [ ] Big minor · #Biz · ~2h · impact:low · id:t7 +- [ ] Biz six · #Biz · ~15m · id:t8 +- [ ] Biz seven · #Biz · ~15m · id:t9 +- [ ] Biz eight · #Biz · ~15m · id:t13 +- [ ] Biz nine · #Biz · ~15m · id:t14 +- [ ] Home two · #Home · ~1h · id:t10 + +## ⏳ WAITING ON +- [ ] Waiting thing · #Biz · impact:high · id:t12 + +## ✅ DONE +`; + await p.evaluate(()=>localStorage.clear());file=IMPACT;await sleep(3400); + await p.click('nav >> text=List');await p.click('.segmented >> text=Impact');await p.waitForSelector('.impact-board'); + const bucket=(c)=>p.locator('[data-drop-group="impact:'+c+'"]');const titles=async(c)=>(await bucket(c).locator('.task-title').allInnerTexts()); + ok('impact: a bucket per category, five at most, easiest wins first',JSON.stringify(await titles('Biz'))===JSON.stringify(['Quick important','Big important','Quick minor','Big minor','Biz six'])); + ok('impact: waiting items are left out',!(await p.locator('.impact-board').innerText()).includes('Waiting thing')); + ok('impact: focus holds each bucket\'s top item',JSON.stringify(await p.locator('.impact-focus .task-title').allInnerTexts())===JSON.stringify(['Quick important','Home thing'])); + ok('impact: opening the view changes nothing',file===IMPACT); + await p.screenshot({path:OUT+'/impact.png'}); + await bucket('Biz').locator('[data-drag-id="t8"] .impact button:has-text("High")').click();await sleep(700);ok('impact: one click rates it and marks it yours',file.includes('- [ ] Biz six · #Biz · ~15m · impact:high* · id:t8')); + await bucket('Biz').locator('[data-drag-id="t8"] .impact button:has-text("High")').click();await sleep(700);ok('impact: clicking your rating again takes it off',file.includes('- [ ] Biz six · #Biz · ~15m · id:t8')); + {const before=file;await bucket('Biz').locator('[data-drag-id="t1"] select.move-to').selectOption('up');await sleep(500);ok('impact: Move up reorders the bucket',(await titles('Biz'))[0]==='Big important');ok('impact: and leaves the list file alone',file===before); + await bucket('Biz').locator('button:has-text("Reset order")').click();await sleep(300);ok('impact: reset goes back to impact order',(await titles('Biz'))[0]==='Quick important');} + {const before=file;await drag('Biz six',bucket('Biz').locator('[data-drag-id="t2"]'));await sleep(500);ok('impact: drag within a bucket reorders the view only',(await titles('Biz'))[0]==='Biz six'&&file===before);await bucket('Biz').locator('button:has-text("Reset order")').click();await sleep(300);} + await bucket('Biz').locator('[data-drag-id="t3"] select.move-to').selectOption('Home');await sleep(700);ok('impact: Move to project changes the category',file.includes('- [ ] Quick minor · #Home · ~10m · impact:low · id:t3'));ok('impact: and it shows in that bucket',(await titles('Home')).includes('Quick minor')); + await drag('Big minor',bucket('Home').locator('[data-drag-id="t5"]'));await sleep(700);ok('impact: dragging to another bucket changes the category',file.includes('- [ ] Big minor · #Home · ~2h · impact:low · id:t7')); + await bucket('Biz').locator('[data-drag-id="t1"] .check').click();await sleep(3900);ok('impact: checked off goes to Done in the file',/- \[x\] Big important · #Biz[^\n]*done \d+\/\d+/.test(file)); + ok('impact: it stays in its place, marked completed',(await titles('Biz'))[1]==='Big important'&&await bucket('Biz').locator('text=Completed').isVisible()); + ok('impact: the next one waits to be asked for',(await titles('Biz')).length===5&&await bucket('Biz').locator('text=Add the next one?').isVisible()); + await p.screenshot({path:OUT+'/impact-completed.png'}); + await bucket('Biz').locator('.impact-next button:has-text("Add")').click();await sleep(300);ok('impact: Add puts it at the bottom',(await titles('Biz')).length===6&&!(await bucket('Biz').locator('text=Add the next one?').isVisible())); + await bucket('Biz').locator('.is-done .check').click();await sleep(700);ok('impact: unchecking reopens it',/- \[ \] Big important · #Biz/.test(file)); + await p.click('.segmented >> text=List');await p.waitForSelector('[data-drag-id="t2"]');ok('impact: the List view is still there',await p.isVisible('[data-drag-id="t2"]'));ok('impact: ratings show as a chip there',await p.locator('[data-drag-id="t2"] .chip-impact').isVisible()); + await sleep(100);ok('impact: no page errors',errs.length===0);if(errs.length)console.log(errs.slice(0,3));} await b.close();srv.close(); diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index fefa964..8c310fd 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,6 +1,7 @@ import { invoke } from '@tauri-apps/api/core' import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { ImpactView } from './ImpactUI' import { inlineParts } from './inline' import { Block, ListTools, useListActions, type ListActions } from './ListUI' import { commsSection, dropEmoji, nowSection } from './master' @@ -139,13 +140,16 @@ export default function App() { ) } +type ListView = 'list' | 'impact' | 'markdown' + function ListPage({ m, a }: { m: Master; a: ListActions }) { - const [raw, setRaw] = useState(() => { - try { return localStorage.getItem('theseus.listView') === 'markdown' } catch { return false } + const [view, setView] = useState(() => { + try { const v = localStorage.getItem('theseus.listView'); return v === 'markdown' || v === 'impact' ? v : 'list' } catch { return 'list' } }) - const choose = (v: boolean) => { - setRaw(v) - try { localStorage.setItem('theseus.listView', v ? 'markdown' : 'list') } catch { /* it's only a convenience */ } + const raw = view === 'markdown' + const choose = (v: ListView) => { + setView(v) + try { localStorage.setItem('theseus.listView', v) } catch { /* it's only a convenience */ } } const closedAtFirst = new Set(['eventually', 'done', 'projects', 'figure out']) return ( @@ -153,12 +157,15 @@ function ListPage({ m, a }: { m: Master; a: ListActions }) {

List

-
Drag to reorder. Double-click to rename. Everything saves as you go.
+
{view === 'impact' + ? 'Each project’s top five, easiest wins first. Rate impact with one click; effort comes from the size estimate.' + : 'Drag to reorder. Double-click to rename. Everything saves as you go.'}
- - + + +
{m.notice &&
{m.notice}
} @@ -166,7 +173,8 @@ function ListPage({ m, a }: { m: Master; a: ListActions }) { {raw && m.text !== null && (