From 48f75cff051eaff18f35a35a816ec682638d7cac Mon Sep 17 00:00:00 2001 From: timmy Date: Wed, 12 Aug 2026 11:26:35 +0000 Subject: [PATCH] feat: launch work directly from mobile queues (Closes #645) --- frontend/dashboard.js | 10 +++++++-- frontend/index.html | 1 + frontend/mobile-queue-launcher.js | 25 +++++++++++++++++++++ frontend/service-worker.js | 1 + tests/test_mobile_task_dock.py | 36 +++++++++++++++++++++++++++++-- tests/test_service_worker.py | 1 + 6 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 frontend/mobile-queue-launcher.js diff --git a/frontend/dashboard.js b/frontend/dashboard.js index f6d9fd4..7ef8d6f 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -56,7 +56,7 @@ qs('#my-work').scrollIntoView({block:'start'}); qs('#my-work').focus(); } - function openMobileQueue(name) { + function selectMobileQueue(name) { if (name === 'attention' && workSession.checkpointed()) timer.beginAttention(); renderAttentionInterruption(); timerView.render(); @@ -75,6 +75,12 @@ planToday: () => openPlanToday(mobileTaskButtons.work), openFallback: openMobileWorkFallback, }); + const mobileQueueLauncher = createMobileQueueLauncher({ + openToday: () => mobileWorkEntry.open(), + selectFilter: selectMobileQueue, + firstAction: name => qs('#my-work-list .my-work-card-main, #my-work-list .draft-resume, #my-work-list .draft-continue, #my-work-list .draft-edit'), + announce: message => { qs('#my-work-action-status').textContent = message; }, + }); const mobileTaskDock = createMobileTaskDock({ nav: qs('#mobile-task-dock'), sessionHud: qs('[data-mobile-today-hud]'), @@ -89,7 +95,7 @@ Array.from(document.querySelectorAll('[data-mobile-queue-count]')).map(element => [element.dataset.mobileQueueCount, element]) ), queueBadge: qs('#mobile-queue-count'), - onSelectQueue: openMobileQueue, + onSelectQueue: name => mobileQueueLauncher.open(name), overlays: mobileTaskOverlays, actions: { work: () => mobileWorkEntry.open(), diff --git a/frontend/index.html b/frontend/index.html index 5da019f..ef3aa98 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -934,6 +934,7 @@ + diff --git a/frontend/mobile-queue-launcher.js b/frontend/mobile-queue-launcher.js new file mode 100644 index 0000000..8cf7faf --- /dev/null +++ b/frontend/mobile-queue-launcher.js @@ -0,0 +1,25 @@ +(function (root, factory) { + if (typeof module === 'object' && module.exports) module.exports = factory; + else root.createMobileQueueLauncher = factory; +})(typeof self !== 'undefined' ? self : this, function createMobileQueueLauncher(options) { + const emptyMessages = { + attention: 'No items need attention.', + update: 'No unread updates are ready to open.', + later: 'No deferred work is ready to open.', + draft: 'No drafts are ready to open.', + }; + + function open(name) { + if (name === 'today') return options.openToday(); + options.selectFilter(name); + const action = options.firstAction(name); + if (!action) { + options.announce(emptyMessages[name] || 'No work is ready to open.'); + return 'empty'; + } + action.click(); + return 'opened'; + } + + return { open }; +}); diff --git a/frontend/service-worker.js b/frontend/service-worker.js index 2fa4270..9edcab1 100644 --- a/frontend/service-worker.js +++ b/frontend/service-worker.js @@ -67,6 +67,7 @@ const SHELL = [ BASE + 'static/context-poller.js', BASE + 'static/mobile-task-dock.js', BASE + 'static/mobile-work-entry.js', + BASE + 'static/mobile-queue-launcher.js', BASE + 'static/mobile-launch.js', BASE + 'static/install-app.js', BASE + 'static/mobile-device-setup.js', diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py index 0e88dfd..895a683 100644 --- a/tests/test_mobile_task_dock.py +++ b/tests/test_mobile_task_dock.py @@ -9,6 +9,7 @@ from tests.dashboard_bundle import dashboard DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js" ENTRY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-work-entry.js" +QUEUE_LAUNCHER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queue-launcher.js" TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js" @@ -334,6 +335,34 @@ process.stdout.write(JSON.stringify({{ } +def test_mobile_queue_launcher_opens_first_actionable_item_after_selecting_queue(): + script = f""" +const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))}); +const calls = []; +const launcher = createLauncher({{ + selectFilter: name => calls.push('filter:' + name), + firstAction: name => name === 'attention' ? {{click() {{ calls.push('open:first'); }}}} : null, + announce: message => calls.push('announce:' + message), +}}); +const opened = launcher.open('attention'); +const empty = launcher.open('later'); +process.stdout.write(JSON.stringify({{opened, empty, calls}})); +""" + result = subprocess.run(["node", "-e", script], capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == { + "opened": "opened", + "empty": "empty", + "calls": [ + "filter:attention", + "open:first", + "filter:later", + "announce:No deferred work is ready to open.", + ], + } + + @pytest.mark.anyio async def test_dashboard_renders_and_wires_mobile_queue_switcher(): html = await dashboard() @@ -349,7 +378,10 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher(): assert 'data-mobile-queue="draft"' in html assert 'id="mobile-queue-count"' in html assert 'mobileTaskDock.updateQueues(counts)' in html - assert "onSelectQueue: openMobileQueue" in html + assert '' in html + assert "const mobileQueueLauncher = createMobileQueueLauncher({" in html + assert "firstAction: name => qs('#my-work-list .my-work-card-main, #my-work-list .draft-resume, #my-work-list .draft-continue, #my-work-list .draft-edit')" in html + assert "onSelectQueue: name => mobileQueueLauncher.open(name)" in html assert '.mobile-queue-sheet' in html assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html assert '.mobile-task-dock[data-attention="true"]' not in html @@ -394,7 +426,7 @@ async def test_dashboard_renders_and_wires_phone_safe_task_dock(): assert '' in html assert "createMobileTaskDock({" in html assert "work: () => mobileWorkEntry.open()" in html - assert "onSelectQueue: openMobileQueue" in html + assert "onSelectQueue: name => mobileQueueLauncher.open(name)" in html assert "qs('[data-work-filter=\"' + name + '\"]').click()" in html assert "find: () => qs('#find-work').click()" in html assert "new: () => qs('#new-issue').click()" in html diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py index 762a37e..1204daa 100644 --- a/tests/test_service_worker.py +++ b/tests/test_service_worker.py @@ -730,6 +730,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell(): "/dashboard/static/context-poller.js", "/dashboard/static/mobile-task-dock.js", "/dashboard/static/mobile-work-entry.js", + "/dashboard/static/mobile-queue-launcher.js", "/dashboard/static/mobile-launch.js", "/dashboard/static/install-app.js", "/dashboard/static/mobile-device-setup.js", -- 2.43.0