From d7130b8c6783dc6ea60ae35da5d8915fdef38987 Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:58:51 +0200 Subject: [PATCH] Format long WebUI durations with hours Add hour-aware output to the shared duration formatter while preserving existing seconds and minutes formatting. Reuse it for the goal elapsed counter and cover both formats with a focused regression test. --- plugins/_goal/tests/test_goal_plugin.py | 20 ++++++++++++++++++++ plugins/_goal/webui/goal-store.js | 9 ++------- webui/js/time-utils.js | 7 ++++--- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/plugins/_goal/tests/test_goal_plugin.py b/plugins/_goal/tests/test_goal_plugin.py index 9732b5921..9214e3d96 100644 --- a/plugins/_goal/tests/test_goal_plugin.py +++ b/plugins/_goal/tests/test_goal_plugin.py @@ -1,5 +1,8 @@ from __future__ import annotations +import base64 +import shutil +import subprocess import uuid from pathlib import Path from types import SimpleNamespace @@ -108,6 +111,23 @@ def test_goal_webui_uses_state_revisions_instead_of_polling(): assert "goalStore.refresh(true)" in refresh +@pytest.mark.skipif(not shutil.which("node"), reason="node is required") +def test_goal_webui_uses_shared_hour_aware_duration_formatter(): + project_root = Path(__file__).resolve().parents[3] + time_utils = (project_root / "webui" / "js" / "time-utils.js").read_bytes() + module_url = "data:text/javascript;base64," + base64.b64encode(time_utils).decode("ascii") + script = f""" +import {{ formatDuration }} from {module_url!r}; +if (formatDuration(3_782_000) !== "1h3m2s") throw new Error("hours"); +if (formatDuration(62_000) !== "1m2s") throw new Error("minutes"); +""" + subprocess.run(["node", "--input-type=module", "-e", script], check=True) + + store = (project_root / "plugins" / "_goal" / "webui" / "goal-store.js").read_text() + assert 'import { formatDuration } from "/js/time-utils.js";' in store + assert "return formatDuration(this.elapsedSeconds * 1000);" in store + + def test_goal_command_sets_pauses_resumes_and_deletes(context_id: str): created = goal_command.run(_payload(context_id, "/goal Add current goal support")) assert created["effects"][0]["message"] == "Goal set." diff --git a/plugins/_goal/webui/goal-store.js b/plugins/_goal/webui/goal-store.js index e55d6ece4..78f8a8a0c 100644 --- a/plugins/_goal/webui/goal-store.js +++ b/plugins/_goal/webui/goal-store.js @@ -1,5 +1,6 @@ import { createStore } from "/js/AlpineStore.js"; import { callJsonApi } from "/js/api.js"; +import { formatDuration } from "/js/time-utils.js"; import { store as chatsStore } from "/components/sidebar/chats/chats-store.js"; import { toastFrontendError, @@ -68,13 +69,7 @@ const model = { }, get elapsedLabel() { - const seconds = this.elapsedSeconds; - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const remainingSeconds = seconds % 60; - if (hours) return `${hours}h ${minutes}m`; - if (minutes) return `${minutes}m ${remainingSeconds}s`; - return `${remainingSeconds}s`; + return formatDuration(this.elapsedSeconds * 1000); }, onMount() { diff --git a/webui/js/time-utils.js b/webui/js/time-utils.js index a59dc7cfe..c65fb66c8 100644 --- a/webui/js/time-utils.js +++ b/webui/js/time-utils.js @@ -292,7 +292,7 @@ export function withUserTimeFormatOptions(options = {}) { /** * Format a duration in milliseconds to a human-readable string * @param {number} durationMs - Duration in milliseconds - * @returns {string} Formatted duration (e.g., '45s', '2m30s') + * @returns {string} Formatted duration (e.g., '45s', '2m30s', '1h3m2s') */ export function formatDuration(durationMs) { if (durationMs == null || durationMs < 0) return '0s'; @@ -304,7 +304,8 @@ export function formatDuration(durationMs) { return `${totalSecs}s`; } - const mins = Math.floor(totalSecs / 60); + const hours = Math.floor(totalSecs / 3600); + const mins = Math.floor((totalSecs % 3600) / 60); const secs = totalSecs % 60; - return `${mins}m${secs}s`; + return hours ? `${hours}h${mins}m${secs}s` : `${mins}m${secs}s`; }