Fix conversation list: remove GitHub link and show created_at date (#7435)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Robert Brennan
2025-03-23 19:08:02 -07:00
committed by GitHub
parent e2a0884ecd
commit 3cef499b81
14 changed files with 65 additions and 57 deletions

View File

@@ -57,7 +57,7 @@ docker run -it --rm --pull=always \
``` ```
> [!WARNING] > [!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. > 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)! You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!

View File

@@ -21,4 +21,4 @@ OpenHands supports several different runtime environments:
- [OpenHands Remote Runtime](./runtimes/remote.md) - Cloud-based runtime for parallel execution (beta) - [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 - [Modal Runtime](./runtimes/modal.md) - Runtime provided by our partners at Modal
- [Daytona Runtime](./runtimes/daytona.md) - Runtime provided by Daytona - [Daytona Runtime](./runtimes/daytona.md) - Runtime provided by Daytona
- [Local Runtime](./runtimes/local.md) - Direct execution on your local machine without Docker - [Local Runtime](./runtimes/local.md) - Direct execution on your local machine without Docker

View File

@@ -29,4 +29,4 @@ bash -i <(curl -sL https://get.daytona.io/openhands)
Once executed, OpenHands should be running locally and ready for use. 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) 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)

View File

@@ -59,4 +59,4 @@ The Local Runtime is particularly useful for:
- CI/CD pipelines where Docker is not available. - CI/CD pipelines where Docker is not available.
- Testing and development of OpenHands itself. - Testing and development of OpenHands itself.
- Environments where container usage is restricted. - Environments where container usage is restricted.
- Scenarios where direct file system access is required. - Scenarios where direct file system access is required.

View File

@@ -10,4 +10,4 @@ docker run # ...
-e RUNTIME=modal \ -e RUNTIME=modal \
-e MODAL_API_TOKEN_ID="your-id" \ -e MODAL_API_TOKEN_ID="your-id" \
-e MODAL_API_TOKEN_SECRET="your-secret" \ -e MODAL_API_TOKEN_SECRET="your-secret" \
``` ```

View File

@@ -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. 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! 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. 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.

View File

@@ -9,67 +9,67 @@ describe("formatTimeDelta", () => {
it("formats the yearly time correctly", () => { it("formats the yearly time correctly", () => {
const oneYearAgo = new Date("2023-01-01T00:00:00Z"); 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"); 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"); 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", () => { it("formats the monthly time correctly", () => {
const oneMonthAgo = new Date("2023-12-01T00:00:00Z"); 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"); 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"); 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", () => { it("formats the daily time correctly", () => {
const oneDayAgo = new Date("2023-12-31T00:00:00Z"); 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"); 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"); 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", () => { it("formats the hourly time correctly", () => {
const oneHourAgo = new Date("2023-12-31T23:00:00Z"); 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"); 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"); 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", () => { it("formats the minute time correctly", () => {
const oneMinuteAgo = new Date("2023-12-31T23:59:00Z"); 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"); 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"); 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", () => { it("formats the second time correctly", () => {
const oneSecondAgo = new Date("2023-12-31T23:59:59Z"); 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"); 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"); const threeSecondsAgo = new Date("2023-12-31T23:59:57Z");
expect(formatTimeDelta(threeSecondsAgo)).toBe("3 seconds"); expect(formatTimeDelta(threeSecondsAgo)).toBe("3s");
}); });
}); });

View File

@@ -22,11 +22,14 @@ interface ConversationCardProps {
title: string; title: string;
selectedRepository: string | null; selectedRepository: string | null;
lastUpdatedAt: string; // ISO 8601 lastUpdatedAt: string; // ISO 8601
createdAt?: string; // ISO 8601
status?: ProjectStatus; status?: ProjectStatus;
variant?: "compact" | "default"; variant?: "compact" | "default";
conversationId?: string; // Optional conversation ID for VS Code URL conversationId?: string; // Optional conversation ID for VS Code URL
} }
const MAX_TIME_BETWEEN_CREATION_AND_UPDATE = 1000 * 60 * 30; // 30 minutes
export function ConversationCard({ export function ConversationCard({
onClick, onClick,
onDelete, onDelete,
@@ -35,7 +38,10 @@ export function ConversationCard({
isActive, isActive,
title, title,
selectedRepository, selectedRepository,
// lastUpdatedAt is kept in props for backward compatibility
// eslint-disable-next-line @typescript-eslint/no-unused-vars
lastUpdatedAt, lastUpdatedAt,
createdAt,
status = "STOPPED", status = "STOPPED",
variant = "default", variant = "default",
conversationId, conversationId,
@@ -105,11 +111,10 @@ export function ConversationCard({
if (data.vscode_url) { if (data.vscode_url) {
window.open(data.vscode_url, "_blank"); window.open(data.vscode_url, "_blank");
} else {
console.error("VS Code URL not available", data.error);
} }
// VS Code URL not available
} catch (error) { } 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]); }, [titleMode]);
const hasContextMenu = !!(onDelete || onChangeTitle || showDisplayCostOption); 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 ( return (
<> <>
@@ -205,7 +216,16 @@ export function ConversationCard({
<ConversationRepoLink selectedRepository={selectedRepository} /> <ConversationRepoLink selectedRepository={selectedRepository} />
)} )}
<p className="text-xs text-neutral-400"> <p className="text-xs text-neutral-400">
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time> <span>Created </span>
<time>
{formatTimeDelta(new Date(createdAt || lastUpdatedAt))} ago
</time>
{showUpdateTime && (
<>
<span>, updated </span>
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
</>
)}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -108,7 +108,9 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
title={project.title} title={project.title}
selectedRepository={project.selected_repository} selectedRepository={project.selected_repository}
lastUpdatedAt={project.last_updated_at} lastUpdatedAt={project.last_updated_at}
createdAt={project.created_at}
status={project.status} status={project.status}
conversationId={project.conversation_id}
/> />
)} )}
</NavLink> </NavLink>

