mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
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:
parent
e2a0884ecd
commit
3cef499b81
@ -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)!
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -10,4 +10,4 @@ docker run # ...
|
||||
-e RUNTIME=modal \
|
||||
-e MODAL_API_TOKEN_ID="your-id" \
|
||||
-e MODAL_API_TOKEN_SECRET="your-secret" \
|
||||
```
|
||||
```
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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`;
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user