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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 65 additions and 57 deletions

View File

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

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)
- [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
- [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.
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.
- Testing and development of OpenHands itself.
- 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 MODAL_API_TOKEN_ID="your-id" \
-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.
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", () => {
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");
});
});

View File

@ -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({
<ConversationRepoLink selectedRepository={selectedRepository} />
)}
<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>
</div>
</div>

View File

@ -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}
/>
)}
</NavLink>

View File

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

View File

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

View File

@ -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,
}
}

View File

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