View File

@@ -5,23 +5,12 @@ interface ConversationRepoLinkProps {
export function ConversationRepoLink({ export function ConversationRepoLink({
selectedRepository, selectedRepository,
}: ConversationRepoLinkProps) { }: ConversationRepoLinkProps) {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
window.open(
`https://github.com/${selectedRepository}`,
"_blank",
"noopener,noreferrer",
);
};
return ( return (
<button <span
type="button"
data-testid="conversation-card-selected-repository" data-testid="conversation-card-selected-repository"
onClick={handleClick} className="text-xs text-neutral-400"
className="text-xs text-neutral-400 hover:text-neutral-200"
> >
{selectedRepository} {selectedRepository}
</button> </span>
); );
} }

View File

@@ -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 * @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 * @example
* // now is 2024-01-01T00:00:00Z * // now is 2024-01-01T00:00:00Z
* formatTimeDelta(new Date("2023-12-31T23:59:59Z")); // "1 second" * formatTimeDelta(new Date("2023-12-31T23:59:59Z")); // "1s"
* formatTimeDelta(new Date("2022-01-01T00:00:00Z")); // "2 years" * formatTimeDelta(new Date("2022-01-01T00:00:00Z")); // "2y"
*/ */
export const formatTimeDelta = (date: Date) => { export const formatTimeDelta = (date: Date) => {
const now = new Date(); const now = new Date();
@@ -19,11 +19,10 @@ export const formatTimeDelta = (date: Date) => {
const months = Math.floor(days / 30); const months = Math.floor(days / 30);
const years = Math.floor(months / 12); const years = Math.floor(months / 12);
if (seconds < 60) return seconds === 1 ? "1 second" : `${seconds} seconds`; if (seconds < 60) return `${seconds}s`;
if (minutes < 60) return minutes === 1 ? "1 minute" : `${minutes} minutes`; if (minutes < 60) return `${minutes}m`;
if (hours < 24) return hours === 1 ? "1 hour" : `${hours} hours`; if (hours < 24) return `${hours}h`;
if (days < 30) return days === 1 ? "1 day" : `${days} days`; if (days < 30) return `${days}d`;
if (months < 12) return months === 1 ? "1 month" : `${months} months`; if (months < 12) return `${months}mo`;
return `${years}y`;
return years === 1 ? "1 year" : `${years} years`;
}; };

View File

@@ -60,7 +60,7 @@ class ActionExecutionClient(Runtime):
attach_to_existing: bool = False, attach_to_existing: bool = False,
headless_mode: bool = True, headless_mode: bool = True,
user_id: str | None = None, 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.session = HttpSession()
self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time

View File

@@ -1,4 +1,3 @@
from types import MappingProxyType
from pydantic import Field from pydantic import Field
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
@@ -16,4 +15,4 @@ class ConversationInitData(Settings):
model_config = { model_config = {
'arbitrary_types_allowed': True, 'arbitrary_types_allowed': True,
} }

View File

@@ -23,7 +23,6 @@ from openhands.events.observation import (
from openhands.events.observation.error import ErrorObservation from openhands.events.observation.error import ErrorObservation
from openhands.events.serialization import event_from_dict, event_to_dict from openhands.events.serialization import event_from_dict, event_to_dict
from openhands.events.stream import EventStreamSubscriber from openhands.events.stream import EventStreamSubscriber
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.llm.llm import LLM from openhands.llm.llm import LLM
from openhands.server.session.agent_session import AgentSession from openhands.server.session.agent_session import AgentSession
from openhands.server.session.conversation_init_data import ConversationInitData from openhands.server.session.conversation_init_data import ConversationInitData