From 3cef499b8149d73a7319beed1cfad0ccc779ee4b Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Sun, 23 Mar 2025 19:08:02 -0700 Subject: [PATCH] Fix conversation list: remove GitHub link and show created_at date (#7435) Co-authored-by: openhands --- README.md | 2 +- docs/modules/usage/runtimes-index.md | 2 +- docs/modules/usage/runtimes/daytona.md | 2 +- docs/modules/usage/runtimes/local.md | 2 +- docs/modules/usage/runtimes/modal.md | 2 +- docs/modules/usage/runtimes/remote.md | 2 +- .../__tests__/utils/format-time-delta.test.ts | 36 +++++++++---------- .../conversation-panel/conversation-card.tsx | 28 ++++++++++++--- .../conversation-panel/conversation-panel.tsx | 2 ++ .../conversation-repo-link.tsx | 17 ++------- frontend/src/utils/format-time-delta.ts | 21 ++++++----- .../action_execution_client.py | 2 +- .../server/session/conversation_init_data.py | 3 +- openhands/server/session/session.py | 1 - 14 files changed, 65 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 904cdc738c..282f0712e5 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ docker run -it --rm --pull=always \ ``` > [!WARNING] -> On a public network? See our [Hardened Docker Installation](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation) guide +> On a public network? See our [Hardened Docker Installation](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation) guide > to secure your deployment by restricting network binding and implementing additional security measures. You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)! diff --git a/docs/modules/usage/runtimes-index.md b/docs/modules/usage/runtimes-index.md index c29dfebe27..e9f1dc24ed 100644 --- a/docs/modules/usage/runtimes-index.md +++ b/docs/modules/usage/runtimes-index.md @@ -21,4 +21,4 @@ OpenHands supports several different runtime environments: - [OpenHands Remote Runtime](./runtimes/remote.md) - Cloud-based runtime for parallel execution (beta) - [Modal Runtime](./runtimes/modal.md) - Runtime provided by our partners at Modal - [Daytona Runtime](./runtimes/daytona.md) - Runtime provided by Daytona -- [Local Runtime](./runtimes/local.md) - Direct execution on your local machine without Docker \ No newline at end of file +- [Local Runtime](./runtimes/local.md) - Direct execution on your local machine without Docker diff --git a/docs/modules/usage/runtimes/daytona.md b/docs/modules/usage/runtimes/daytona.md index 7424241f05..107f957c5a 100644 --- a/docs/modules/usage/runtimes/daytona.md +++ b/docs/modules/usage/runtimes/daytona.md @@ -29,4 +29,4 @@ bash -i <(curl -sL https://get.daytona.io/openhands) Once executed, OpenHands should be running locally and ready for use. -For more details and manual initialization, view the entire [README.md](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/impl/daytona/README.md) \ No newline at end of file +For more details and manual initialization, view the entire [README.md](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/impl/daytona/README.md) diff --git a/docs/modules/usage/runtimes/local.md b/docs/modules/usage/runtimes/local.md index 2fb1ec4f78..46e9a631e9 100644 --- a/docs/modules/usage/runtimes/local.md +++ b/docs/modules/usage/runtimes/local.md @@ -59,4 +59,4 @@ The Local Runtime is particularly useful for: - CI/CD pipelines where Docker is not available. - Testing and development of OpenHands itself. - Environments where container usage is restricted. -- Scenarios where direct file system access is required. \ No newline at end of file +- Scenarios where direct file system access is required. diff --git a/docs/modules/usage/runtimes/modal.md b/docs/modules/usage/runtimes/modal.md index 590fbc8c64..bcfbd0085c 100644 --- a/docs/modules/usage/runtimes/modal.md +++ b/docs/modules/usage/runtimes/modal.md @@ -10,4 +10,4 @@ docker run # ... -e RUNTIME=modal \ -e MODAL_API_TOKEN_ID="your-id" \ -e MODAL_API_TOKEN_SECRET="your-secret" \ -``` \ No newline at end of file +``` diff --git a/docs/modules/usage/runtimes/remote.md b/docs/modules/usage/runtimes/remote.md index c6688136ba..2df02d6356 100644 --- a/docs/modules/usage/runtimes/remote.md +++ b/docs/modules/usage/runtimes/remote.md @@ -3,4 +3,4 @@ OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to launch runtimes in parallel in the cloud. Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out! -NOTE: This runtime is specifically designed for agent evaluation purposes only through [OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be used to launch production OpenHands applications. \ No newline at end of file +NOTE: This runtime is specifically designed for agent evaluation purposes only through [OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be used to launch production OpenHands applications. diff --git a/frontend/__tests__/utils/format-time-delta.test.ts b/frontend/__tests__/utils/format-time-delta.test.ts index 283001bf20..496643a118 100644 --- a/frontend/__tests__/utils/format-time-delta.test.ts +++ b/frontend/__tests__/utils/format-time-delta.test.ts @@ -9,67 +9,67 @@ describe("formatTimeDelta", () => { it("formats the yearly time correctly", () => { const oneYearAgo = new Date("2023-01-01T00:00:00Z"); - expect(formatTimeDelta(oneYearAgo)).toBe("1 year"); + expect(formatTimeDelta(oneYearAgo)).toBe("1y"); const twoYearsAgo = new Date("2022-01-01T00:00:00Z"); - expect(formatTimeDelta(twoYearsAgo)).toBe("2 years"); + expect(formatTimeDelta(twoYearsAgo)).toBe("2y"); const threeYearsAgo = new Date("2021-01-01T00:00:00Z"); - expect(formatTimeDelta(threeYearsAgo)).toBe("3 years"); + expect(formatTimeDelta(threeYearsAgo)).toBe("3y"); }); it("formats the monthly time correctly", () => { const oneMonthAgo = new Date("2023-12-01T00:00:00Z"); - expect(formatTimeDelta(oneMonthAgo)).toBe("1 month"); + expect(formatTimeDelta(oneMonthAgo)).toBe("1mo"); const twoMonthsAgo = new Date("2023-11-01T00:00:00Z"); - expect(formatTimeDelta(twoMonthsAgo)).toBe("2 months"); + expect(formatTimeDelta(twoMonthsAgo)).toBe("2mo"); const threeMonthsAgo = new Date("2023-10-01T00:00:00Z"); - expect(formatTimeDelta(threeMonthsAgo)).toBe("3 months"); + expect(formatTimeDelta(threeMonthsAgo)).toBe("3mo"); }); it("formats the daily time correctly", () => { const oneDayAgo = new Date("2023-12-31T00:00:00Z"); - expect(formatTimeDelta(oneDayAgo)).toBe("1 day"); + expect(formatTimeDelta(oneDayAgo)).toBe("1d"); const twoDaysAgo = new Date("2023-12-30T00:00:00Z"); - expect(formatTimeDelta(twoDaysAgo)).toBe("2 days"); + expect(formatTimeDelta(twoDaysAgo)).toBe("2d"); const threeDaysAgo = new Date("2023-12-29T00:00:00Z"); - expect(formatTimeDelta(threeDaysAgo)).toBe("3 days"); + expect(formatTimeDelta(threeDaysAgo)).toBe("3d"); }); it("formats the hourly time correctly", () => { const oneHourAgo = new Date("2023-12-31T23:00:00Z"); - expect(formatTimeDelta(oneHourAgo)).toBe("1 hour"); + expect(formatTimeDelta(oneHourAgo)).toBe("1h"); const twoHoursAgo = new Date("2023-12-31T22:00:00Z"); - expect(formatTimeDelta(twoHoursAgo)).toBe("2 hours"); + expect(formatTimeDelta(twoHoursAgo)).toBe("2h"); const threeHoursAgo = new Date("2023-12-31T21:00:00Z"); - expect(formatTimeDelta(threeHoursAgo)).toBe("3 hours"); + expect(formatTimeDelta(threeHoursAgo)).toBe("3h"); }); it("formats the minute time correctly", () => { const oneMinuteAgo = new Date("2023-12-31T23:59:00Z"); - expect(formatTimeDelta(oneMinuteAgo)).toBe("1 minute"); + expect(formatTimeDelta(oneMinuteAgo)).toBe("1m"); const twoMinutesAgo = new Date("2023-12-31T23:58:00Z"); - expect(formatTimeDelta(twoMinutesAgo)).toBe("2 minutes"); + expect(formatTimeDelta(twoMinutesAgo)).toBe("2m"); const threeMinutesAgo = new Date("2023-12-31T23:57:00Z"); - expect(formatTimeDelta(threeMinutesAgo)).toBe("3 minutes"); + expect(formatTimeDelta(threeMinutesAgo)).toBe("3m"); }); it("formats the second time correctly", () => { const oneSecondAgo = new Date("2023-12-31T23:59:59Z"); - expect(formatTimeDelta(oneSecondAgo)).toBe("1 second"); + expect(formatTimeDelta(oneSecondAgo)).toBe("1s"); const twoSecondsAgo = new Date("2023-12-31T23:59:58Z"); - expect(formatTimeDelta(twoSecondsAgo)).toBe("2 seconds"); + expect(formatTimeDelta(twoSecondsAgo)).toBe("2s"); const threeSecondsAgo = new Date("2023-12-31T23:59:57Z"); - expect(formatTimeDelta(threeSecondsAgo)).toBe("3 seconds"); + expect(formatTimeDelta(threeSecondsAgo)).toBe("3s"); }); }); diff --git a/frontend/src/components/features/conversation-panel/conversation-card.tsx b/frontend/src/components/features/conversation-panel/conversation-card.tsx index 9b89d174cd..00fd0d765f 100644 --- a/frontend/src/components/features/conversation-panel/conversation-card.tsx +++ b/frontend/src/components/features/conversation-panel/conversation-card.tsx @@ -22,11 +22,14 @@ interface ConversationCardProps { title: string; selectedRepository: string | null; lastUpdatedAt: string; // ISO 8601 + createdAt?: string; // ISO 8601 status?: ProjectStatus; variant?: "compact" | "default"; conversationId?: string; // Optional conversation ID for VS Code URL } +const MAX_TIME_BETWEEN_CREATION_AND_UPDATE = 1000 * 60 * 30; // 30 minutes + export function ConversationCard({ onClick, onDelete, @@ -35,7 +38,10 @@ export function ConversationCard({ isActive, title, selectedRepository, + // lastUpdatedAt is kept in props for backward compatibility + // eslint-disable-next-line @typescript-eslint/no-unused-vars lastUpdatedAt, + createdAt, status = "STOPPED", variant = "default", conversationId, @@ -105,11 +111,10 @@ export function ConversationCard({ if (data.vscode_url) { window.open(data.vscode_url, "_blank"); - } else { - console.error("VS Code URL not available", data.error); } + // VS Code URL not available } catch (error) { - console.error("Failed to fetch VS Code URL", error); + // Failed to fetch VS Code URL } } @@ -128,6 +133,12 @@ export function ConversationCard({ }, [titleMode]); const hasContextMenu = !!(onDelete || onChangeTitle || showDisplayCostOption); + const timeBetweenUpdateAndCreation = createdAt + ? new Date(lastUpdatedAt).getTime() - new Date(createdAt).getTime() + : 0; + const showUpdateTime = + createdAt && + timeBetweenUpdateAndCreation > MAX_TIME_BETWEEN_CREATION_AND_UPDATE; return ( <> @@ -205,7 +216,16 @@ export function ConversationCard({ )}

- + Created + + {showUpdateTime && ( + <> + , updated + + + )}

diff --git a/frontend/src/components/features/conversation-panel/conversation-panel.tsx b/frontend/src/components/features/conversation-panel/conversation-panel.tsx index d669748ba8..f35ed756ad 100644 --- a/frontend/src/components/features/conversation-panel/conversation-panel.tsx +++ b/frontend/src/components/features/conversation-panel/conversation-panel.tsx @@ -108,7 +108,9 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) { title={project.title} selectedRepository={project.selected_repository} lastUpdatedAt={project.last_updated_at} + createdAt={project.created_at} status={project.status} + conversationId={project.conversation_id} /> )} diff --git a/frontend/src/components/features/conversation-panel/conversation-repo-link.tsx b/frontend/src/components/features/conversation-panel/conversation-repo-link.tsx index 5772ee41f1..655a67d53e 100644 --- a/frontend/src/components/features/conversation-panel/conversation-repo-link.tsx +++ b/frontend/src/components/features/conversation-panel/conversation-repo-link.tsx @@ -5,23 +5,12 @@ interface ConversationRepoLinkProps { export function ConversationRepoLink({ selectedRepository, }: ConversationRepoLinkProps) { - const handleClick = (event: React.MouseEvent) => { - event.preventDefault(); - window.open( - `https://github.com/${selectedRepository}`, - "_blank", - "noopener,noreferrer", - ); - }; - return ( - + ); } diff --git a/frontend/src/utils/format-time-delta.ts b/frontend/src/utils/format-time-delta.ts index b384614394..3bd43f14f5 100644 --- a/frontend/src/utils/format-time-delta.ts +++ b/frontend/src/utils/format-time-delta.ts @@ -1,12 +1,12 @@ /** - * Formats a date into a human-readable string representing the time delta between the given date and the current date. + * Formats a date into a compact string representing the time delta between the given date and the current date. * @param date The date to format - * @returns A human-readable string representing the time delta between the given date and the current date + * @returns A compact string representing the time delta between the given date and the current date * * @example * // now is 2024-01-01T00:00:00Z - * formatTimeDelta(new Date("2023-12-31T23:59:59Z")); // "1 second" - * formatTimeDelta(new Date("2022-01-01T00:00:00Z")); // "2 years" + * formatTimeDelta(new Date("2023-12-31T23:59:59Z")); // "1s" + * formatTimeDelta(new Date("2022-01-01T00:00:00Z")); // "2y" */ export const formatTimeDelta = (date: Date) => { const now = new Date(); @@ -19,11 +19,10 @@ export const formatTimeDelta = (date: Date) => { const months = Math.floor(days / 30); const years = Math.floor(months / 12); - if (seconds < 60) return seconds === 1 ? "1 second" : `${seconds} seconds`; - if (minutes < 60) return minutes === 1 ? "1 minute" : `${minutes} minutes`; - if (hours < 24) return hours === 1 ? "1 hour" : `${hours} hours`; - if (days < 30) return days === 1 ? "1 day" : `${days} days`; - if (months < 12) return months === 1 ? "1 month" : `${months} months`; - - return years === 1 ? "1 year" : `${years} years`; + if (seconds < 60) return `${seconds}s`; + if (minutes < 60) return `${minutes}m`; + if (hours < 24) return `${hours}h`; + if (days < 30) return `${days}d`; + if (months < 12) return `${months}mo`; + return `${years}y`; }; diff --git a/openhands/runtime/impl/action_execution/action_execution_client.py b/openhands/runtime/impl/action_execution/action_execution_client.py index 66f6efc341..1640958026 100644 --- a/openhands/runtime/impl/action_execution/action_execution_client.py +++ b/openhands/runtime/impl/action_execution/action_execution_client.py @@ -60,7 +60,7 @@ class ActionExecutionClient(Runtime): attach_to_existing: bool = False, headless_mode: bool = True, user_id: str | None = None, - git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None + git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None, ): self.session = HttpSession() self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time diff --git a/openhands/server/session/conversation_init_data.py b/openhands/server/session/conversation_init_data.py index 1ce27e3e96..397d119c50 100644 --- a/openhands/server/session/conversation_init_data.py +++ b/openhands/server/session/conversation_init_data.py @@ -1,4 +1,3 @@ -from types import MappingProxyType from pydantic import Field from openhands.integrations.provider import PROVIDER_TOKEN_TYPE @@ -16,4 +15,4 @@ class ConversationInitData(Settings): model_config = { 'arbitrary_types_allowed': True, - } \ No newline at end of file + } diff --git a/openhands/server/session/session.py b/openhands/server/session/session.py index 346c5be2f0..f1d74799e0 100644 --- a/openhands/server/session/session.py +++ b/openhands/server/session/session.py @@ -23,7 +23,6 @@ from openhands.events.observation import ( from openhands.events.observation.error import ErrorObservation from openhands.events.serialization import event_from_dict, event_to_dict from openhands.events.stream import EventStreamSubscriber -from openhands.integrations.provider import PROVIDER_TOKEN_TYPE from openhands.llm.llm import LLM from openhands.server.session.agent_session import AgentSession from openhands.server.session.conversation_init_data import ConversationInitData