mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05: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:
@@ -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)!
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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" \
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user