···11+-- Update active_todos view to include urgency calculation and sort by urgency
22+DROP VIEW IF EXISTS active_todos;
33+44+CREATE VIEW IF NOT EXISTS active_todos AS
55+SELECT
66+ id,
77+ description,
88+ project,
99+ tags,
1010+ due,
1111+ wait,
1212+ priority,
1313+ -- Calculate urgency score
1414+ (
1515+ -- Priority: +6 points if priority = '1'
1616+ CASE WHEN priority = '1' THEN 6 ELSE 0 END +
1717+1818+ -- Due date urgency
1919+ CASE
2020+ WHEN due != '' AND date(due) < date('now') THEN 12 -- Overdue
2121+ WHEN due != '' AND date(due) = date('now') THEN 8 -- Due today
2222+ WHEN due != '' AND date(due) <= date('now', '+7 days') THEN 5 -- Due this week
2323+ WHEN due != '' AND date(due) <= date('now', '+30 days') THEN 2 -- Due this month
2424+ ELSE 0
2525+ END +
2626+2727+ -- Project: +1 if has project
2828+ CASE WHEN project != '' THEN 1 ELSE 0 END +
2929+3030+ -- Tags: +1 if has tags
3131+ CASE WHEN tags != '[]' AND tags != '' THEN 1 ELSE 0 END
3232+3333+ ) as urgency,
3434+ -- working_id based on urgency-sorted order
3535+ ROW_NUMBER() OVER (ORDER BY
3636+ (
3737+ CASE WHEN priority = '1' THEN 6 ELSE 0 END +
3838+ CASE
3939+ WHEN due != '' AND date(due) < date('now') THEN 12
4040+ WHEN due != '' AND date(due) = date('now') THEN 8
4141+ WHEN due != '' AND date(due) <= date('now', '+7 days') THEN 5
4242+ WHEN due != '' AND date(due) <= date('now', '+30 days') THEN 2
4343+ ELSE 0
4444+ END +
4545+ CASE WHEN project != '' THEN 1 ELSE 0 END +
4646+ CASE WHEN tags != '[]' AND tags != '' THEN 1 ELSE 0 END
4747+ ) DESC,
4848+ id
4949+ ) as working_id
5050+FROM todos
5151+WHERE completed = 0;
+58-8
mast-react-vite/src/App.tsx
···11-import { useState, useEffect, useMemo } from "react";
11+import { useState, useEffect, useMemo, useRef } from "react";
22import { useQuery } from "@vlcn.io/react";
33import { DataTable } from "@/components/ui/data-table";
44import * as commandParser from "@/lib/command_js.js";
55import { ActionParser } from "@/components/ui/action-parser";
66import { useSelection } from "@/contexts/selection-context";
77import { useFilter } from "@/contexts/filter-context";
88+import { sortByUrgency } from "@/lib/urgency";
89import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
910import { AppSidebar } from "@/components/ui/app-sidebar";
1011import { Badge } from "@/components/ui/badge";
···4546 console.log("updating todos: ", whereClause, "newParams: ", newParams);
4647 const rawTodos = useQuery(
4748 ctx,
4848- `SELECT * FROM active_todos ${whereClause ? "WHERE " + whereClause : ""}`,
4949+ `SELECT * FROM urgent_todos ${whereClause ? "WHERE " + whereClause : ""}`,
4950 newParams,
5051 ).data;
5152···105106 completed: 2, // preview-add state
106107 working_id: 0,
107108 };
108108- return [newTodo, ...todos];
109109+ // Sort by urgency to show where the new todo would appear
110110+ const todosWithNew = [...todos, newTodo];
111111+ const sorted = sortByUrgency(todosWithNew);
112112+ // Update working_id based on new sort order
113113+ return sorted.map((todo, index) => ({
114114+ ...todo,
115115+ working_id: index + 1
116116+ }));
109117110118 case "done":
111111- return todos.map((todo) => {
119119+ const doneTodos = todos.map((todo) => {
112120 if (selectedItems.has(todo.working_id)) {
113121 return { ...todo, completed: 3 }; // preview-done state
114122 }
115123 return todo;
116124 });
125125+ // Sort by urgency and update working_id
126126+ const sortedDone = sortByUrgency(doneTodos);
127127+ return sortedDone.map((todo, index) => ({
128128+ ...todo,
129129+ working_id: index + 1
130130+ }));
117131118132 case "modify":
119119- return todos.map((todo) => {
133133+ const modifiedTodos = todos.map((todo) => {
120134 if (selectedItems.has(todo.working_id)) {
121135 // Create a preview object that contains the original todo and the changes
122136 const previewChanges = {};
···154168 }
155169 return todo;
156170 });
171171+ // Sort by urgency and update working_id
172172+ const sortedModified = sortByUrgency(modifiedTodos);
173173+ return sortedModified.map((todo, index) => ({
174174+ ...todo,
175175+ working_id: index + 1
176176+ }));
157177158178159179 case "filter":
···237257238258 // Use previewTodos if available, otherwise use regular todos
239259 const displayTodos = previewTodos.length > 0 ? previewTodos : todos;
260260+261261+ // Calculate scroll position for preview todos
262262+ const prevNewTextRef = useRef("");
263263+ const prevPreviewPositionRef = useRef(-1);
264264+265265+ const scrollToPreviewIndex = useMemo(() => {
266266+ if (previewTodos.length === 0 || parsedCommand.action !== "add") {
267267+ return undefined;
268268+ }
269269+270270+ // Find the preview todo (the one with completed: 2)
271271+ const previewIndex = previewTodos.findIndex(todo => todo.completed === 2);
272272+273273+ if (previewIndex === -1) {
274274+ return undefined;
275275+ }
276276+277277+ // Check if we should scroll:
278278+ // 1. If we just started typing (previous text was empty)
279279+ // 2. If the preview position changed significantly
280280+ const justStartedTyping = prevNewTextRef.current === "" && newText !== "";
281281+ const positionChanged = Math.abs(previewIndex - prevPreviewPositionRef.current) > 1;
282282+283283+ // Update refs for next comparison
284284+ const shouldScroll = justStartedTyping || positionChanged;
285285+ prevNewTextRef.current = newText;
286286+ prevPreviewPositionRef.current = previewIndex;
287287+288288+ return shouldScroll ? previewIndex : undefined;
289289+ }, [previewTodos, parsedCommand.action, newText]);
240290241291 const handleEnter = (e) => {
242292 if (e.key === "Enter") {
···249299 if (sel.type === "id") {
250300 conditions.push(`id IN (
251301 SELECT id
252252- FROM active_todos
302302+ FROM urgent_todos
253303 WHERE working_id IN (${sel.ids.map(() => "?").join(",")})
254304 )`);
255305 params.push(...sel.ids);
···426476 />
427477 <div className="h-2" />
428478 <div className="flex-1 w-full">
429429- <DataTable data={displayTodos} onMarkDone={handleMarkDone} />
479479+ <DataTable data={displayTodos} onMarkDone={handleMarkDone} scrollToPreviewIndex={scrollToPreviewIndex} />
430480 </div>
431481 </section>
432482 </div>
···466516 />
467517 <div className="h-2" />
468518 <div className="flex-1 w-full h-full">
469469- <DataTable data={displayTodos} onMarkDone={handleMarkDone} />
519519+ <DataTable data={displayTodos} onMarkDone={handleMarkDone} scrollToPreviewIndex={scrollToPreviewIndex} />
470520 </div>
471521 </section>
472522 </div>
+2-2
mast-react-vite/src/components/ui/app-sidebar.tsx
···8080 };
8181 const projects = useQuery(
8282 ctx,
8383- `SELECT DISTINCT project FROM active_todos WHERE project != '' ORDER BY project`
8383+ `SELECT DISTINCT project FROM urgent_todos WHERE project != '' ORDER BY project`
8484 ).data || [];
85858686 const handleProjectClick = (project, e) => {
···101101102102 const tags = useQuery(
103103 ctx,
104104- `SELECT DISTINCT value as tag FROM active_todos, json_each(active_todos.tags) ORDER BY tag`
104104+ `SELECT DISTINCT value as tag FROM urgent_todos, json_each(urgent_todos.tags) ORDER BY tag`
105105 ).data || [];
106106107107 const handleTagClick = (tag, e) => {
+32-4
mast-react-vite/src/components/ui/data-table.tsx
···11"use client";
2233-import { useState } from "react";
33+import { useState, useRef, useEffect } from "react";
44import { ScrollArea } from "@/components/ui/scroll-area";
55import { Task } from "@/components/ui/task";
66···1313interface DataTableProps<TData> {
1414 data: TData[];
1515 onMarkDone?: (selectedIds: number[]) => void;
1616+ scrollToPreviewIndex?: number; // Index of item to scroll to (for preview positioning)
1617}
17181818-export function DataTable<TData>({ data, onMarkDone }: DataTableProps<TData>) {
1919+export function DataTable<TData>({ data, onMarkDone, scrollToPreviewIndex }: DataTableProps<TData>) {
2020+ const desktopScrollRef = useRef<HTMLDivElement>(null);
2121+ const mobileScrollRef = useRef<HTMLDivElement>(null);
2222+2323+ // Scroll to preview position when scrollToPreviewIndex changes
2424+ useEffect(() => {
2525+ if (scrollToPreviewIndex !== undefined && scrollToPreviewIndex >= 0) {
2626+ // Estimate item height (adjust based on your actual task height)
2727+ const estimatedItemHeight = 80; // Approximate height of a Task component
2828+ const scrollPosition = scrollToPreviewIndex * estimatedItemHeight;
2929+3030+ // Scroll both desktop and mobile areas (only one will be visible)
3131+ if (desktopScrollRef.current) {
3232+ const viewport = desktopScrollRef.current.querySelector('[data-radix-scroll-area-viewport]');
3333+ if (viewport) {
3434+ viewport.scrollTo({ top: scrollPosition, behavior: 'smooth' });
3535+ }
3636+ }
3737+3838+ if (mobileScrollRef.current) {
3939+ const viewport = mobileScrollRef.current.querySelector('[data-radix-scroll-area-viewport]');
4040+ if (viewport) {
4141+ viewport.scrollTo({ top: scrollPosition, behavior: 'smooth' });
4242+ }
4343+ }
4444+ }
4545+ }, [scrollToPreviewIndex]);
4646+1947 return (
2048 <>
2149 <div className="hidden md:flex flex-col w-full">
2222- <ScrollArea className="h-[calc(100vh-12rem)] rounded-md border">
5050+ <ScrollArea ref={desktopScrollRef} className="h-[calc(100vh-12rem)] rounded-md border">
2351 {data.length ? (
2452 data.map((item, index) => <Task key={index} data={item} onMarkDone={onMarkDone} />)
2553 ) : (
···32603361 {/* Mobile view layout - shown only on mobile */}
3462 <div className="md:hidden">
3535- <ScrollArea className="min-h-[120%] h-[calc(100vh)] border">
6363+ <ScrollArea ref={mobileScrollRef} className="min-h-[120%] h-[calc(100vh)] border">
3664 {data.length ? (
3765 data.map((item, index) => <Task key={index} data={item} onMarkDone={onMarkDone} />)
3866 ) : (
+72
mast-react-vite/src/lib/urgency.ts
···11+// Urgency calculation that matches the SQL logic in urgent_todos view
22+export function calculateUrgency(todo: {
33+ priority?: string | boolean;
44+ due?: string | null;
55+ project?: string;
66+ tags?: string;
77+}): number {
88+ let urgency = 0;
99+1010+ // Priority: +6 points if priority = '1' or true
1111+ if (todo.priority === '1' || todo.priority === true) {
1212+ urgency += 6;
1313+ }
1414+1515+ // Due date urgency
1616+ if (todo.due && todo.due !== '') {
1717+ const dueDate = new Date(todo.due);
1818+ const now = new Date();
1919+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
2020+ const todayTime = today.getTime();
2121+ const dueDateDay = new Date(dueDate.getFullYear(), dueDate.getMonth(), dueDate.getDate());
2222+ const dueDateTime = dueDateDay.getTime();
2323+2424+ if (dueDateTime < todayTime) {
2525+ urgency += 12; // Overdue
2626+ } else if (dueDateTime === todayTime) {
2727+ urgency += 8; // Due today
2828+ } else if (dueDateTime <= todayTime + (7 * 24 * 60 * 60 * 1000)) {
2929+ urgency += 5; // Due this week
3030+ } else if (dueDateTime <= todayTime + (30 * 24 * 60 * 60 * 1000)) {
3131+ urgency += 2; // Due this month
3232+ }
3333+ }
3434+3535+ // Project: +1 if has project
3636+ if (todo.project && todo.project !== '') {
3737+ urgency += 1;
3838+ }
3939+4040+ // Tags: +1 if has tags
4141+ if (todo.tags && todo.tags !== '[]' && todo.tags !== '') {
4242+ try {
4343+ const parsedTags = JSON.parse(todo.tags);
4444+ if (Array.isArray(parsedTags) && parsedTags.length > 0) {
4545+ urgency += 1;
4646+ }
4747+ } catch {
4848+ // If tags is not valid JSON, assume it's a non-empty string
4949+ urgency += 1;
5050+ }
5151+ }
5252+5353+ return urgency;
5454+}
5555+5656+// Sort todos by urgency (descending) then by id
5757+export function sortByUrgency<T extends { id: string | number; priority?: string | boolean; due?: string | null; project?: string; tags?: string }>(todos: T[]): T[] {
5858+ return [...todos].sort((a, b) => {
5959+ const urgencyA = calculateUrgency(a);
6060+ const urgencyB = calculateUrgency(b);
6161+6262+ if (urgencyA !== urgencyB) {
6363+ return urgencyB - urgencyA; // Higher urgency first
6464+ }
6565+6666+ // If urgency is the same, sort by id
6767+ if (typeof a.id === 'string' && typeof b.id === 'string') {
6868+ return a.id.localeCompare(b.id);
6969+ }
7070+ return Number(a.id) - Number(b.id);
7171+ });
7272+}
+49
mast-react-vite/src/main.tsx
···339339 urgency
340340 FROM todos
341341 WHERE completed = 0;
342342+343343+ CREATE VIEW IF NOT EXISTS urgent_todos AS
344344+ SELECT
345345+ id,
346346+ description,
347347+ project,
348348+ tags,
349349+ due,
350350+ wait,
351351+ priority,
352352+ -- Calculate urgency score
353353+ (
354354+ -- Priority: +6 points if priority = '1'
355355+ CASE WHEN priority = '1' THEN 6 ELSE 0 END +
356356+357357+ -- Due date urgency
358358+ CASE
359359+ WHEN due != '' AND date(due) < date('now') THEN 12 -- Overdue
360360+ WHEN due != '' AND date(due) = date('now') THEN 8 -- Due today
361361+ WHEN due != '' AND date(due) <= date('now', '+7 days') THEN 5 -- Due this week
362362+ WHEN due != '' AND date(due) <= date('now', '+30 days') THEN 2 -- Due this month
363363+ ELSE 0
364364+ END +
365365+366366+ -- Project: +1 if has project
367367+ CASE WHEN project != '' THEN 1 ELSE 0 END +
368368+369369+ -- Tags: +1 if has tags
370370+ CASE WHEN tags != '[]' AND tags != '' THEN 1 ELSE 0 END
371371+372372+ ) as urgency,
373373+ -- working_id based on urgency-sorted order
374374+ ROW_NUMBER() OVER (ORDER BY
375375+ (
376376+ CASE WHEN priority = '1' THEN 6 ELSE 0 END +
377377+ CASE
378378+ WHEN due != '' AND date(due) < date('now') THEN 12
379379+ WHEN due != '' AND date(due) = date('now') THEN 8
380380+ WHEN due != '' AND date(due) <= date('now', '+7 days') THEN 5
381381+ WHEN due != '' AND date(due) <= date('now', '+30 days') THEN 2
382382+ ELSE 0
383383+ END +
384384+ CASE WHEN project != '' THEN 1 ELSE 0 END +
385385+ CASE WHEN tags != '[]' AND tags != '' THEN 1 ELSE 0 END
386386+ ) DESC,
387387+ id
388388+ ) as working_id
389389+ FROM todos
390390+ WHERE completed = 0;
342391 `);
343392 const rx = tblrx(db);
344393