From 26f77c207b6961f5368e722f51a108dbf3ce6546 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Sat, 15 Nov 2025 12:15:07 +0100 Subject: [PATCH] feat: sort briefs by updated at (#1409) --- apps/cli/src/utils/brief-selection.ts | 21 +++++++++++++------ package-lock.json | 12 +++++++++++ package.json | 1 + packages/tm-core/package.json | 1 + packages/tm-core/src/index.ts | 1 + .../auth/services/organization.service.ts | 7 ++++--- packages/tm-core/src/utils/time.utils.ts | 18 ++++++++++++++++ 7 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 packages/tm-core/src/utils/time.utils.ts diff --git a/apps/cli/src/utils/brief-selection.ts b/apps/cli/src/utils/brief-selection.ts index 42d2aa85..caefd04c 100644 --- a/apps/cli/src/utils/brief-selection.ts +++ b/apps/cli/src/utils/brief-selection.ts @@ -3,12 +3,13 @@ * Reusable functions for selecting briefs interactively or via URL/ID */ -import chalk from 'chalk'; import search from '@inquirer/search'; -import ora, { Ora } from 'ora'; -import { AuthManager } from '@tm/core'; -import * as ui from './ui.js'; +import type { AuthManager } from '@tm/core'; +import { formatRelativeTime } from '@tm/core'; +import chalk from 'chalk'; +import ora, { type Ora } from 'ora'; import { getBriefStatusWithColor } from '../ui/formatters/status-formatters.js'; +import * as ui from './ui.js'; export interface BriefSelectionResult { success: boolean; @@ -122,8 +123,12 @@ export async function selectBriefInteractive( ) : ''; + const updatedAtDisplay = brief.updatedAt + ? chalk.gray(` • ${formatRelativeTime(brief.updatedAt)}`) + : ''; + groupedOptions.push({ - name: ` ${title}${taskCountDisplay} ${chalk.gray(`(${shortId})`)}`, + name: ` ${title}${taskCountDisplay} ${chalk.gray(`(${shortId})`)}${updatedAtDisplay}`, value: brief, description: description ? chalk.gray(` ${description.slice(0, 80)}`) @@ -158,8 +163,12 @@ export async function selectBriefInteractive( ) : ''; + const updatedAtDisplay = brief.updatedAt + ? chalk.gray(` • ${formatRelativeTime(brief.updatedAt)}`) + : ''; + groupedOptions.push({ - name: ` ${title}${taskCountDisplay} ${chalk.gray(`(${shortId})`)}`, + name: ` ${title}${taskCountDisplay} ${chalk.gray(`(${shortId})`)}${updatedAtDisplay}`, value: brief, description: description ? chalk.gray(` ${description.slice(0, 80)}`) diff --git a/package-lock.json b/package-lock.json index 9b63d1b8..3b62dcee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "cli-table3": "^0.6.5", "commander": "^12.1.0", "cors": "^2.8.5", + "date-fns": "^4.1.0", "dotenv": "^16.6.1", "express": "^4.21.2", "fastmcp": "^3.23.0", @@ -13239,6 +13240,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "2.6.9", "license": "MIT", @@ -28729,6 +28740,7 @@ "name": "@tm/core", "dependencies": { "@supabase/supabase-js": "^2.57.4", + "date-fns": "^4.1.0", "fs-extra": "^11.3.2", "simple-git": "^3.28.0", "steno": "^4.0.2", diff --git a/package.json b/package.json index 0ed4dac9..d472c380 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "cli-table3": "^0.6.5", "commander": "^12.1.0", "cors": "^2.8.5", + "date-fns": "^4.1.0", "dotenv": "^16.6.1", "express": "^4.21.2", "fastmcp": "^3.23.0", diff --git a/packages/tm-core/package.json b/packages/tm-core/package.json index 8be869bc..5a43c21b 100644 --- a/packages/tm-core/package.json +++ b/packages/tm-core/package.json @@ -31,6 +31,7 @@ }, "dependencies": { "@supabase/supabase-js": "^2.57.4", + "date-fns": "^4.1.0", "fs-extra": "^11.3.2", "simple-git": "^3.28.0", "steno": "^4.0.2", diff --git a/packages/tm-core/src/index.ts b/packages/tm-core/src/index.ts index 639aa4fc..4cb66a16 100644 --- a/packages/tm-core/src/index.ts +++ b/packages/tm-core/src/index.ts @@ -50,6 +50,7 @@ export * from './common/errors/index.js'; // Utils export * from './common/utils/index.js'; +export * from './utils/time.utils.js'; // ========== Domain-Specific Type Exports ========== diff --git a/packages/tm-core/src/modules/auth/services/organization.service.ts b/packages/tm-core/src/modules/auth/services/organization.service.ts index 11956833..b85375a7 100644 --- a/packages/tm-core/src/modules/auth/services/organization.service.ts +++ b/packages/tm-core/src/modules/auth/services/organization.service.ts @@ -3,13 +3,13 @@ * Handles fetching and managing organizations and briefs from the API */ -import { SupabaseClient } from '@supabase/supabase-js'; +import type { SupabaseClient } from '@supabase/supabase-js'; import { ERROR_CODES, TaskMasterError } from '../../../common/errors/task-master-error.js'; import { getLogger } from '../../../common/logger/index.js'; -import { Database } from '../../../common/types/database.types.js'; +import type { Database } from '../../../common/types/database.types.js'; import type { Brief } from '../../briefs/types.js'; /** @@ -171,7 +171,8 @@ export class OrganizationService { title ) `) - .eq('account_id', orgId); + .eq('account_id', orgId) + .order('updated_at', { ascending: false }); if (error) { throw new TaskMasterError( diff --git a/packages/tm-core/src/utils/time.utils.ts b/packages/tm-core/src/utils/time.utils.ts new file mode 100644 index 00000000..146b9c51 --- /dev/null +++ b/packages/tm-core/src/utils/time.utils.ts @@ -0,0 +1,18 @@ +/** + * @fileoverview Time utilities for formatting relative timestamps + * Shared across CLI, MCP, extension, and other interfaces + */ + +import { formatDistanceToNow } from 'date-fns'; + +/** + * Format a date as relative time from now (e.g., "2 hours ago", "3 days ago") + * @param date - Date string or Date object to format + * @returns Relative time string (e.g., "less than a minute ago", "5 minutes ago", "2 weeks ago") + */ +export function formatRelativeTime(date: string | Date): string { + const dateObj = typeof date === 'string' ? new Date(date) : date; + + // Use date-fns for robust formatting with proper edge case handling + return formatDistanceToNow(dateObj, { addSuffix: true }); +}