Skip to content

Commit 08d389e

Browse files
jgclarkcursoragent
andcommitted
Add Interactive Processing for Active Projects and Projects to Review.
Walk project rows via >> N with the existing project dialog; PROJACT allows back-navigate, PROJREVIEW is forward-only. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b0553ba commit 08d389e

9 files changed

Lines changed: 293 additions & 129 deletions

File tree

‎jgclark.Dashboard/README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ In sections with more than 1 item, a `>> N` button is available (where `N` is th
107107
Note:
108108
- you can break out from the sequence at any time by closing the dialog.
109109
- by default this only processes tasks that are currently shown -- so it won't process any ones of lower priority that you have hidden. If you turn off the setting 'Do "Move all items" buttons only move shown items when filtering?', then `>> N` and Interactive Processing include those hidden lower-priority items as well.
110+
- for **Active Projects** and **Projects to Review**, `>> N` walks project rows only (not the next-action tasks under them). Active Projects allows skip back; Projects to Review is forward-only. **Start Reviews** is unchanged.
110111
- there are 3 settings that control aspects of this in the Dashboard Settings dialog.
111112

112113
### Add Task/Checklist items
@@ -253,6 +254,8 @@ The actions available in the Edit dialog for this section are:
253254

254255
The 'Start Reviews' button does the same as the button of the same name in the Project & Reviews plugin, and is the equivalent of its **/start reviews** command. See the documentation for how that works, and which commands to follow it with once you've done reviewed the note.
255256

257+
Both project sections also support **Interactive Processing** (the `>> N` button) when there is more than one project row: same project edit dialog, walked one project at a time. See [Interactive Processing Dialog](#interactive-processing-dialog) above.
258+
256259
### Overdue section
257260
This finds open items with a schedule date (e.g. `>2025-01-22`) in the past. This can generate a lot of tasks, and take a while, so there's a setting "Number of days to look back for Overdue tasks", which if set to any number > 0, will filter by due date (if set) or on date of a calendar note.
258261

‎jgclark.Dashboard/src/constants.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//-----------------------------------------------------------------------------
33
// Constants for Dashboard code.
44
// Check each of them when adding a new Section.
5-
// Last updated 2026-08-01 for v2.4.0.b59, @jgclark + @CursorAI
5+
// Last updated 2026-08-21 for v2.5.0, @jgclark + @CursorAI
66
//-----------------------------------------------------------------------------
77
import pluginJson from '../plugin.json'
88
import type { TSectionDetails, TSectionCode } from './types'
@@ -63,7 +63,7 @@ export const sectionsPriorityBeforeTagWhenCalendarFocus: Array<TSectionCode> = [
6363
export const dontDedupeSectionCodes = ['INFO', 'PROJACT', 'PROJREVIEW', 'SEARCH', 'SAVEDSEARCH']
6464

6565
// Enable interactive processing for these itemTypes:
66-
export const interactiveProcessingPossibleSectionTypes = ['DT', 'DY', 'DO', 'LW', 'W', 'M', 'Q', 'Y', 'TAG', 'OVERDUE', 'PRIORITY', 'REM']
66+
export const interactiveProcessingPossibleSectionTypes = ['DT', 'DY', 'DO', 'LW', 'W', 'M', 'Q', 'Y', 'TAG', 'OVERDUE', 'PRIORITY', 'REM', 'PROJACT', 'PROJREVIEW']
6767

6868
// When the user toggles visibility of a calendar period section only, refresh these sections (if enabled).
6969
// This ensures Wins / Priority / Overdue deduping stays correct.

‎jgclark.Dashboard/src/react/components/Dialog.jsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Props = {
2727
/**
2828
* Display a Dialog for a Task, Reminder, or Project if reactSettings.dialogData.isOpen is true.
2929
* Reminder vs task is chosen from details.item.itemType so Interactive Processing can switch mid-session (#779).
30+
* Project dialogs use isTask false (PROJACT / PROJREVIEW IP).
3031
* @param {Props} props The properties for the Dialog component.
3132
* @return {?React$Node} Renderable React node or null.
3233
*/

‎jgclark.Dashboard/src/react/components/DialogForProjectItems.jsx‎

Lines changed: 185 additions & 97 deletions
Large diffs are not rendered by default.

‎jgclark.Dashboard/src/react/components/Section/Section.jsx‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//--------------------------------------------------------------------------
33
// Dashboard React component to show a whole Dashboard Section
44
// Called by Dashboard component.
5-
// Last updated 2026-08-21 for v2.4.1 by @jgclark + @CursorAI
5+
// Last updated 2026-08-21 for v2.5.0, @jgclark + @CursorAI
66
//--------------------------------------------------------------------------
77

88
//--------------------------------------------------------------------------
@@ -369,22 +369,25 @@ const Section = ({ section, onButtonClick, isViewVisible = true }: SectionProps)
369369

370370
// handle a click to start interactive processing
371371
// When moveOnlyShownItemsWhenFiltered is false, include lower-priority (and limit-hidden) items, not just those currently shown.
372-
// Count and start list both use getInteractiveProcessingItems (open/checklist/reminder) (#779)
372+
// Count and start list both use getInteractiveProcessingItems (open/checklist/reminder/project)
373373
const handleInteractiveProcessingClick = useCallback(
374374
(e: MouseEvent): void => {
375375
const moveOnlyShownItemsWhenFiltered = dashboardSettings?.moveOnlyShownItemsWhenFiltered ?? true
376376
const sourceItems = moveOnlyShownItemsWhenFiltered ? itemsToShow : allSortedItems
377-
const processableItems = getInteractiveProcessingItems(sourceItems)
377+
const processableItems = getInteractiveProcessingItems(sourceItems, section.sectionCode)
378378
if (processableItems.length === 0) {
379379
logDebug(
380380
'Section',
381-
`Interactive Processing click on ${section.sectionCode}: no processable open/checklist/reminder items (source had ${String(sourceItems.length)} row(s))`,
381+
`Interactive Processing click on ${section.sectionCode}: no processable open/checklist/reminder/project items (source had ${String(sourceItems.length)} row(s))`,
382382
)
383383
return
384384
}
385385

386+
const firstItem = processableItems[0]
386387
const clickPosition = { clientY: e.clientY, clientX: e.clientX + 200 }
387-
const itemDetails = { actionType: '', item: processableItems[0], sectionCodes: [section.sectionCode] }
388+
const itemDetails = { actionType: '', item: firstItem, sectionCodes: [section.sectionCode] }
389+
// Project rows need isTask false so Dialog.jsx routes to DialogForProjectItems
390+
const isTask = firstItem.itemType !== 'project'
388391
setReactSettings((prevSettings) => {
389392
const newReactSettings = {
390393
...prevSettings,
@@ -396,7 +399,7 @@ const Section = ({ section, onButtonClick, isViewVisible = true }: SectionProps)
396399
visibleItems: [...processableItems],
397400
clickPosition,
398401
},
399-
dialogData: { isOpen: true, isTask: true, details: itemDetails, clickPosition },
402+
dialogData: { isOpen: true, isTask, details: itemDetails, clickPosition },
400403
}
401404
return newReactSettings
402405
})
@@ -603,12 +606,11 @@ const Section = ({ section, onButtonClick, isViewVisible = true }: SectionProps)
603606
) : null
604607

605608
// Decide whether to show interactiveProcessing button:
606-
// - N and the start list use the same processable filter (open/checklist/reminder) (#779)
609+
// - N and the start list use the same processable filter (open/checklist/reminder/project)
607610
// - when moveOnlyShownItemsWhenFiltered, source is visible rows; otherwise allSortedItems (incl. priority/limit-hidden)
608-
// - hide when there is nothing actionable for the chosen mode (e.g. all tasks priority-filtered and only-shown on)
609-
// - TODO(later): enable for PROJREVIEW/PROJACT
611+
// - hide when there is nothing actionable for the chosen mode (e.g. all tasks priority-filtered and only-shown on)
610612
const ipSourceItems = moveOnlyShownItemsWhenFiltered ? itemsToShow : allSortedItems
611-
const ipItemCount = countInteractiveProcessingItems(ipSourceItems)
613+
const ipItemCount = countInteractiveProcessingItems(ipSourceItems, section.sectionCode)
612614
const showIPButton =
613615
dashboardSettings.enableInteractiveProcessing &&
614616
interactiveProcessingPossibleSectionTypes.includes(section.sectionCode) &&

‎jgclark.Dashboard/src/react/components/Section/sectionHelpers.js‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22
//--------------------------------------------------------------------------
33
// Helpers for the Section component.
4-
// Last updated 2026-08-21 for v2.4.1 by @jgclark + @CursorAI
4+
// Last updated 2026-08-21 for v2.5.0, @jgclark + @CursorAI
55
//--------------------------------------------------------------------------
66

77
import type { TSection, TSectionItem, TDashboardSettings, TDashboardSettingsIn, TSectionCode, TSectionDetails, TSettingItem } from '../../../types.js'
@@ -23,31 +23,39 @@ export function countRealSectionItems(items: ?Array<TSectionItem>): number {
2323

2424
/**
2525
* Whether a section row can be walked by Interactive Processing.
26-
* Includes open tasks, checklists, and Apple Reminders (#779).
26+
* Includes open tasks, checklists, Apple Reminders (#779), and project rows (PROJACT/PROJREVIEW).
27+
* Callers for PROJ* must still restrict to `project` only via getInteractiveProcessingItems(sectionCode).
2728
* @param {TSectionItem} item
2829
* @returns {boolean}
2930
*/
3031
export function isInteractiveProcessingItem(item: TSectionItem): boolean {
31-
return item.itemType === 'open' || item.itemType === 'checklist' || item.itemType === 'reminder'
32+
return item.itemType === 'open' || item.itemType === 'checklist' || item.itemType === 'reminder' || item.itemType === 'project'
3233
}
3334

3435
/**
3536
* Rows Interactive Processing will actually process from a candidate list.
37+
* For PROJACT / PROJREVIEW, only `project` rows (not next-action task children).
3638
* @param {?Array<TSectionItem>} items
39+
* @param {?TSectionCode | string} sectionCode
3740
* @returns {Array<TSectionItem>}
3841
*/
39-
export function getInteractiveProcessingItems(items: ?Array<TSectionItem>): Array<TSectionItem> {
42+
export function getInteractiveProcessingItems(items: ?Array<TSectionItem>, sectionCode: ?TSectionCode | string = null): Array<TSectionItem> {
4043
if (!items || items.length === 0) return []
41-
return items.filter(isInteractiveProcessingItem)
44+
const processable = items.filter(isInteractiveProcessingItem)
45+
if (sectionCode === 'PROJACT' || sectionCode === 'PROJREVIEW') {
46+
return processable.filter((item) => item.itemType === 'project')
47+
}
48+
return processable
4249
}
4350

4451
/**
4552
* Count of Interactive Processing processable rows (must match getInteractiveProcessingItems).
4653
* @param {?Array<TSectionItem>} items
54+
* @param {?TSectionCode | string} sectionCode
4755
* @returns {number}
4856
*/
49-
export function countInteractiveProcessingItems(items: ?Array<TSectionItem>): number {
50-
return getInteractiveProcessingItems(items).length
57+
export function countInteractiveProcessingItems(items: ?Array<TSectionItem>, sectionCode: ?TSectionCode | string = null): number {
58+
return getInteractiveProcessingItems(items, sectionCode).length
5159
}
5260

5361
/**

‎jgclark.Dashboard/src/react/components/interactiveProcessingHelpers.js‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @flow
22
//--------------------------------------------------------------------------
3-
// Shared Interactive Processing helpers for task and reminder dialogs.
3+
// Shared Interactive Processing helpers for task, reminder, and project dialogs.
44
// Last updated 2026-08-21 for v2.4.1 by @CursorAI
55
//--------------------------------------------------------------------------
66

@@ -16,6 +16,16 @@ export type TInteractiveProcessingState = {
1616
clickPosition?: { clientX: number, clientY: number },
1717
}
1818

19+
/**
20+
* Whether Dialog.jsx should use the task/reminder slot (vs project dialog).
21+
* Reminders keep isTask true and are routed by itemType; projects need isTask false.
22+
* @param {?TIPVisibleItem | TSectionItem} item
23+
* @returns {boolean}
24+
*/
25+
export function isTaskDialogItem(item: ?TIPVisibleItem | TSectionItem): boolean {
26+
return item?.itemType !== 'project'
27+
}
28+
1929
/**
2030
* Next unprocessed index in the IP list (forward by default), or -1 if none remain.
2131
* @param {?Array<TIPVisibleItem>} visibleItems
@@ -97,14 +107,15 @@ export function buildReactSettingsForIPBackNavigate(prevSettings: any): any {
97107
if (!visibleItems || typeof currentIPIndex !== 'number' || currentIPIndex <= 0) return prevSettings
98108

99109
const newIPIndex = currentIPIndex - 1
110+
const nextItem = visibleItems[newIPIndex]
100111
return {
101112
...prevSettings,
102113
interactiveProcessing: { ...interactiveProcessing, currentIPIndex: newIPIndex },
103114
dialogData: {
104115
...prevSettings.dialogData,
105116
isOpen: true,
106-
isTask: true,
107-
details: { ...prevSettings.dialogData.details, item: visibleItems[newIPIndex] },
117+
isTask: isTaskDialogItem(nextItem),
118+
details: { ...prevSettings.dialogData.details, item: nextItem },
108119
},
109120
lastChange: `_Dashboard-handleIPBackNavigate to index ${String(newIPIndex)}`,
110121
}
@@ -131,15 +142,16 @@ export function buildReactSettingsAfterIPAdvance(
131142
const newIPIndex = getNextIPIndex(visibleItems, currentIPIndex, !skipForward)
132143

133144
if (newIPIndex !== -1) {
145+
const nextItem = visibleItems[newIPIndex]
134146
return {
135147
...prevSettings,
136148
interactiveProcessing: { ...interactiveProcessing, currentIPIndex: newIPIndex, visibleItems },
137149
dialogData: {
138150
...prevSettings.dialogData,
139151
isOpen: true,
140-
// Keep non-project dialogs in the task/reminder slot; Dialog.jsx routes by itemType
141-
isTask: true,
142-
details: { ...prevSettings.dialogData.details, item: visibleItems[newIPIndex] },
152+
// Project rows need isTask false; reminders keep true and Dialog.jsx routes by itemType
153+
isTask: isTaskDialogItem(nextItem),
154+
details: { ...prevSettings.dialogData.details, item: nextItem },
143155
},
144156
lastChange: `_Dashboard-handleIPItemProcessed more IP items to process`,
145157
}
@@ -151,7 +163,7 @@ export function buildReactSettingsAfterIPAdvance(
151163
dialogData: {
152164
...prevSettings.dialogData,
153165
isOpen: false,
154-
isTask: true,
166+
isTask: isTaskDialogItem(visibleItems[currentIPIndex]),
155167
},
156168
lastChange: `_Dashboard-handleIPItemProcessed no more IP items to process`,
157169
}

‎jgclark.Dashboard/src/react/components/testing/interactiveProcessingHelpers.test.js‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
canNavigateBackInIP,
1010
getNextIPIndex,
1111
ipItemsHaveBeenSkipped,
12+
isTaskDialogItem,
1213
markCurrentIPItemProcessed,
1314
} from '../interactiveProcessingHelpers.js'
1415

@@ -63,6 +64,15 @@ describe('interactiveProcessingHelpers', () => {
6364
})
6465
})
6566

67+
describe('isTaskDialogItem', () => {
68+
test('is false only for project rows', () => {
69+
expect(isTaskDialogItem({ ID: 'p', itemType: 'project' })).toBe(false)
70+
expect(isTaskDialogItem({ ID: 't', itemType: 'open' })).toBe(true)
71+
expect(isTaskDialogItem({ ID: 'r', itemType: 'reminder' })).toBe(true)
72+
expect(isTaskDialogItem(null)).toBe(true)
73+
})
74+
})
75+
6676
describe('canNavigateBackInIP / buildReactSettingsForIPBackNavigate', () => {
6777
test('canNavigateBackInIP is true from the second item', () => {
6878
expect(canNavigateBackInIP(0)).toBe(false)
@@ -80,10 +90,23 @@ describe('interactiveProcessingHelpers', () => {
8090
const next = buildReactSettingsForIPBackNavigate(prev)
8191
expect(next.interactiveProcessing.currentIPIndex).toBe(0)
8292
expect(next.dialogData.details.item).toEqual(a)
93+
expect(next.dialogData.isTask).toBe(true)
8394

8495
const atStart = { ...prev, interactiveProcessing: { ...prev.interactiveProcessing, currentIPIndex: 0 } }
8596
expect(buildReactSettingsForIPBackNavigate(atStart)).toBe(atStart)
8697
})
98+
99+
test('buildReactSettingsForIPBackNavigate sets isTask false for projects', () => {
100+
const a = { ID: 'a', itemType: 'project' }
101+
const b = { ID: 'b', itemType: 'project' }
102+
const prev = {
103+
interactiveProcessing: { currentIPIndex: 1, totalTasks: 2, visibleItems: [a, b] },
104+
dialogData: { isOpen: true, isTask: false, details: { item: b } },
105+
}
106+
const next = buildReactSettingsForIPBackNavigate(prev)
107+
expect(next.dialogData.isTask).toBe(false)
108+
expect(next.dialogData.details.item).toEqual(a)
109+
})
87110
})
88111

89112
describe('buildReactSettingsAfterIPAdvance', () => {
@@ -102,10 +125,28 @@ describe('interactiveProcessingHelpers', () => {
102125
expect(mid.interactiveProcessing.currentIPIndex).toBe(1)
103126
expect(mid.dialogData.details.item).toEqual(reminder)
104127
expect(mid.dialogData.isOpen).toBe(true)
128+
expect(mid.dialogData.isTask).toBe(true)
105129

106130
const end = buildReactSettingsAfterIPAdvance(mid, { skippedItem: false, skipForward: true })
107131
expect(end.interactiveProcessing).toBeNull()
108132
expect(end.dialogData.isOpen).toBe(false)
109133
})
134+
135+
test('keeps isTask false when advancing between project rows', () => {
136+
const p1 = { ID: 'p1', itemType: 'project' }
137+
const p2 = { ID: 'p2', itemType: 'project' }
138+
const prev = {
139+
interactiveProcessing: {
140+
currentIPIndex: 0,
141+
totalTasks: 2,
142+
visibleItems: [p1, p2],
143+
},
144+
dialogData: { isOpen: true, isTask: false, details: { item: p1 } },
145+
}
146+
const mid = buildReactSettingsAfterIPAdvance(prev, { skippedItem: false, skipForward: true, markTaskChildren: false })
147+
expect(mid.interactiveProcessing.currentIPIndex).toBe(1)
148+
expect(mid.dialogData.isTask).toBe(false)
149+
expect(mid.dialogData.details.item).toEqual(p2)
150+
})
110151
})
111152
})

‎jgclark.Dashboard/src/react/components/testing/sectionHelpers.test.js‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,34 @@ describe('sectionHelpers', () => {
2020
const openTask = { ID: '1', itemType: 'open' }
2121
const checklist = { ID: '2', itemType: 'checklist' }
2222
const reminder = { ID: '3', itemType: 'reminder' }
23+
const project = { ID: '6', itemType: 'project' }
2324
const congrats = { ID: '4', itemType: 'itemCongrats' }
2425
const filterMsg = { ID: '5', itemType: 'filterIndicator' }
2526

26-
test('isInteractiveProcessingItem is true for open, checklist, and reminder', () => {
27+
test('isInteractiveProcessingItem is true for open, checklist, reminder, and project', () => {
2728
expect(sh.isInteractiveProcessingItem(openTask)).toBe(true)
2829
expect(sh.isInteractiveProcessingItem(checklist)).toBe(true)
2930
expect(sh.isInteractiveProcessingItem(reminder)).toBe(true)
31+
expect(sh.isInteractiveProcessingItem(project)).toBe(true)
3032
expect(sh.isInteractiveProcessingItem(congrats)).toBe(false)
3133
expect(sh.isInteractiveProcessingItem(filterMsg)).toBe(false)
3234
})
3335

34-
test('getInteractiveProcessingItems keeps tasks and reminders; drops message rows', () => {
35-
const mixed = [openTask, reminder, checklist, congrats, filterMsg]
36-
expect(sh.getInteractiveProcessingItems(mixed)).toEqual([openTask, reminder, checklist])
36+
test('getInteractiveProcessingItems keeps tasks, reminders, and projects; drops message rows', () => {
37+
const mixed = [openTask, reminder, checklist, project, congrats, filterMsg]
38+
expect(sh.getInteractiveProcessingItems(mixed)).toEqual([openTask, reminder, checklist, project])
39+
})
40+
41+
test('getInteractiveProcessingItems for PROJ* keeps project rows only (not next-action tasks)', () => {
42+
const mixed = [project, openTask, checklist, congrats]
43+
expect(sh.getInteractiveProcessingItems(mixed, 'PROJACT')).toEqual([project])
44+
expect(sh.getInteractiveProcessingItems(mixed, 'PROJREVIEW')).toEqual([project])
45+
expect(sh.countInteractiveProcessingItems(mixed, 'PROJACT')).toBe(1)
3746
})
3847

3948
test('countInteractiveProcessingItems matches filtered length (IP button N)', () => {
40-
const mixed = [openTask, reminder, reminder, checklist]
41-
expect(sh.countInteractiveProcessingItems(mixed)).toBe(4)
49+
const mixed = [openTask, reminder, reminder, checklist, project]
50+
expect(sh.countInteractiveProcessingItems(mixed)).toBe(5)
4251
expect(sh.countInteractiveProcessingItems([])).toBe(0)
4352
expect(sh.countInteractiveProcessingItems(null)).toBe(0)
4453
expect(sh.countInteractiveProcessingItems([congrats, filterMsg])).toBe(0)

0 commit comments

Comments
 (0)