@openhands/agent-canvas 1.0.0-alpha.8 → 1.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/build/assets/{add-backend-modal-KMmPQNZU.js → add-backend-modal-FsnpTTgO.js} +1 -1
- package/build/assets/{agent-server-conversation-service.api-DSl9G5UR.js → agent-server-conversation-service.api-BZmUqtiO.js} +1 -1
- package/build/assets/{automation-detail-g5-RZ0da.js → automation-detail-R-99FUce.js} +1 -1
- package/build/assets/{automations-list-DHoq_0MM.js → automations-list-Dfu2c-_D.js} +1 -1
- package/build/assets/backend-form-modal-DxYjqqAK.js +1 -0
- package/build/assets/browser-HrYc5Gce.js +5 -0
- package/build/assets/conversation--ldUK72N.js +19 -0
- package/build/assets/conversation-eNrhH94O.js +1 -0
- package/build/assets/conversation-panel-B49Jpqpb.js +1 -0
- package/build/assets/{conversation-service.api-C8pYCyV6.js → conversation-service.api--f8WglOC.js} +1 -1
- package/build/assets/conversation-websocket-context-BW68-J8o.js +3 -0
- package/build/assets/{entry.client-D9uR9Blz.js → entry.client-CqqXOSvd.js} +2 -2
- package/build/assets/{files-tab-B3A1NDlZ.js → files-tab-CQHdWpQt.js} +1 -1
- package/build/assets/git-control-bar-branch-button-C8u5rzjc.js +27 -0
- package/build/assets/{git-provider-icon-DYE9n7fs.js → git-provider-icon-D-a-rcLm.js} +1 -1
- package/build/assets/{home-dIzxi5Dd.js → home-DD0GroCu.js} +1 -1
- package/build/assets/{launch-hZ0ifhcV.js → launch-B2mbfOSm.js} +1 -1
- package/build/assets/llm-settings-BEyqixPI.js +1 -0
- package/build/assets/{llm-settings-CcHqGOYL.js → llm-settings-BdiaGFbg.js} +1 -1
- package/build/assets/{manage-backends-modal-rYeyGx7j.js → manage-backends-modal-s22zCdEW.js} +1 -1
- package/build/assets/{manifest-97e839da.js → manifest-9d1c34fb.js} +1 -1
- package/build/assets/{messages-T2ewVkbp.js → messages-6aOyUu3r.js} +1 -1
- package/build/assets/{path-utils-CqJboYxo.js → path-utils-BVbe598W.js} +1 -1
- package/build/assets/{planner-tab-BrntFmb1.js → planner-tab-bN6r1G-1.js} +1 -1
- package/build/assets/{recommended-automations-launcher-BI9NhG8Y.js → recommended-automations-launcher-mJhK6Atl.js} +1 -1
- package/build/assets/{root-BS1Td78t.js → root-3t9rxEpE.js} +2 -2
- package/build/assets/{root-layout-BLjAEgle.js → root-layout-BjVwHmta.js} +2 -2
- package/build/assets/{shared-conversation-a0QV8o99.js → shared-conversation-EZV0FRIf.js} +1 -1
- package/build/assets/{sidebar-mobile-menu-toggle-DTUNI1WQ.js → sidebar-mobile-menu-toggle-BnbzzpQl.js} +1 -1
- package/build/assets/{skills-settings-DOnMn9q1.js → skills-settings-CG2hu34D.js} +1 -1
- package/build/assets/{task-list-tab-Day9nhRT.js → task-list-tab-465DDju0.js} +1 -1
- package/build/assets/{terminal-ro4SNjUU.js → terminal-CcgBEVnC.js} +1 -1
- package/build/assets/{use-active-conversation-D15D9GgR.js → use-active-conversation-DS5j9R4q.js} +1 -1
- package/build/assets/{use-agent-state-DE5dlEXJ.js → use-agent-state-D2C9SeGw.js} +1 -1
- package/build/assets/{use-create-conversation-DW7AGgLA.js → use-create-conversation-BEZg__Vv.js} +1 -1
- package/build/assets/{use-event-store-CQZCcVz-.js → use-event-store-BT_gV3ut.js} +1 -1
- package/build/assets/{use-handle-plan-click-DpgEQDAV.js → use-handle-plan-click-uOpew2LO.js} +1 -1
- package/build/assets/{use-runtime-is-ready-XFbT16BD.js → use-runtime-is-ready-pGSbPddC.js} +1 -1
- package/build/assets/{use-skills-Xe0vjPMt.js → use-skills-BIvlWblA.js} +1 -1
- package/build/assets/{use-task-list-Bs90uF2N.js → use-task-list-DDeNHprj.js} +1 -1
- package/build/assets/{use-unified-vscode-url-BOsIOd-b.js → use-unified-vscode-url-wAMzv8Sn.js} +1 -1
- package/build/assets/{use-user-conversation-Mc0mQgkl.js → use-user-conversation-B_zDoSeh.js} +1 -1
- package/build/assets/{vscode-tab-C0ShhiSU.js → vscode-tab-B0vdh9gU.js} +1 -1
- package/build/index.html +4 -4
- package/dist/api/agent-server-adapter.cjs +1 -1
- package/dist/api/agent-server-adapter.cjs.map +1 -1
- package/dist/api/agent-server-adapter.js +1 -0
- package/dist/api/agent-server-adapter.js.map +1 -1
- package/dist/api/skills-service.cjs +1 -1
- package/dist/api/skills-service.cjs.map +1 -1
- package/dist/api/skills-service.d.ts +1 -1
- package/dist/api/skills-service.js +2 -2
- package/dist/api/skills-service.js.map +1 -1
- package/dist/components/features/backends/backend-form-modal.cjs +1 -1
- package/dist/components/features/backends/backend-form-modal.cjs.map +1 -1
- package/dist/components/features/backends/backend-form-modal.js +149 -142
- package/dist/components/features/backends/backend-form-modal.js.map +1 -1
- package/dist/components/features/conversation-panel/skills-modal.cjs +1 -1
- package/dist/components/features/conversation-panel/skills-modal.cjs.map +1 -1
- package/dist/components/features/conversation-panel/skills-modal.js +1 -1
- package/dist/components/features/conversation-panel/skills-modal.js.map +1 -1
- package/dist/contexts/conversation-websocket-context.cjs +3 -3
- package/dist/contexts/conversation-websocket-context.cjs.map +1 -1
- package/dist/contexts/conversation-websocket-context.js +97 -89
- package/dist/contexts/conversation-websocket-context.js.map +1 -1
- package/dist/hooks/chat/use-slash-command.cjs +1 -1
- package/dist/hooks/chat/use-slash-command.cjs.map +1 -1
- package/dist/hooks/chat/use-slash-command.js +1 -1
- package/dist/hooks/chat/use-slash-command.js.map +1 -1
- package/dist/hooks/query/use-conversation-skills.cjs +2 -0
- package/dist/hooks/query/use-conversation-skills.cjs.map +1 -0
- package/dist/hooks/query/use-conversation-skills.d.ts +7 -0
- package/dist/hooks/query/use-conversation-skills.js +8 -0
- package/dist/hooks/query/use-conversation-skills.js.map +1 -0
- package/dist/hooks/query/use-skills.cjs +1 -1
- package/dist/hooks/query/use-skills.cjs.map +1 -1
- package/dist/hooks/query/use-skills.d.ts +6 -1
- package/dist/hooks/query/use-skills.js +3 -3
- package/dist/hooks/query/use-skills.js.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.cjs.map +1 -1
- package/dist/package.js +3 -3
- package/dist/package.js.map +1 -1
- package/dist/routes/conversation.cjs +1 -1
- package/dist/routes/conversation.cjs.map +1 -1
- package/dist/routes/conversation.js +61 -63
- package/dist/routes/conversation.js.map +1 -1
- package/dist/stores/use-event-store.cjs +1 -1
- package/dist/stores/use-event-store.cjs.map +1 -1
- package/dist/stores/use-event-store.d.ts +22 -0
- package/dist/stores/use-event-store.js +9 -1
- package/dist/stores/use-event-store.js.map +1 -1
- package/dist/ui/context-menu.d.ts +1 -1
- package/dist/ui/help-link.d.ts +1 -1
- package/package.json +3 -3
- package/scripts/dev-safe.mjs +35 -17
- package/scripts/dev-with-automation.mjs +24 -49
- package/build/assets/backend-form-modal-K6IMCr3p.js +0 -1
- package/build/assets/browser-DKG63inJ.js +0 -5
- package/build/assets/conversation-BD5WemJI.js +0 -19
- package/build/assets/conversation-C47K62n8.js +0 -1
- package/build/assets/conversation-panel-Dn-S56Gk.js +0 -1
- package/build/assets/conversation-websocket-context-Ywrxd_9p.js +0 -3
- package/build/assets/git-control-bar-branch-button-CcIpmyfM.js +0 -27
- package/build/assets/llm-settings-2036m7Wt.js +0 -1
- /package/build/assets/{link-external-Df8J52xI.js → link-external-C9d6Fo3x.js} +0 -0
- /package/build/assets/{use-llm-profiles-D3-KXwQ0.js → use-llm-profiles-O4a9V6RC.js} +0 -0
|
@@ -9,88 +9,86 @@ import { useConversationStateStore as s } from "../stores/conversation-state-sto
|
|
|
9
9
|
import { useActiveBackend as c } from "../contexts/active-backend-context.js";
|
|
10
10
|
import { clearLastConversationId as l, setLastConversationId as u } from "../api/backend-registry/last-conversation-store.js";
|
|
11
11
|
import { displayErrorToast as d } from "../utils/custom-toast-handlers.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
import S from "react";
|
|
22
|
-
import {
|
|
23
|
-
import { useLocation as w, useMatch as T, useNavigate as E } from "react-router";
|
|
12
|
+
import { useErrorMessageStore as f } from "../stores/error-message-store.js";
|
|
13
|
+
import { resumeCloudSandbox as p } from "../api/cloud/conversation-service.api.js";
|
|
14
|
+
import { useActiveConversation as m } from "../hooks/query/use-active-conversation.js";
|
|
15
|
+
import { EventHandler as h } from "../wrapper/event-handler.js";
|
|
16
|
+
import { useTaskPolling as g } from "../hooks/query/use-task-polling.js";
|
|
17
|
+
import { useIsAuthed as _ } from "../hooks/query/use-is-authed.js";
|
|
18
|
+
import { ConversationMain as v, ConversationMobilePanelPage as y } from "../components/features/conversation/conversation-main/conversation-main.js";
|
|
19
|
+
import { WebSocketProviderWrapper as b } from "../contexts/websocket-provider-wrapper.js";
|
|
20
|
+
import x from "react";
|
|
21
|
+
import { jsx as S } from "react/jsx-runtime";
|
|
22
|
+
import { useLocation as C, useMatch as w, useNavigate as T } from "react-router";
|
|
24
23
|
//#region src/routes/conversation.tsx
|
|
25
|
-
function
|
|
26
|
-
let { t:
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
function E() {
|
|
25
|
+
let { t: E } = e("openhands"), { conversationId: D } = r(), O = w("/conversations/:conversationId/panel"), { isTask: k, taskStatus: A, taskDetail: j } = g(), M = c(), N = x.useRef(M.backend.id), P = x.useRef(M.orgId), F = N.current !== M.backend.id || P.current !== M.orgId, { data: I, isFetched: L } = m(), { data: R } = _(), { resetConversationState: z } = a(), B = T(), V = C(), H = i((e) => e.clearTerminal), U = s((e) => e.reset), W = o((e) => e.setCurrentAgentState), G = f((e) => e.removeErrorMessage);
|
|
26
|
+
x.useEffect(() => {
|
|
27
|
+
H(), z(), U(), W(n.LOADING), G();
|
|
29
28
|
}, [
|
|
30
|
-
|
|
29
|
+
D,
|
|
30
|
+
H,
|
|
31
|
+
z,
|
|
32
|
+
U,
|
|
31
33
|
W,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (j && M === "ERROR") {
|
|
39
|
-
d(N || D(t.CONVERSATION$FAILED_TO_START_FROM_TASK));
|
|
40
|
-
let e = U.state?.resumedFromConversationId;
|
|
41
|
-
H(e ? `/conversations/${e}` : "/conversations", { replace: !0 });
|
|
34
|
+
G
|
|
35
|
+
]), x.useEffect(() => {
|
|
36
|
+
if (k && A === "ERROR") {
|
|
37
|
+
d(j || E(t.CONVERSATION$FAILED_TO_START_FROM_TASK));
|
|
38
|
+
let e = V.state?.resumedFromConversationId;
|
|
39
|
+
B(e ? `/conversations/${e}` : "/conversations", { replace: !0 });
|
|
42
40
|
}
|
|
43
41
|
}, [
|
|
42
|
+
k,
|
|
43
|
+
A,
|
|
44
44
|
j,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
]), S.useEffect(() => {
|
|
51
|
-
!z || !B || L || R || (l(P.backend.id, P.orgId), d(D(t.CONVERSATION$NOT_EXIST_OR_NO_PERMISSION)), H("/conversations"));
|
|
45
|
+
E,
|
|
46
|
+
B,
|
|
47
|
+
V.state
|
|
48
|
+
]), x.useEffect(() => {
|
|
49
|
+
!L || !R || F || I || (l(M.backend.id, M.orgId), d(E(t.CONVERSATION$NOT_EXIST_OR_NO_PERMISSION)), B("/conversations"));
|
|
52
50
|
}, [
|
|
51
|
+
I,
|
|
52
|
+
L,
|
|
53
53
|
R,
|
|
54
|
-
z,
|
|
55
54
|
B,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
L || O && (O.startsWith("task-") || u(P.backend.id, P.orgId, O));
|
|
55
|
+
E,
|
|
56
|
+
F,
|
|
57
|
+
M.backend.id,
|
|
58
|
+
M.orgId
|
|
59
|
+
]), x.useEffect(() => {
|
|
60
|
+
F || D && (D.startsWith("task-") || u(M.backend.id, M.orgId, D));
|
|
63
61
|
}, [
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
D,
|
|
63
|
+
F,
|
|
64
|
+
M.backend.id,
|
|
65
|
+
M.orgId
|
|
68
66
|
]);
|
|
69
|
-
let
|
|
70
|
-
return
|
|
71
|
-
!
|
|
72
|
-
d(
|
|
67
|
+
let K = x.useRef(null);
|
|
68
|
+
return x.useEffect(() => {
|
|
69
|
+
!L || !I || M.backend.kind === "cloud" && I.sandbox_status === "PAUSED" && I.sandbox_id && K.current !== I.id && (K.current = I.id, p(I.sandbox_id).catch(() => {
|
|
70
|
+
d(E(t.CONVERSATION$FAILED_TO_START_FROM_TASK));
|
|
73
71
|
}));
|
|
74
72
|
}, [
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
]), /* @__PURE__ */
|
|
82
|
-
conversationId:
|
|
83
|
-
children: /* @__PURE__ */
|
|
73
|
+
L,
|
|
74
|
+
I?.id,
|
|
75
|
+
I?.sandbox_status,
|
|
76
|
+
I?.sandbox_id,
|
|
77
|
+
M.backend.kind,
|
|
78
|
+
E
|
|
79
|
+
]), /* @__PURE__ */ S(b, {
|
|
80
|
+
conversationId: D,
|
|
81
|
+
children: /* @__PURE__ */ S(h, { children: /* @__PURE__ */ S("div", {
|
|
84
82
|
"data-testid": "app-route",
|
|
85
83
|
className: "flex h-full flex-col",
|
|
86
|
-
children:
|
|
84
|
+
children: O ? /* @__PURE__ */ S(y, { onNavigateBack: () => B(`/conversations/${D}`) }) : /* @__PURE__ */ S(v, {})
|
|
87
85
|
}) })
|
|
88
86
|
});
|
|
89
87
|
}
|
|
90
|
-
function
|
|
91
|
-
return /* @__PURE__ */
|
|
88
|
+
function D() {
|
|
89
|
+
return /* @__PURE__ */ S(E, {});
|
|
92
90
|
}
|
|
93
91
|
//#endregion
|
|
94
|
-
export {
|
|
92
|
+
export { D as default };
|
|
95
93
|
|
|
96
94
|
//# sourceMappingURL=conversation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation.js","names":[],"sources":["../../src/routes/conversation.tsx"],"sourcesContent":["import React from \"react\";\nimport { useNavigate, useLocation, useMatch } from \"react-router\";\nimport { useTranslation } from \"react-i18next\";\n\nimport { useConversationId } from \"#/hooks/use-conversation-id\";\nimport { useCommandStore } from \"#/stores/command-store\";\nimport { useConversationStore } from \"#/stores/conversation-store\";\nimport { useAgentStore } from \"#/stores/agent-store\";\nimport { useConversationStateStore } from \"#/stores/conversation-state-store\";\nimport { useActiveBackend } from \"#/contexts/active-backend-context\";\nimport {\n clearLastConversationId,\n setLastConversationId,\n} from \"#/api/backend-registry/last-conversation-store\";\nimport { AgentState } from \"#/types/agent-state\";\n\nimport { EventHandler } from \"../wrapper/event-handler\";\n\nimport { useActiveConversation } from \"#/hooks/query/use-active-conversation\";\nimport { useTaskPolling } from \"#/hooks/query/use-task-polling\";\n\nimport { displayErrorToast } from \"#/utils/custom-toast-handlers\";\nimport { useIsAuthed } from \"#/hooks/query/use-is-authed\";\nimport {\n ConversationMain,\n ConversationMobilePanelPage,\n} from \"#/components/features/conversation/conversation-main/conversation-main\";\n\nimport { WebSocketProviderWrapper } from \"#/contexts/websocket-provider-wrapper\";\nimport { useErrorMessageStore } from \"#/stores/error-message-store\";\nimport { I18nKey } from \"#/i18n/declaration\";\nimport { useEventStore } from \"#/stores/use-event-store\";\nimport { resumeCloudSandbox } from \"#/api/cloud/conversation-service.api\";\n\nfunction AppContent() {\n const { t } = useTranslation(\"openhands\");\n const { conversationId } = useConversationId();\n const panelViewMatch = useMatch(\"/conversations/:conversationId/panel\");\n const clearEvents = useEventStore((state) => state.clearEvents);\n\n const { isTask, taskStatus, taskDetail } = useTaskPolling();\n\n // The conversationId in the URL belongs to whichever backend was\n // active when the route first mounted. If the user switches backends\n // while this route is still mounted, the id is meaningless under the\n // new backend — disable the active-conversation fetch (and its 404\n // toast) so we don't fire a request that the BackendSelector's\n // redirect will immediately navigate away from anyway. Mirrors the\n // same guard in `routes/automation-detail.tsx`.\n const active = useActiveBackend();\n const mountedBackendId = React.useRef(active.backend.id);\n const mountedOrgId = React.useRef(active.orgId);\n const backendChanged =\n mountedBackendId.current !== active.backend.id ||\n mountedOrgId.current !== active.orgId;\n\n const { data: conversation, isFetched } = useActiveConversation();\n const { data: isAuthed } = useIsAuthed();\n const { resetConversationState } = useConversationStore();\n const navigate = useNavigate();\n const location = useLocation();\n const clearTerminal = useCommandStore((state) => state.clearTerminal);\n const resetConversationRuntimeState = useConversationStateStore(\n (state) => state.reset,\n );\n const setCurrentAgentState = useAgentStore(\n (state) => state.setCurrentAgentState,\n );\n const removeErrorMessage = useErrorMessageStore(\n (state) => state.removeErrorMessage,\n );\n\n React.useEffect(() => {\n clearTerminal();\n resetConversationState();\n resetConversationRuntimeState();\n setCurrentAgentState(AgentState.LOADING);\n removeErrorMessage();\n clearEvents();\n }, [\n conversationId,\n clearTerminal,\n resetConversationState,\n resetConversationRuntimeState,\n setCurrentAgentState,\n removeErrorMessage,\n clearEvents,\n ]);\n\n React.useEffect(() => {\n if (isTask && taskStatus === \"ERROR\") {\n displayErrorToast(\n taskDetail || t(I18nKey.CONVERSATION$FAILED_TO_START_FROM_TASK),\n );\n // Navigate back to the original conversation when a resume task fails so\n // the user isn't stranded at the dead task-{id} URL. The resume effect's\n // ref prevents it from immediately retrying once we land there.\n const resumedFrom = (location.state as Record<string, unknown> | null)\n ?.resumedFromConversationId as string | undefined;\n navigate(\n resumedFrom ? `/conversations/${resumedFrom}` : \"/conversations\",\n { replace: true },\n );\n }\n }, [isTask, taskStatus, taskDetail, t, navigate, location.state]);\n\n React.useEffect(() => {\n if (!isFetched || !isAuthed) return;\n // The BackendSelector is in the middle of redirecting us away from\n // this route — don't toast/navigate based on a 404 that's just\n // \"this id doesn't exist on the new backend\".\n if (backendChanged) return;\n\n if (!conversation) {\n // Clear the per-backend \"last selected\" slot so the next switch\n // to this backend doesn't try to revisit a stale id.\n clearLastConversationId(active.backend.id, active.orgId);\n displayErrorToast(t(I18nKey.CONVERSATION$NOT_EXIST_OR_NO_PERMISSION));\n navigate(\"/conversations\");\n }\n }, [\n conversation,\n isFetched,\n isAuthed,\n navigate,\n t,\n backendChanged,\n active.backend.id,\n active.orgId,\n ]);\n\n // Remember the most recently selected conversation for the current\n // (backend, org) so flipping back to this backend later restores the\n // user to where they left off. Skip while a backend switch is in\n // flight: the id in the URL is from the previous backend and would\n // otherwise overwrite the new backend's memory.\n React.useEffect(() => {\n if (backendChanged) return;\n if (!conversationId) return;\n if (conversationId.startsWith(\"task-\")) return;\n setLastConversationId(active.backend.id, active.orgId, conversationId);\n }, [conversationId, backendChanged, active.backend.id, active.orgId]);\n\n // Cloud conversation resume: mirrors OpenHands' useSandboxRecovery.\n //\n // When the cloud API reports sandbox_status === \"PAUSED\" the sandbox is\n // sleeping. The correct wake-up call is POST /api/v1/sandboxes/{id}/resume\n // (a lightweight unpause). The previous approach — creating a new start task\n // via POST /api/v1/app-conversations — was wrong: it tries to provision a\n // fresh conversation in the sandbox and is subject to a 120-second cold-start\n // timeout that can fail. The resume endpoint simply unpauses the existing one.\n //\n // After calling resume we stay on the current URL. The 3-second refetch\n // interval in useActiveConversation (active while conversation_url is null)\n // polls until conversation_url populates, then the WebSocket connects.\n //\n // A ref guards against duplicate triggers per unique conversation.id within\n // the same route-mount lifetime.\n const resumeTriggeredForRef = React.useRef<string | null>(null);\n React.useEffect(() => {\n if (!isFetched || !conversation) return;\n if (active.backend.kind !== \"cloud\") return;\n if (conversation.sandbox_status !== \"PAUSED\") return; // only resume PAUSED sandboxes\n if (!conversation.sandbox_id) return; // no sandbox to resume\n if (resumeTriggeredForRef.current === conversation.id) return; // already sent\n\n resumeTriggeredForRef.current = conversation.id;\n\n resumeCloudSandbox(conversation.sandbox_id).catch(() => {\n displayErrorToast(t(I18nKey.CONVERSATION$FAILED_TO_START_FROM_TASK));\n });\n }, [\n isFetched,\n conversation?.id,\n conversation?.sandbox_status,\n conversation?.sandbox_id,\n active.backend.kind,\n t,\n ]);\n\n const content = (\n <EventHandler>\n <div data-testid=\"app-route\" className=\"flex h-full flex-col\">\n {panelViewMatch ? (\n <ConversationMobilePanelPage\n onNavigateBack={() => navigate(`/conversations/${conversationId}`)}\n />\n ) : (\n <ConversationMain />\n )}\n </div>\n </EventHandler>\n );\n\n return (\n <WebSocketProviderWrapper conversationId={conversationId}>\n {content}\n </WebSocketProviderWrapper>\n );\n}\n\nexport function ConversationView() {\n return <AppContent />;\n}\n\nexport default ConversationView;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAS,IAAa;CACpB,IAAM,EAAE,SAAM,EAAe,YAAY,EACnC,EAAE,sBAAmB,GAAmB,EACxC,IAAiB,EAAS,uCAAuC,EACjE,IAAc,GAAe,MAAU,EAAM,YAAY,EAEzD,EAAE,WAAQ,eAAY,kBAAe,GAAgB,EASrD,IAAS,GAAkB,EAC3B,IAAmB,EAAM,OAAO,EAAO,QAAQ,GAAG,EAClD,IAAe,EAAM,OAAO,EAAO,MAAM,EACzC,IACJ,EAAiB,YAAY,EAAO,QAAQ,MAC5C,EAAa,YAAY,EAAO,OAE5B,EAAE,MAAM,GAAc,iBAAc,GAAuB,EAC3D,EAAE,MAAM,MAAa,GAAa,EAClC,EAAE,8BAA2B,GAAsB,EACnD,IAAW,GAAa,EACxB,IAAW,GAAa,EACxB,IAAgB,GAAiB,MAAU,EAAM,cAAc,EAC/D,IAAgC,GACnC,MAAU,EAAM,MAClB,EACK,IAAuB,GAC1B,MAAU,EAAM,qBAClB,EACK,IAAqB,GACxB,MAAU,EAAM,mBAClB;AAkED,CAhEA,EAAM,gBAAgB;AAMpB,EALA,GAAe,EACf,GAAwB,EACxB,GAA+B,EAC/B,EAAqB,EAAW,QAAQ,EACxC,GAAoB,EACpB,GAAa;IACZ;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,EAEF,EAAM,gBAAgB;AACpB,MAAI,KAAU,MAAe,SAAS;AACpC,KACE,KAAc,EAAE,EAAQ,uCAAuC,CAChE;GAID,IAAM,IAAe,EAAS,OAC1B;AACJ,KACE,IAAc,kBAAkB,MAAgB,kBAChD,EAAE,SAAS,IAAM,CAClB;;IAEF;EAAC;EAAQ;EAAY;EAAY;EAAG;EAAU,EAAS;EAAM,CAAC,EAEjE,EAAM,gBAAgB;AAChB,GAAC,KAAa,CAAC,KAIf,KAEC,MAGH,EAAwB,EAAO,QAAQ,IAAI,EAAO,MAAM,EACxD,EAAkB,EAAE,EAAQ,wCAAwC,CAAC,EACrE,EAAS,iBAAiB;IAE3B;EACD;EACA;EACA;EACA;EACA;EACA;EACA,EAAO,QAAQ;EACf,EAAO;EACR,CAAC,EAOF,EAAM,gBAAgB;AAChB,OACC,MACD,EAAe,WAAW,QAAQ,IACtC,EAAsB,EAAO,QAAQ,IAAI,EAAO,OAAO,EAAe;IACrE;EAAC;EAAgB;EAAgB,EAAO,QAAQ;EAAI,EAAO;EAAM,CAAC;CAiBrE,IAAM,IAAwB,EAAM,OAAsB,KAAK;AAoC/D,QAnCA,EAAM,gBAAgB;AAChB,GAAC,KAAa,CAAC,KACf,EAAO,QAAQ,SAAS,WACxB,EAAa,mBAAmB,YAC/B,EAAa,cACd,EAAsB,YAAY,EAAa,OAEnD,EAAsB,UAAU,EAAa,IAE7C,EAAmB,EAAa,WAAW,CAAC,YAAY;AACtD,KAAkB,EAAE,EAAQ,uCAAuC,CAAC;IACpE;IACD;EACD;EACA,GAAc;EACd,GAAc;EACd,GAAc;EACd,EAAO,QAAQ;EACf;EACD,CAAC,EAiBA,kBAAC,GAAD;EAA0C;YAd1C,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;GAAK,eAAY;GAAY,WAAU;aACpC,IACC,kBAAC,GAAD,EACE,sBAAsB,EAAS,kBAAkB,IAAiB,EAClE,CAAA,GAEF,kBAAC,GAAD,EAAoB,CAAA;GAElB,CAAA,EACO,CAKZ;EACwB,CAAA;;AAI/B,SAAgB,IAAmB;AACjC,QAAO,kBAAC,GAAD,EAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"conversation.js","names":[],"sources":["../../src/routes/conversation.tsx"],"sourcesContent":["import React from \"react\";\nimport { useNavigate, useLocation, useMatch } from \"react-router\";\nimport { useTranslation } from \"react-i18next\";\n\nimport { useConversationId } from \"#/hooks/use-conversation-id\";\nimport { useCommandStore } from \"#/stores/command-store\";\nimport { useConversationStore } from \"#/stores/conversation-store\";\nimport { useAgentStore } from \"#/stores/agent-store\";\nimport { useConversationStateStore } from \"#/stores/conversation-state-store\";\nimport { useActiveBackend } from \"#/contexts/active-backend-context\";\nimport {\n clearLastConversationId,\n setLastConversationId,\n} from \"#/api/backend-registry/last-conversation-store\";\nimport { AgentState } from \"#/types/agent-state\";\n\nimport { EventHandler } from \"../wrapper/event-handler\";\n\nimport { useActiveConversation } from \"#/hooks/query/use-active-conversation\";\nimport { useTaskPolling } from \"#/hooks/query/use-task-polling\";\n\nimport { displayErrorToast } from \"#/utils/custom-toast-handlers\";\nimport { useIsAuthed } from \"#/hooks/query/use-is-authed\";\nimport {\n ConversationMain,\n ConversationMobilePanelPage,\n} from \"#/components/features/conversation/conversation-main/conversation-main\";\n\nimport { WebSocketProviderWrapper } from \"#/contexts/websocket-provider-wrapper\";\nimport { useErrorMessageStore } from \"#/stores/error-message-store\";\nimport { I18nKey } from \"#/i18n/declaration\";\nimport { resumeCloudSandbox } from \"#/api/cloud/conversation-service.api\";\n\nfunction AppContent() {\n const { t } = useTranslation(\"openhands\");\n const { conversationId } = useConversationId();\n const panelViewMatch = useMatch(\"/conversations/:conversationId/panel\");\n\n const { isTask, taskStatus, taskDetail } = useTaskPolling();\n\n // The conversationId in the URL belongs to whichever backend was\n // active when the route first mounted. If the user switches backends\n // while this route is still mounted, the id is meaningless under the\n // new backend — disable the active-conversation fetch (and its 404\n // toast) so we don't fire a request that the BackendSelector's\n // redirect will immediately navigate away from anyway. Mirrors the\n // same guard in `routes/automation-detail.tsx`.\n const active = useActiveBackend();\n const mountedBackendId = React.useRef(active.backend.id);\n const mountedOrgId = React.useRef(active.orgId);\n const backendChanged =\n mountedBackendId.current !== active.backend.id ||\n mountedOrgId.current !== active.orgId;\n\n const { data: conversation, isFetched } = useActiveConversation();\n const { data: isAuthed } = useIsAuthed();\n const { resetConversationState } = useConversationStore();\n const navigate = useNavigate();\n const location = useLocation();\n const clearTerminal = useCommandStore((state) => state.clearTerminal);\n const resetConversationRuntimeState = useConversationStateStore(\n (state) => state.reset,\n );\n const setCurrentAgentState = useAgentStore(\n (state) => state.setCurrentAgentState,\n );\n const removeErrorMessage = useErrorMessageStore(\n (state) => state.removeErrorMessage,\n );\n\n // Per-conversation UI/runtime resets. The event store is cleared separately,\n // inside ConversationWebSocketProvider, so the clear is ordered *before* the\n // preloaded-history re-seed (see the note there) — clearing it here would run\n // too late and wipe the freshly seeded history on a conversation switch.\n React.useEffect(() => {\n clearTerminal();\n resetConversationState();\n resetConversationRuntimeState();\n setCurrentAgentState(AgentState.LOADING);\n removeErrorMessage();\n }, [\n conversationId,\n clearTerminal,\n resetConversationState,\n resetConversationRuntimeState,\n setCurrentAgentState,\n removeErrorMessage,\n ]);\n\n React.useEffect(() => {\n if (isTask && taskStatus === \"ERROR\") {\n displayErrorToast(\n taskDetail || t(I18nKey.CONVERSATION$FAILED_TO_START_FROM_TASK),\n );\n // Navigate back to the original conversation when a resume task fails so\n // the user isn't stranded at the dead task-{id} URL. The resume effect's\n // ref prevents it from immediately retrying once we land there.\n const resumedFrom = (location.state as Record<string, unknown> | null)\n ?.resumedFromConversationId as string | undefined;\n navigate(\n resumedFrom ? `/conversations/${resumedFrom}` : \"/conversations\",\n { replace: true },\n );\n }\n }, [isTask, taskStatus, taskDetail, t, navigate, location.state]);\n\n React.useEffect(() => {\n if (!isFetched || !isAuthed) return;\n // The BackendSelector is in the middle of redirecting us away from\n // this route — don't toast/navigate based on a 404 that's just\n // \"this id doesn't exist on the new backend\".\n if (backendChanged) return;\n\n if (!conversation) {\n // Clear the per-backend \"last selected\" slot so the next switch\n // to this backend doesn't try to revisit a stale id.\n clearLastConversationId(active.backend.id, active.orgId);\n displayErrorToast(t(I18nKey.CONVERSATION$NOT_EXIST_OR_NO_PERMISSION));\n navigate(\"/conversations\");\n }\n }, [\n conversation,\n isFetched,\n isAuthed,\n navigate,\n t,\n backendChanged,\n active.backend.id,\n active.orgId,\n ]);\n\n // Remember the most recently selected conversation for the current\n // (backend, org) so flipping back to this backend later restores the\n // user to where they left off. Skip while a backend switch is in\n // flight: the id in the URL is from the previous backend and would\n // otherwise overwrite the new backend's memory.\n React.useEffect(() => {\n if (backendChanged) return;\n if (!conversationId) return;\n if (conversationId.startsWith(\"task-\")) return;\n setLastConversationId(active.backend.id, active.orgId, conversationId);\n }, [conversationId, backendChanged, active.backend.id, active.orgId]);\n\n // Cloud conversation resume: mirrors OpenHands' useSandboxRecovery.\n //\n // When the cloud API reports sandbox_status === \"PAUSED\" the sandbox is\n // sleeping. The correct wake-up call is POST /api/v1/sandboxes/{id}/resume\n // (a lightweight unpause). The previous approach — creating a new start task\n // via POST /api/v1/app-conversations — was wrong: it tries to provision a\n // fresh conversation in the sandbox and is subject to a 120-second cold-start\n // timeout that can fail. The resume endpoint simply unpauses the existing one.\n //\n // After calling resume we stay on the current URL. The 3-second refetch\n // interval in useActiveConversation (active while conversation_url is null)\n // polls until conversation_url populates, then the WebSocket connects.\n //\n // A ref guards against duplicate triggers per unique conversation.id within\n // the same route-mount lifetime.\n const resumeTriggeredForRef = React.useRef<string | null>(null);\n React.useEffect(() => {\n if (!isFetched || !conversation) return;\n if (active.backend.kind !== \"cloud\") return;\n if (conversation.sandbox_status !== \"PAUSED\") return; // only resume PAUSED sandboxes\n if (!conversation.sandbox_id) return; // no sandbox to resume\n if (resumeTriggeredForRef.current === conversation.id) return; // already sent\n\n resumeTriggeredForRef.current = conversation.id;\n\n resumeCloudSandbox(conversation.sandbox_id).catch(() => {\n displayErrorToast(t(I18nKey.CONVERSATION$FAILED_TO_START_FROM_TASK));\n });\n }, [\n isFetched,\n conversation?.id,\n conversation?.sandbox_status,\n conversation?.sandbox_id,\n active.backend.kind,\n t,\n ]);\n\n const content = (\n <EventHandler>\n <div data-testid=\"app-route\" className=\"flex h-full flex-col\">\n {panelViewMatch ? (\n <ConversationMobilePanelPage\n onNavigateBack={() => navigate(`/conversations/${conversationId}`)}\n />\n ) : (\n <ConversationMain />\n )}\n </div>\n </EventHandler>\n );\n\n return (\n <WebSocketProviderWrapper conversationId={conversationId}>\n {content}\n </WebSocketProviderWrapper>\n );\n}\n\nexport function ConversationView() {\n return <AppContent />;\n}\n\nexport default ConversationView;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAS,IAAa;CACpB,IAAM,EAAE,SAAM,EAAe,YAAY,EACnC,EAAE,sBAAmB,GAAmB,EACxC,IAAiB,EAAS,uCAAuC,EAEjE,EAAE,WAAQ,eAAY,kBAAe,GAAgB,EASrD,IAAS,GAAkB,EAC3B,IAAmB,EAAM,OAAO,EAAO,QAAQ,GAAG,EAClD,IAAe,EAAM,OAAO,EAAO,MAAM,EACzC,IACJ,EAAiB,YAAY,EAAO,QAAQ,MAC5C,EAAa,YAAY,EAAO,OAE5B,EAAE,MAAM,GAAc,iBAAc,GAAuB,EAC3D,EAAE,MAAM,MAAa,GAAa,EAClC,EAAE,8BAA2B,GAAsB,EACnD,IAAW,GAAa,EACxB,IAAW,GAAa,EACxB,IAAgB,GAAiB,MAAU,EAAM,cAAc,EAC/D,IAAgC,GACnC,MAAU,EAAM,MAClB,EACK,IAAuB,GAC1B,MAAU,EAAM,qBAClB,EACK,IAAqB,GACxB,MAAU,EAAM,mBAClB;AAoED,CA9DA,EAAM,gBAAgB;AAKpB,EAJA,GAAe,EACf,GAAwB,EACxB,GAA+B,EAC/B,EAAqB,EAAW,QAAQ,EACxC,GAAoB;IACnB;EACD;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,EAEF,EAAM,gBAAgB;AACpB,MAAI,KAAU,MAAe,SAAS;AACpC,KACE,KAAc,EAAE,EAAQ,uCAAuC,CAChE;GAID,IAAM,IAAe,EAAS,OAC1B;AACJ,KACE,IAAc,kBAAkB,MAAgB,kBAChD,EAAE,SAAS,IAAM,CAClB;;IAEF;EAAC;EAAQ;EAAY;EAAY;EAAG;EAAU,EAAS;EAAM,CAAC,EAEjE,EAAM,gBAAgB;AAChB,GAAC,KAAa,CAAC,KAIf,KAEC,MAGH,EAAwB,EAAO,QAAQ,IAAI,EAAO,MAAM,EACxD,EAAkB,EAAE,EAAQ,wCAAwC,CAAC,EACrE,EAAS,iBAAiB;IAE3B;EACD;EACA;EACA;EACA;EACA;EACA;EACA,EAAO,QAAQ;EACf,EAAO;EACR,CAAC,EAOF,EAAM,gBAAgB;AAChB,OACC,MACD,EAAe,WAAW,QAAQ,IACtC,EAAsB,EAAO,QAAQ,IAAI,EAAO,OAAO,EAAe;IACrE;EAAC;EAAgB;EAAgB,EAAO,QAAQ;EAAI,EAAO;EAAM,CAAC;CAiBrE,IAAM,IAAwB,EAAM,OAAsB,KAAK;AAoC/D,QAnCA,EAAM,gBAAgB;AAChB,GAAC,KAAa,CAAC,KACf,EAAO,QAAQ,SAAS,WACxB,EAAa,mBAAmB,YAC/B,EAAa,cACd,EAAsB,YAAY,EAAa,OAEnD,EAAsB,UAAU,EAAa,IAE7C,EAAmB,EAAa,WAAW,CAAC,YAAY;AACtD,KAAkB,EAAE,EAAQ,uCAAuC,CAAC;IACpE;IACD;EACD;EACA,GAAc;EACd,GAAc;EACd,GAAc;EACd,EAAO,QAAQ;EACf;EACD,CAAC,EAiBA,kBAAC,GAAD;EAA0C;YAd1C,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;GAAK,eAAY;GAAY,WAAU;aACpC,IACC,kBAAC,GAAD,EACE,sBAAsB,EAAS,kBAAkB,IAAiB,EAClE,CAAA,GAEF,kBAAC,GAAD,EAAoB,CAAA;GAElB,CAAA,EACO,CAKZ;EACwB,CAAA;;AAI/B,SAAgB,IAAmB;AACjC,QAAO,kBAAC,GAAD,EAAc,CAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
require(`../_virtual/_rolldown/runtime.cjs`);const e=require(`../node_modules/zustand/esm/react.cjs`),t=require(`../utils/handle-event-for-ui.cjs`);var n=e=>`id`in e?e.id:void 0,r=e=>`timestamp`in e?e.timestamp:void 0,i=(e,t)=>{let n=r(e),i=r(t);return!n&&!i?0:n?i?n.localeCompare(i):-1:1},a=(e,t)=>{if(e.length===0)return!1;let n=e[e.length-1],i=r(n),a=r(t);return!i||!a?!1:a<i},o=(e,r)=>{let i=n(r);if(i!==void 0&&e.eventIds.has(i))return e;let a=i===void 0?e.eventIds:new Set(e.eventIds).add(i);return{...e,events:[...e.events,r],eventIds:a,uiEvents:t.handleEventForUI(r,e.uiEvents)}},s=e=>({...e,events:[...e.events].sort(i),uiEvents:[...e.uiEvents].sort(i)}),c=(e,t)=>{let n=o(e,t);return n===e?e:!a(e.events,t)&&!a(e.uiEvents,t)?n:s(n)},l=e.create()(e=>({events:[],eventIds:new Set,uiEvents:[],addEvent:t=>e(e=>c(e,t)),addEvents:r=>e(e=>{if(r.length===0)return e;let i=new Set(e.eventIds),a=[...e.events],o=[...e.uiEvents],c=!1;for(let e of r){let r=n(e);r!==void 0&&i.has(r)||(c=!0,r!==void 0&&i.add(r),a.push(e),o=t.handleEventForUI(e,o))}return c?s({...e,events:a,eventIds:i,uiEvents:o}):e}),clearEvents:()=>e(()=>({events:[],eventIds:new Set,uiEvents:[]}))}));exports.useEventStore=l;
|
|
1
|
+
require(`../_virtual/_rolldown/runtime.cjs`);const e=require(`../node_modules/zustand/esm/react.cjs`),t=require(`../utils/handle-event-for-ui.cjs`);var n=e=>`id`in e?e.id:void 0,r=e=>`timestamp`in e?e.timestamp:void 0,i=(e,t)=>{let n=r(e),i=r(t);return!n&&!i?0:n?i?n.localeCompare(i):-1:1},a=(e,t)=>{if(e.length===0)return!1;let n=e[e.length-1],i=r(n),a=r(t);return!i||!a?!1:a<i},o=(e,r)=>{let i=n(r);if(i!==void 0&&e.eventIds.has(i))return e;let a=i===void 0?e.eventIds:new Set(e.eventIds).add(i);return{...e,events:[...e.events,r],eventIds:a,uiEvents:t.handleEventForUI(r,e.uiEvents)}},s=e=>({...e,events:[...e.events].sort(i),uiEvents:[...e.uiEvents].sort(i)}),c=(e,t)=>{let n=o(e,t);return n===e?e:!a(e.events,t)&&!a(e.uiEvents,t)?n:s(n)},l=e.create()(e=>({events:[],eventIds:new Set,uiEvents:[],loadedConversationId:null,addEvent:t=>e(e=>c(e,t)),addEvents:r=>e(e=>{if(r.length===0)return e;let i=new Set(e.eventIds),a=[...e.events],o=[...e.uiEvents],c=!1;for(let e of r){let r=n(e);r!==void 0&&i.has(r)||(c=!0,r!==void 0&&i.add(r),a.push(e),o=t.handleEventForUI(e,o))}return c?s({...e,events:a,eventIds:i,uiEvents:o}):e}),clearEvents:()=>e(()=>({events:[],eventIds:new Set,uiEvents:[],loadedConversationId:null})),clearEventsForConversation:t=>e(()=>({events:[],eventIds:new Set,uiEvents:[],loadedConversationId:t}))}));exports.useEventStore=l;
|
|
2
2
|
//# sourceMappingURL=use-event-store.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-event-store.cjs","names":[],"sources":["../../src/stores/use-event-store.ts"],"sourcesContent":["import { create } from \"zustand\";\nimport { OpenHandsEvent } from \"#/types/agent-server/core\";\nimport { handleEventForUI } from \"#/utils/handle-event-for-ui\";\n\nexport type OHEvent = OpenHandsEvent & {\n isFromPlanningAgent?: boolean;\n};\n\nconst getEventId = (event: OHEvent): string | number | undefined =>\n \"id\" in event ? event.id : undefined;\n\nconst getEventTimestamp = (event: OHEvent): string | undefined =>\n \"timestamp\" in event ? event.timestamp : undefined;\n\n/**\n * Compare two events by timestamp for sorting.\n * Events without timestamps are placed at the end.\n */\nconst compareEventsByTimestamp = (a: OHEvent, b: OHEvent): number => {\n const timestampA = getEventTimestamp(a);\n const timestampB = getEventTimestamp(b);\n\n // Events without timestamps go to the end\n if (!timestampA && !timestampB) return 0;\n if (!timestampA) return 1;\n if (!timestampB) return -1;\n\n // Compare ISO timestamp strings (lexicographic comparison works for ISO format)\n return timestampA.localeCompare(timestampB);\n};\n\n/**\n * Check if the new event needs sorting (i.e., it's out of order).\n * Returns true if the new event's timestamp is earlier than the last event's timestamp.\n */\nconst needsSorting = (events: OHEvent[], newEvent: OHEvent): boolean => {\n if (events.length === 0) return false;\n\n const lastEvent = events[events.length - 1];\n const lastTimestamp = getEventTimestamp(lastEvent);\n const newTimestamp = getEventTimestamp(newEvent);\n\n // If either event doesn't have a timestamp, don't sort\n if (!lastTimestamp || !newTimestamp) return false;\n\n // Sort needed if new event's timestamp is earlier than last event's timestamp\n return newTimestamp < lastTimestamp;\n};\n\nexport interface EventState {\n events: OHEvent[];\n eventIds: Set<string | number>;\n uiEvents: OHEvent[];\n addEvent: (event: OHEvent) => void;\n /**\n * Bulk-insert events. Used for the initial REST history load and for\n * \"scroll up to load older\" pagination. Newly-added events are de-duped\n * against the existing store and the combined list is re-sorted by\n * timestamp so older pages drop into the correct position.\n */\n addEvents: (events: OHEvent[]) => void;\n clearEvents: () => void;\n}\n\nconst appendEvent = (state: EventState, event: OHEvent): EventState => {\n // Deduplicate: skip if event with same id already exists (O(1) lookup)\n const eventId = getEventId(event);\n if (eventId !== undefined && state.eventIds.has(eventId)) {\n return state;\n }\n\n const newEventIds =\n eventId !== undefined\n ? new Set(state.eventIds).add(eventId)\n : state.eventIds;\n\n return {\n ...state,\n events: [...state.events, event],\n eventIds: newEventIds,\n uiEvents: handleEventForUI(event, state.uiEvents),\n };\n};\n\nconst sortEventState = (state: EventState): EventState => ({\n ...state,\n events: [...state.events].sort(compareEventsByTimestamp),\n uiEvents: [...state.uiEvents].sort(compareEventsByTimestamp),\n});\n\nconst applyAddEvent = (state: EventState, event: OHEvent): EventState => {\n const next = appendEvent(state, event);\n if (next === state) {\n return state;\n }\n\n if (\n !needsSorting(state.events, event) &&\n !needsSorting(state.uiEvents, event)\n ) {\n return next;\n }\n\n return sortEventState(next);\n};\n\nexport const useEventStore = create<EventState>()((set) => ({\n events: [],\n eventIds: new Set(),\n uiEvents: [],\n addEvent: (event: OHEvent) => set((state) => applyAddEvent(state, event)),\n addEvents: (incoming: OHEvent[]) =>\n set((state) => {\n if (incoming.length === 0) return state;\n\n const eventIds = new Set(state.eventIds);\n const events = [...state.events];\n let uiEvents = [...state.uiEvents];\n let added = false;\n\n for (const event of incoming) {\n const eventId = getEventId(event);\n const isDuplicate = eventId !== undefined && eventIds.has(eventId);\n\n if (!isDuplicate) {\n added = true;\n if (eventId !== undefined) {\n eventIds.add(eventId);\n }\n events.push(event);\n uiEvents = handleEventForUI(event, uiEvents);\n }\n }\n\n if (!added) {\n return state;\n }\n\n return sortEventState({\n ...state,\n events,\n eventIds,\n uiEvents,\n });\n }),\n clearEvents: () =>\n set(() => ({\n events: [],\n eventIds: new Set(),\n uiEvents: [],\n })),\n}));\n\n// In dev builds, expose the store on `window` so that fixture/preview\n// scripts (e.g. .pr/issue-132 demo capture) can inject synthetic events\n// without round-tripping through the agent-server. Tree-shaken in\n// production builds via `import.meta.env.DEV`.\nif (\n typeof window !== \"undefined\" &&\n typeof import.meta !== \"undefined\" &&\n (import.meta as { env?: { DEV?: boolean } }).env?.DEV\n) {\n (\n window as unknown as { __OH_EVENT_STORE__?: typeof useEventStore }\n ).__OH_EVENT_STORE__ = useEventStore;\n}\n"],"mappings":"oJAQA,IAAM,EAAc,GAClB,OAAQ,EAAQ,EAAM,GAAK,IAAA,GAEvB,EAAqB,GACzB,cAAe,EAAQ,EAAM,UAAY,IAAA,GAMrC,GAA4B,EAAY,IAAuB,CACnE,IAAM,EAAa,EAAkB,EAAE,CACjC,EAAa,EAAkB,EAAE,CAQvC,MALI,CAAC,GAAc,CAAC,EAAmB,EAClC,EACA,EAGE,EAAW,cAAc,EAAW,CAHnB,GADA,GAWpB,GAAgB,EAAmB,IAA+B,CACtE,GAAI,EAAO,SAAW,EAAG,MAAO,GAEhC,IAAM,EAAY,EAAO,EAAO,OAAS,GACnC,EAAgB,EAAkB,EAAU,CAC5C,EAAe,EAAkB,EAAS,CAMhD,MAHI,CAAC,GAAiB,CAAC,EAAqB,GAGrC,EAAe,
|
|
1
|
+
{"version":3,"file":"use-event-store.cjs","names":[],"sources":["../../src/stores/use-event-store.ts"],"sourcesContent":["import { create } from \"zustand\";\nimport { OpenHandsEvent } from \"#/types/agent-server/core\";\nimport { handleEventForUI } from \"#/utils/handle-event-for-ui\";\n\nexport type OHEvent = OpenHandsEvent & {\n isFromPlanningAgent?: boolean;\n};\n\nconst getEventId = (event: OHEvent): string | number | undefined =>\n \"id\" in event ? event.id : undefined;\n\nconst getEventTimestamp = (event: OHEvent): string | undefined =>\n \"timestamp\" in event ? event.timestamp : undefined;\n\n/**\n * Compare two events by timestamp for sorting.\n * Events without timestamps are placed at the end.\n */\nconst compareEventsByTimestamp = (a: OHEvent, b: OHEvent): number => {\n const timestampA = getEventTimestamp(a);\n const timestampB = getEventTimestamp(b);\n\n // Events without timestamps go to the end\n if (!timestampA && !timestampB) return 0;\n if (!timestampA) return 1;\n if (!timestampB) return -1;\n\n // Compare ISO timestamp strings (lexicographic comparison works for ISO format)\n return timestampA.localeCompare(timestampB);\n};\n\n/**\n * Check if the new event needs sorting (i.e., it's out of order).\n * Returns true if the new event's timestamp is earlier than the last event's timestamp.\n */\nconst needsSorting = (events: OHEvent[], newEvent: OHEvent): boolean => {\n if (events.length === 0) return false;\n\n const lastEvent = events[events.length - 1];\n const lastTimestamp = getEventTimestamp(lastEvent);\n const newTimestamp = getEventTimestamp(newEvent);\n\n // If either event doesn't have a timestamp, don't sort\n if (!lastTimestamp || !newTimestamp) return false;\n\n // Sort needed if new event's timestamp is earlier than last event's timestamp\n return newTimestamp < lastTimestamp;\n};\n\nexport interface EventState {\n events: OHEvent[];\n eventIds: Set<string | number>;\n uiEvents: OHEvent[];\n /**\n * The conversation whose events currently populate the store. The store is\n * global (not keyed by conversation), so the conversation route uses this to\n * tell a genuine conversation switch apart from a remount of the *same*\n * conversation (e.g. navigating to Settings and back) — only the former\n * should clear the accumulated events.\n */\n loadedConversationId: string | null;\n addEvent: (event: OHEvent) => void;\n /**\n * Bulk-insert events. Used for the initial REST history load and for\n * \"scroll up to load older\" pagination. Newly-added events are de-duped\n * against the existing store and the combined list is re-sorted by\n * timestamp so older pages drop into the correct position.\n */\n addEvents: (events: OHEvent[]) => void;\n /**\n * Clear all events. Also resets `loadedConversationId` to `null` so the\n * store never claims to hold a conversation whose events have been wiped —\n * the invariant (`loadedConversationId` reflects the conversation whose\n * events are in the arrays) holds even for a standalone clear.\n */\n clearEvents: () => void;\n /**\n * Atomically clear all events and record which conversation is now loaded.\n * Collapsing the reset and the bookkeeping into a single `set` keeps the\n * store invariant enforced at the boundary, rather than relying on every\n * call-site to invoke a clear and a `loadedConversationId` setter in the\n * right order.\n */\n clearEventsForConversation: (conversationId: string | null) => void;\n}\n\nconst appendEvent = (state: EventState, event: OHEvent): EventState => {\n // Deduplicate: skip if event with same id already exists (O(1) lookup)\n const eventId = getEventId(event);\n if (eventId !== undefined && state.eventIds.has(eventId)) {\n return state;\n }\n\n const newEventIds =\n eventId !== undefined\n ? new Set(state.eventIds).add(eventId)\n : state.eventIds;\n\n return {\n ...state,\n events: [...state.events, event],\n eventIds: newEventIds,\n uiEvents: handleEventForUI(event, state.uiEvents),\n };\n};\n\nconst sortEventState = (state: EventState): EventState => ({\n ...state,\n events: [...state.events].sort(compareEventsByTimestamp),\n uiEvents: [...state.uiEvents].sort(compareEventsByTimestamp),\n});\n\nconst applyAddEvent = (state: EventState, event: OHEvent): EventState => {\n const next = appendEvent(state, event);\n if (next === state) {\n return state;\n }\n\n if (\n !needsSorting(state.events, event) &&\n !needsSorting(state.uiEvents, event)\n ) {\n return next;\n }\n\n return sortEventState(next);\n};\n\nexport const useEventStore = create<EventState>()((set) => ({\n events: [],\n eventIds: new Set(),\n uiEvents: [],\n loadedConversationId: null,\n addEvent: (event: OHEvent) => set((state) => applyAddEvent(state, event)),\n addEvents: (incoming: OHEvent[]) =>\n set((state) => {\n if (incoming.length === 0) return state;\n\n const eventIds = new Set(state.eventIds);\n const events = [...state.events];\n let uiEvents = [...state.uiEvents];\n let added = false;\n\n for (const event of incoming) {\n const eventId = getEventId(event);\n const isDuplicate = eventId !== undefined && eventIds.has(eventId);\n\n if (!isDuplicate) {\n added = true;\n if (eventId !== undefined) {\n eventIds.add(eventId);\n }\n events.push(event);\n uiEvents = handleEventForUI(event, uiEvents);\n }\n }\n\n if (!added) {\n return state;\n }\n\n return sortEventState({\n ...state,\n events,\n eventIds,\n uiEvents,\n });\n }),\n clearEvents: () =>\n set(() => ({\n events: [],\n eventIds: new Set(),\n uiEvents: [],\n loadedConversationId: null,\n })),\n clearEventsForConversation: (conversationId: string | null) =>\n set(() => ({\n events: [],\n eventIds: new Set(),\n uiEvents: [],\n loadedConversationId: conversationId,\n })),\n}));\n\n// In dev builds, expose the store on `window` so that fixture/preview\n// scripts (e.g. .pr/issue-132 demo capture) can inject synthetic events\n// without round-tripping through the agent-server. Tree-shaken in\n// production builds via `import.meta.env.DEV`.\nif (\n typeof window !== \"undefined\" &&\n typeof import.meta !== \"undefined\" &&\n (import.meta as { env?: { DEV?: boolean } }).env?.DEV\n) {\n (\n window as unknown as { __OH_EVENT_STORE__?: typeof useEventStore }\n ).__OH_EVENT_STORE__ = useEventStore;\n}\n"],"mappings":"oJAQA,IAAM,EAAc,GAClB,OAAQ,EAAQ,EAAM,GAAK,IAAA,GAEvB,EAAqB,GACzB,cAAe,EAAQ,EAAM,UAAY,IAAA,GAMrC,GAA4B,EAAY,IAAuB,CACnE,IAAM,EAAa,EAAkB,EAAE,CACjC,EAAa,EAAkB,EAAE,CAQvC,MALI,CAAC,GAAc,CAAC,EAAmB,EAClC,EACA,EAGE,EAAW,cAAc,EAAW,CAHnB,GADA,GAWpB,GAAgB,EAAmB,IAA+B,CACtE,GAAI,EAAO,SAAW,EAAG,MAAO,GAEhC,IAAM,EAAY,EAAO,EAAO,OAAS,GACnC,EAAgB,EAAkB,EAAU,CAC5C,EAAe,EAAkB,EAAS,CAMhD,MAHI,CAAC,GAAiB,CAAC,EAAqB,GAGrC,EAAe,GAwClB,GAAe,EAAmB,IAA+B,CAErE,IAAM,EAAU,EAAW,EAAM,CACjC,GAAI,IAAY,IAAA,IAAa,EAAM,SAAS,IAAI,EAAQ,CACtD,OAAO,EAGT,IAAM,EACJ,IAAY,IAAA,GAER,EAAM,SADN,IAAI,IAAI,EAAM,SAAS,CAAC,IAAI,EAAQ,CAG1C,MAAO,CACL,GAAG,EACH,OAAQ,CAAC,GAAG,EAAM,OAAQ,EAAM,CAChC,SAAU,EACV,SAAU,EAAA,iBAAiB,EAAO,EAAM,SAAS,CAClD,EAGG,EAAkB,IAAmC,CACzD,GAAG,EACH,OAAQ,CAAC,GAAG,EAAM,OAAO,CAAC,KAAK,EAAyB,CACxD,SAAU,CAAC,GAAG,EAAM,SAAS,CAAC,KAAK,EAAyB,CAC7D,EAEK,GAAiB,EAAmB,IAA+B,CACvE,IAAM,EAAO,EAAY,EAAO,EAAM,CAYtC,OAXI,IAAS,EACJ,EAIP,CAAC,EAAa,EAAM,OAAQ,EAAM,EAClC,CAAC,EAAa,EAAM,SAAU,EAAM,CAE7B,EAGF,EAAe,EAAK,EAGhB,EAAgB,EAAA,QAAoB,CAAE,IAAS,CAC1D,OAAQ,EAAE,CACV,SAAU,IAAI,IACd,SAAU,EAAE,CACZ,qBAAsB,KACtB,SAAW,GAAmB,EAAK,GAAU,EAAc,EAAO,EAAM,CAAC,CACzE,UAAY,GACV,EAAK,GAAU,CACb,GAAI,EAAS,SAAW,EAAG,OAAO,EAElC,IAAM,EAAW,IAAI,IAAI,EAAM,SAAS,CAClC,EAAS,CAAC,GAAG,EAAM,OAAO,CAC5B,EAAW,CAAC,GAAG,EAAM,SAAS,CAC9B,EAAQ,GAEZ,IAAK,IAAM,KAAS,EAAU,CAC5B,IAAM,EAAU,EAAW,EAAM,CACb,IAAY,IAAA,IAAa,EAAS,IAAI,EAAQ,GAGhE,EAAQ,GACJ,IAAY,IAAA,IACd,EAAS,IAAI,EAAQ,CAEvB,EAAO,KAAK,EAAM,CAClB,EAAW,EAAA,iBAAiB,EAAO,EAAS,EAQhD,OAJK,EAIE,EAAe,CACpB,GAAG,EACH,SACA,WACA,WACD,CAAC,CARO,GAST,CACJ,gBACE,OAAW,CACT,OAAQ,EAAE,CACV,SAAU,IAAI,IACd,SAAU,EAAE,CACZ,qBAAsB,KACvB,EAAE,CACL,2BAA6B,GAC3B,OAAW,CACT,OAAQ,EAAE,CACV,SAAU,IAAI,IACd,SAAU,EAAE,CACZ,qBAAsB,EACvB,EAAE,CACN,EAAE"}
|
|
@@ -6,6 +6,14 @@ export interface EventState {
|
|
|
6
6
|
events: OHEvent[];
|
|
7
7
|
eventIds: Set<string | number>;
|
|
8
8
|
uiEvents: OHEvent[];
|
|
9
|
+
/**
|
|
10
|
+
* The conversation whose events currently populate the store. The store is
|
|
11
|
+
* global (not keyed by conversation), so the conversation route uses this to
|
|
12
|
+
* tell a genuine conversation switch apart from a remount of the *same*
|
|
13
|
+
* conversation (e.g. navigating to Settings and back) — only the former
|
|
14
|
+
* should clear the accumulated events.
|
|
15
|
+
*/
|
|
16
|
+
loadedConversationId: string | null;
|
|
9
17
|
addEvent: (event: OHEvent) => void;
|
|
10
18
|
/**
|
|
11
19
|
* Bulk-insert events. Used for the initial REST history load and for
|
|
@@ -14,6 +22,20 @@ export interface EventState {
|
|
|
14
22
|
* timestamp so older pages drop into the correct position.
|
|
15
23
|
*/
|
|
16
24
|
addEvents: (events: OHEvent[]) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Clear all events. Also resets `loadedConversationId` to `null` so the
|
|
27
|
+
* store never claims to hold a conversation whose events have been wiped —
|
|
28
|
+
* the invariant (`loadedConversationId` reflects the conversation whose
|
|
29
|
+
* events are in the arrays) holds even for a standalone clear.
|
|
30
|
+
*/
|
|
17
31
|
clearEvents: () => void;
|
|
32
|
+
/**
|
|
33
|
+
* Atomically clear all events and record which conversation is now loaded.
|
|
34
|
+
* Collapsing the reset and the bookkeeping into a single `set` keeps the
|
|
35
|
+
* store invariant enforced at the boundary, rather than relying on every
|
|
36
|
+
* call-site to invoke a clear and a `loadedConversationId` setter in the
|
|
37
|
+
* right order.
|
|
38
|
+
*/
|
|
39
|
+
clearEventsForConversation: (conversationId: string | null) => void;
|
|
18
40
|
}
|
|
19
41
|
export declare const useEventStore: import("zustand").UseBoundStore<import("zustand").StoreApi<EventState>>;
|
|
@@ -29,6 +29,7 @@ var n = (e) => "id" in e ? e.id : void 0, r = (e) => "timestamp" in e ? e.timest
|
|
|
29
29
|
events: [],
|
|
30
30
|
eventIds: /* @__PURE__ */ new Set(),
|
|
31
31
|
uiEvents: [],
|
|
32
|
+
loadedConversationId: null,
|
|
32
33
|
addEvent: (t) => e((e) => c(e, t)),
|
|
33
34
|
addEvents: (r) => e((e) => {
|
|
34
35
|
if (r.length === 0) return e;
|
|
@@ -47,7 +48,14 @@ var n = (e) => "id" in e ? e.id : void 0, r = (e) => "timestamp" in e ? e.timest
|
|
|
47
48
|
clearEvents: () => e(() => ({
|
|
48
49
|
events: [],
|
|
49
50
|
eventIds: /* @__PURE__ */ new Set(),
|
|
50
|
-
uiEvents: []
|
|
51
|
+
uiEvents: [],
|
|
52
|
+
loadedConversationId: null
|
|
53
|
+
})),
|
|
54
|
+
clearEventsForConversation: (t) => e(() => ({
|
|
55
|
+
events: [],
|
|
56
|
+
eventIds: /* @__PURE__ */ new Set(),
|
|
57
|
+
uiEvents: [],
|
|
58
|
+
loadedConversationId: t
|
|
51
59
|
}))
|
|
52
60
|
}));
|
|
53
61
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-event-store.js","names":[],"sources":["../../src/stores/use-event-store.ts"],"sourcesContent":["import { create } from \"zustand\";\nimport { OpenHandsEvent } from \"#/types/agent-server/core\";\nimport { handleEventForUI } from \"#/utils/handle-event-for-ui\";\n\nexport type OHEvent = OpenHandsEvent & {\n isFromPlanningAgent?: boolean;\n};\n\nconst getEventId = (event: OHEvent): string | number | undefined =>\n \"id\" in event ? event.id : undefined;\n\nconst getEventTimestamp = (event: OHEvent): string | undefined =>\n \"timestamp\" in event ? event.timestamp : undefined;\n\n/**\n * Compare two events by timestamp for sorting.\n * Events without timestamps are placed at the end.\n */\nconst compareEventsByTimestamp = (a: OHEvent, b: OHEvent): number => {\n const timestampA = getEventTimestamp(a);\n const timestampB = getEventTimestamp(b);\n\n // Events without timestamps go to the end\n if (!timestampA && !timestampB) return 0;\n if (!timestampA) return 1;\n if (!timestampB) return -1;\n\n // Compare ISO timestamp strings (lexicographic comparison works for ISO format)\n return timestampA.localeCompare(timestampB);\n};\n\n/**\n * Check if the new event needs sorting (i.e., it's out of order).\n * Returns true if the new event's timestamp is earlier than the last event's timestamp.\n */\nconst needsSorting = (events: OHEvent[], newEvent: OHEvent): boolean => {\n if (events.length === 0) return false;\n\n const lastEvent = events[events.length - 1];\n const lastTimestamp = getEventTimestamp(lastEvent);\n const newTimestamp = getEventTimestamp(newEvent);\n\n // If either event doesn't have a timestamp, don't sort\n if (!lastTimestamp || !newTimestamp) return false;\n\n // Sort needed if new event's timestamp is earlier than last event's timestamp\n return newTimestamp < lastTimestamp;\n};\n\nexport interface EventState {\n events: OHEvent[];\n eventIds: Set<string | number>;\n uiEvents: OHEvent[];\n addEvent: (event: OHEvent) => void;\n /**\n * Bulk-insert events. Used for the initial REST history load and for\n * \"scroll up to load older\" pagination. Newly-added events are de-duped\n * against the existing store and the combined list is re-sorted by\n * timestamp so older pages drop into the correct position.\n */\n addEvents: (events: OHEvent[]) => void;\n clearEvents: () => void;\n}\n\nconst appendEvent = (state: EventState, event: OHEvent): EventState => {\n // Deduplicate: skip if event with same id already exists (O(1) lookup)\n const eventId = getEventId(event);\n if (eventId !== undefined && state.eventIds.has(eventId)) {\n return state;\n }\n\n const newEventIds =\n eventId !== undefined\n ? new Set(state.eventIds).add(eventId)\n : state.eventIds;\n\n return {\n ...state,\n events: [...state.events, event],\n eventIds: newEventIds,\n uiEvents: handleEventForUI(event, state.uiEvents),\n };\n};\n\nconst sortEventState = (state: EventState): EventState => ({\n ...state,\n events: [...state.events].sort(compareEventsByTimestamp),\n uiEvents: [...state.uiEvents].sort(compareEventsByTimestamp),\n});\n\nconst applyAddEvent = (state: EventState, event: OHEvent): EventState => {\n const next = appendEvent(state, event);\n if (next === state) {\n return state;\n }\n\n if (\n !needsSorting(state.events, event) &&\n !needsSorting(state.uiEvents, event)\n ) {\n return next;\n }\n\n return sortEventState(next);\n};\n\nexport const useEventStore = create<EventState>()((set) => ({\n events: [],\n eventIds: new Set(),\n uiEvents: [],\n addEvent: (event: OHEvent) => set((state) => applyAddEvent(state, event)),\n addEvents: (incoming: OHEvent[]) =>\n set((state) => {\n if (incoming.length === 0) return state;\n\n const eventIds = new Set(state.eventIds);\n const events = [...state.events];\n let uiEvents = [...state.uiEvents];\n let added = false;\n\n for (const event of incoming) {\n const eventId = getEventId(event);\n const isDuplicate = eventId !== undefined && eventIds.has(eventId);\n\n if (!isDuplicate) {\n added = true;\n if (eventId !== undefined) {\n eventIds.add(eventId);\n }\n events.push(event);\n uiEvents = handleEventForUI(event, uiEvents);\n }\n }\n\n if (!added) {\n return state;\n }\n\n return sortEventState({\n ...state,\n events,\n eventIds,\n uiEvents,\n });\n }),\n clearEvents: () =>\n set(() => ({\n events: [],\n eventIds: new Set(),\n uiEvents: [],\n })),\n}));\n\n// In dev builds, expose the store on `window` so that fixture/preview\n// scripts (e.g. .pr/issue-132 demo capture) can inject synthetic events\n// without round-tripping through the agent-server. Tree-shaken in\n// production builds via `import.meta.env.DEV`.\nif (\n typeof window !== \"undefined\" &&\n typeof import.meta !== \"undefined\" &&\n (import.meta as { env?: { DEV?: boolean } }).env?.DEV\n) {\n (\n window as unknown as { __OH_EVENT_STORE__?: typeof useEventStore }\n ).__OH_EVENT_STORE__ = useEventStore;\n}\n"],"mappings":";;;AAQA,IAAM,KAAc,MAClB,QAAQ,IAAQ,EAAM,KAAK,KAAA,GAEvB,KAAqB,MACzB,eAAe,IAAQ,EAAM,YAAY,KAAA,GAMrC,KAA4B,GAAY,MAAuB;CACnE,IAAM,IAAa,EAAkB,EAAE,EACjC,IAAa,EAAkB,EAAE;AAQvC,QALI,CAAC,KAAc,CAAC,IAAmB,IAClC,IACA,IAGE,EAAW,cAAc,EAAW,GAHnB,KADA;GAWpB,KAAgB,GAAmB,MAA+B;AACtE,KAAI,EAAO,WAAW,EAAG,QAAO;CAEhC,IAAM,IAAY,EAAO,EAAO,SAAS,IACnC,IAAgB,EAAkB,EAAU,EAC5C,IAAe,EAAkB,EAAS;AAMhD,QAHI,CAAC,KAAiB,CAAC,IAAqB,KAGrC,IAAe;
|
|
1
|
+
{"version":3,"file":"use-event-store.js","names":[],"sources":["../../src/stores/use-event-store.ts"],"sourcesContent":["import { create } from \"zustand\";\nimport { OpenHandsEvent } from \"#/types/agent-server/core\";\nimport { handleEventForUI } from \"#/utils/handle-event-for-ui\";\n\nexport type OHEvent = OpenHandsEvent & {\n isFromPlanningAgent?: boolean;\n};\n\nconst getEventId = (event: OHEvent): string | number | undefined =>\n \"id\" in event ? event.id : undefined;\n\nconst getEventTimestamp = (event: OHEvent): string | undefined =>\n \"timestamp\" in event ? event.timestamp : undefined;\n\n/**\n * Compare two events by timestamp for sorting.\n * Events without timestamps are placed at the end.\n */\nconst compareEventsByTimestamp = (a: OHEvent, b: OHEvent): number => {\n const timestampA = getEventTimestamp(a);\n const timestampB = getEventTimestamp(b);\n\n // Events without timestamps go to the end\n if (!timestampA && !timestampB) return 0;\n if (!timestampA) return 1;\n if (!timestampB) return -1;\n\n // Compare ISO timestamp strings (lexicographic comparison works for ISO format)\n return timestampA.localeCompare(timestampB);\n};\n\n/**\n * Check if the new event needs sorting (i.e., it's out of order).\n * Returns true if the new event's timestamp is earlier than the last event's timestamp.\n */\nconst needsSorting = (events: OHEvent[], newEvent: OHEvent): boolean => {\n if (events.length === 0) return false;\n\n const lastEvent = events[events.length - 1];\n const lastTimestamp = getEventTimestamp(lastEvent);\n const newTimestamp = getEventTimestamp(newEvent);\n\n // If either event doesn't have a timestamp, don't sort\n if (!lastTimestamp || !newTimestamp) return false;\n\n // Sort needed if new event's timestamp is earlier than last event's timestamp\n return newTimestamp < lastTimestamp;\n};\n\nexport interface EventState {\n events: OHEvent[];\n eventIds: Set<string | number>;\n uiEvents: OHEvent[];\n /**\n * The conversation whose events currently populate the store. The store is\n * global (not keyed by conversation), so the conversation route uses this to\n * tell a genuine conversation switch apart from a remount of the *same*\n * conversation (e.g. navigating to Settings and back) — only the former\n * should clear the accumulated events.\n */\n loadedConversationId: string | null;\n addEvent: (event: OHEvent) => void;\n /**\n * Bulk-insert events. Used for the initial REST history load and for\n * \"scroll up to load older\" pagination. Newly-added events are de-duped\n * against the existing store and the combined list is re-sorted by\n * timestamp so older pages drop into the correct position.\n */\n addEvents: (events: OHEvent[]) => void;\n /**\n * Clear all events. Also resets `loadedConversationId` to `null` so the\n * store never claims to hold a conversation whose events have been wiped —\n * the invariant (`loadedConversationId` reflects the conversation whose\n * events are in the arrays) holds even for a standalone clear.\n */\n clearEvents: () => void;\n /**\n * Atomically clear all events and record which conversation is now loaded.\n * Collapsing the reset and the bookkeeping into a single `set` keeps the\n * store invariant enforced at the boundary, rather than relying on every\n * call-site to invoke a clear and a `loadedConversationId` setter in the\n * right order.\n */\n clearEventsForConversation: (conversationId: string | null) => void;\n}\n\nconst appendEvent = (state: EventState, event: OHEvent): EventState => {\n // Deduplicate: skip if event with same id already exists (O(1) lookup)\n const eventId = getEventId(event);\n if (eventId !== undefined && state.eventIds.has(eventId)) {\n return state;\n }\n\n const newEventIds =\n eventId !== undefined\n ? new Set(state.eventIds).add(eventId)\n : state.eventIds;\n\n return {\n ...state,\n events: [...state.events, event],\n eventIds: newEventIds,\n uiEvents: handleEventForUI(event, state.uiEvents),\n };\n};\n\nconst sortEventState = (state: EventState): EventState => ({\n ...state,\n events: [...state.events].sort(compareEventsByTimestamp),\n uiEvents: [...state.uiEvents].sort(compareEventsByTimestamp),\n});\n\nconst applyAddEvent = (state: EventState, event: OHEvent): EventState => {\n const next = appendEvent(state, event);\n if (next === state) {\n return state;\n }\n\n if (\n !needsSorting(state.events, event) &&\n !needsSorting(state.uiEvents, event)\n ) {\n return next;\n }\n\n return sortEventState(next);\n};\n\nexport const useEventStore = create<EventState>()((set) => ({\n events: [],\n eventIds: new Set(),\n uiEvents: [],\n loadedConversationId: null,\n addEvent: (event: OHEvent) => set((state) => applyAddEvent(state, event)),\n addEvents: (incoming: OHEvent[]) =>\n set((state) => {\n if (incoming.length === 0) return state;\n\n const eventIds = new Set(state.eventIds);\n const events = [...state.events];\n let uiEvents = [...state.uiEvents];\n let added = false;\n\n for (const event of incoming) {\n const eventId = getEventId(event);\n const isDuplicate = eventId !== undefined && eventIds.has(eventId);\n\n if (!isDuplicate) {\n added = true;\n if (eventId !== undefined) {\n eventIds.add(eventId);\n }\n events.push(event);\n uiEvents = handleEventForUI(event, uiEvents);\n }\n }\n\n if (!added) {\n return state;\n }\n\n return sortEventState({\n ...state,\n events,\n eventIds,\n uiEvents,\n });\n }),\n clearEvents: () =>\n set(() => ({\n events: [],\n eventIds: new Set(),\n uiEvents: [],\n loadedConversationId: null,\n })),\n clearEventsForConversation: (conversationId: string | null) =>\n set(() => ({\n events: [],\n eventIds: new Set(),\n uiEvents: [],\n loadedConversationId: conversationId,\n })),\n}));\n\n// In dev builds, expose the store on `window` so that fixture/preview\n// scripts (e.g. .pr/issue-132 demo capture) can inject synthetic events\n// without round-tripping through the agent-server. Tree-shaken in\n// production builds via `import.meta.env.DEV`.\nif (\n typeof window !== \"undefined\" &&\n typeof import.meta !== \"undefined\" &&\n (import.meta as { env?: { DEV?: boolean } }).env?.DEV\n) {\n (\n window as unknown as { __OH_EVENT_STORE__?: typeof useEventStore }\n ).__OH_EVENT_STORE__ = useEventStore;\n}\n"],"mappings":";;;AAQA,IAAM,KAAc,MAClB,QAAQ,IAAQ,EAAM,KAAK,KAAA,GAEvB,KAAqB,MACzB,eAAe,IAAQ,EAAM,YAAY,KAAA,GAMrC,KAA4B,GAAY,MAAuB;CACnE,IAAM,IAAa,EAAkB,EAAE,EACjC,IAAa,EAAkB,EAAE;AAQvC,QALI,CAAC,KAAc,CAAC,IAAmB,IAClC,IACA,IAGE,EAAW,cAAc,EAAW,GAHnB,KADA;GAWpB,KAAgB,GAAmB,MAA+B;AACtE,KAAI,EAAO,WAAW,EAAG,QAAO;CAEhC,IAAM,IAAY,EAAO,EAAO,SAAS,IACnC,IAAgB,EAAkB,EAAU,EAC5C,IAAe,EAAkB,EAAS;AAMhD,QAHI,CAAC,KAAiB,CAAC,IAAqB,KAGrC,IAAe;GAwClB,KAAe,GAAmB,MAA+B;CAErE,IAAM,IAAU,EAAW,EAAM;AACjC,KAAI,MAAY,KAAA,KAAa,EAAM,SAAS,IAAI,EAAQ,CACtD,QAAO;CAGT,IAAM,IACJ,MAAY,KAAA,IAER,EAAM,WADN,IAAI,IAAI,EAAM,SAAS,CAAC,IAAI,EAAQ;AAG1C,QAAO;EACL,GAAG;EACH,QAAQ,CAAC,GAAG,EAAM,QAAQ,EAAM;EAChC,UAAU;EACV,UAAU,EAAiB,GAAO,EAAM,SAAS;EAClD;GAGG,KAAkB,OAAmC;CACzD,GAAG;CACH,QAAQ,CAAC,GAAG,EAAM,OAAO,CAAC,KAAK,EAAyB;CACxD,UAAU,CAAC,GAAG,EAAM,SAAS,CAAC,KAAK,EAAyB;CAC7D,GAEK,KAAiB,GAAmB,MAA+B;CACvE,IAAM,IAAO,EAAY,GAAO,EAAM;AAYtC,QAXI,MAAS,IACJ,IAIP,CAAC,EAAa,EAAM,QAAQ,EAAM,IAClC,CAAC,EAAa,EAAM,UAAU,EAAM,GAE7B,IAGF,EAAe,EAAK;GAGhB,IAAgB,GAAoB,EAAE,OAAS;CAC1D,QAAQ,EAAE;CACV,0BAAU,IAAI,KAAK;CACnB,UAAU,EAAE;CACZ,sBAAsB;CACtB,WAAW,MAAmB,GAAK,MAAU,EAAc,GAAO,EAAM,CAAC;CACzE,YAAY,MACV,GAAK,MAAU;AACb,MAAI,EAAS,WAAW,EAAG,QAAO;EAElC,IAAM,IAAW,IAAI,IAAI,EAAM,SAAS,EAClC,IAAS,CAAC,GAAG,EAAM,OAAO,EAC5B,IAAW,CAAC,GAAG,EAAM,SAAS,EAC9B,IAAQ;AAEZ,OAAK,IAAM,KAAS,GAAU;GAC5B,IAAM,IAAU,EAAW,EAAM;AAGjC,GAFoB,MAAY,KAAA,KAAa,EAAS,IAAI,EAAQ,KAGhE,IAAQ,IACJ,MAAY,KAAA,KACd,EAAS,IAAI,EAAQ,EAEvB,EAAO,KAAK,EAAM,EAClB,IAAW,EAAiB,GAAO,EAAS;;AAQhD,SAJK,IAIE,EAAe;GACpB,GAAG;GACH;GACA;GACA;GACD,CAAC,GARO;GAST;CACJ,mBACE,SAAW;EACT,QAAQ,EAAE;EACV,0BAAU,IAAI,KAAK;EACnB,UAAU,EAAE;EACZ,sBAAsB;EACvB,EAAE;CACL,6BAA6B,MAC3B,SAAW;EACT,QAAQ,EAAE;EACV,0BAAU,IAAI,KAAK;EACnB,UAAU,EAAE;EACZ,sBAAsB;EACvB,EAAE;CACN,EAAE"}
|
|
@@ -5,7 +5,7 @@ declare const contextMenuVariants: (props?: ({
|
|
|
5
5
|
size?: "default" | "compact" | null | undefined;
|
|
6
6
|
layout?: "vertical" | null | undefined;
|
|
7
7
|
position?: "none" | "top" | "bottom" | null | undefined;
|
|
8
|
-
spacing?: "
|
|
8
|
+
spacing?: "default" | "none" | null | undefined;
|
|
9
9
|
alignment?: "none" | "left" | "right" | null | undefined;
|
|
10
10
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
11
11
|
interface ContextMenuProps {
|
package/dist/ui/help-link.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type VariantProps } from "class-variance-authority";
|
|
2
2
|
declare const helpLinkVariants: (props?: ({
|
|
3
|
-
size?: "
|
|
3
|
+
size?: "default" | "settings" | null | undefined;
|
|
4
4
|
linkColor?: "default" | "white" | null | undefined;
|
|
5
5
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
6
|
interface HelpLinkProps extends VariantProps<typeof helpLinkVariants> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openhands/agent-canvas",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.9",
|
|
4
4
|
"description": "Agent Canvas UI for OpenHands - run AI coding agents with a visual interface",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"@heroui/react": "2.8.10",
|
|
24
24
|
"@microlink/react-json-view": "1.31.20",
|
|
25
25
|
"@monaco-editor/react": "4.7.0",
|
|
26
|
-
"@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#
|
|
27
|
-
"@openhands/typescript-client": "1.24.
|
|
26
|
+
"@openhands/extensions": "git+https://github.com/OpenHands/extensions.git#e14f740c59b4bfd7369d4bb6aea5eeb33dd05909",
|
|
27
|
+
"@openhands/typescript-client": "1.24.3",
|
|
28
28
|
"@react-router/node": "7.14.2",
|
|
29
29
|
"@react-router/serve": "7.14.2",
|
|
30
30
|
"@tailwindcss/vite": "4.2.4",
|
package/scripts/dev-safe.mjs
CHANGED
|
@@ -210,6 +210,36 @@ function tryPort(port, host = "127.0.0.1") {
|
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Assert that all listed ports are available, throwing a descriptive error if
|
|
215
|
+
* any are already in use.
|
|
216
|
+
*
|
|
217
|
+
* Intended as a pre-flight check before spawning services so that a concurrent
|
|
218
|
+
* agent-canvas instance is detected immediately rather than silently starting
|
|
219
|
+
* on a different port.
|
|
220
|
+
*
|
|
221
|
+
* @param {Array<{name: string, port: number}>} portConfigs - Named port list
|
|
222
|
+
* @param {string} [host]
|
|
223
|
+
*/
|
|
224
|
+
export async function assertPortsFree(portConfigs, host = "127.0.0.1") {
|
|
225
|
+
const results = await Promise.all(
|
|
226
|
+
portConfigs.map(async ({ name, port }) => ({
|
|
227
|
+
name,
|
|
228
|
+
port,
|
|
229
|
+
free: await tryPort(port, host),
|
|
230
|
+
})),
|
|
231
|
+
);
|
|
232
|
+
const busy = results.filter(({ free }) => !free);
|
|
233
|
+
if (busy.length === 0) return;
|
|
234
|
+
|
|
235
|
+
const lines = busy.map(({ name, port }) => ` • ${name}: port ${port}`).join("\n");
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Cannot start: the following ports are already in use:\n\n${lines}\n\n` +
|
|
238
|
+
`Another agent-canvas instance may already be running.\n` +
|
|
239
|
+
`Stop it first, or override the port via environment variables (e.g. PORT=<other>).`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
213
243
|
/**
|
|
214
244
|
* Find multiple free ports at once, each preferring its specified default.
|
|
215
245
|
*
|
|
@@ -491,26 +521,14 @@ export async function buildSafeDevConfigAsync(
|
|
|
491
521
|
preferredBackendPort + 1,
|
|
492
522
|
);
|
|
493
523
|
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
{ name: "
|
|
497
|
-
{ name: "vscode",
|
|
524
|
+
// Fail fast if any required port is already in use.
|
|
525
|
+
await assertPortsFree([
|
|
526
|
+
{ name: "agent-server", port: preferredBackendPort },
|
|
527
|
+
{ name: "vscode", port: preferredVscodePort },
|
|
498
528
|
]);
|
|
499
529
|
|
|
500
|
-
// Log if we're using non-default ports
|
|
501
|
-
if (ports.backend !== preferredBackendPort) {
|
|
502
|
-
console.log(
|
|
503
|
-
` ℹ Port ${preferredBackendPort} busy, using ${ports.backend} for agent-server`,
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
if (ports.vscode !== preferredVscodePort) {
|
|
507
|
-
console.log(
|
|
508
|
-
` ℹ Port ${preferredVscodePort} busy, using ${ports.vscode} for vscode`,
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
530
|
return buildConfigFromPorts(
|
|
513
|
-
{ backendPort:
|
|
531
|
+
{ backendPort: preferredBackendPort, vscodePort: preferredVscodePort },
|
|
514
532
|
cwd,
|
|
515
533
|
env,
|
|
516
534
|
);
|
|
@@ -51,13 +51,13 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
51
51
|
import process from "node:process";
|
|
52
52
|
|
|
53
53
|
import {
|
|
54
|
+
assertPortsFree,
|
|
54
55
|
buildAgentServerCommand,
|
|
55
56
|
buildSafeDevConfig,
|
|
56
57
|
buildAgentServerEnv,
|
|
57
58
|
buildNpmScriptCommand,
|
|
58
59
|
buildRuntimeServicesInfo,
|
|
59
60
|
formatMissingUvxGuidance,
|
|
60
|
-
findFreePorts,
|
|
61
61
|
getOrCreatePersistedApiKey,
|
|
62
62
|
validateFrontendDependencies,
|
|
63
63
|
validateLocalAgentServerPath,
|
|
@@ -281,52 +281,27 @@ async function buildConfig(args, env = process.env) {
|
|
|
281
281
|
env.OH_AUTOMATION_REPO = args.automationRepo;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
// Preferred ports (from env or defaults)
|
|
284
|
+
// Preferred ports (from env or defaults).
|
|
285
|
+
// OH_CANVAS_SAFE_BACKEND_PORT / OH_CANVAS_SAFE_AUTOMATION_PORT /
|
|
286
|
+
// OH_CANVAS_SAFE_VITE_PORT allow tests (and advanced users) to redirect
|
|
287
|
+
// internal service ports without affecting the production default.
|
|
285
288
|
const preferredIngressPort = args.port || parseInt(env.PORT, 10) || 8000;
|
|
286
|
-
const preferredBackendPort =
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
{ name: "
|
|
296
|
-
{ name: "
|
|
289
|
+
const preferredBackendPort =
|
|
290
|
+
parseInt(env.OH_CANVAS_SAFE_BACKEND_PORT, 10) || DEFAULT_BACKEND_PORT;
|
|
291
|
+
const preferredAutomationPort =
|
|
292
|
+
parseInt(env.OH_CANVAS_SAFE_AUTOMATION_PORT, 10) || DEFAULT_AUTOMATION_PORT;
|
|
293
|
+
const preferredVitePort = parseInt(env.OH_CANVAS_SAFE_VITE_PORT, 10) || 3001;
|
|
294
|
+
|
|
295
|
+
// Fail fast if any preferred port is already in use.
|
|
296
|
+
logStep("ports", "Checking ports...");
|
|
297
|
+
await assertPortsFree([
|
|
298
|
+
{ name: "ingress", port: preferredIngressPort },
|
|
299
|
+
{ name: "agent-server", port: preferredBackendPort },
|
|
300
|
+
{ name: "automation", port: preferredAutomationPort },
|
|
301
|
+
{ name: "vite", port: preferredVitePort },
|
|
297
302
|
]);
|
|
298
303
|
|
|
299
|
-
|
|
300
|
-
if (ports.ingress !== preferredIngressPort) {
|
|
301
|
-
logService(
|
|
302
|
-
"ports",
|
|
303
|
-
`Port ${preferredIngressPort} busy, using ${ports.ingress} for ingress`,
|
|
304
|
-
c.yellow,
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
if (ports.backend !== preferredBackendPort) {
|
|
308
|
-
logService(
|
|
309
|
-
"ports",
|
|
310
|
-
`Port ${preferredBackendPort} busy, using ${ports.backend} for agent-server`,
|
|
311
|
-
c.yellow,
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
if (ports.automation !== preferredAutomationPort) {
|
|
315
|
-
logService(
|
|
316
|
-
"ports",
|
|
317
|
-
`Port ${preferredAutomationPort} busy, using ${ports.automation} for automation`,
|
|
318
|
-
c.yellow,
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
if (ports.vite !== preferredVitePort) {
|
|
322
|
-
logService(
|
|
323
|
-
"ports",
|
|
324
|
-
`Port ${preferredVitePort} busy, using ${ports.vite} for vite`,
|
|
325
|
-
c.yellow,
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const vscodePort = ports.backend + 1000;
|
|
304
|
+
const vscodePort = preferredBackendPort + 1000;
|
|
330
305
|
|
|
331
306
|
// Session API key — shared by both agent-server and automation backend.
|
|
332
307
|
// Both validate it via the `X-Session-API-Key` header.
|
|
@@ -336,19 +311,19 @@ async function buildConfig(args, env = process.env) {
|
|
|
336
311
|
const safeConfig = buildSafeDevConfig(projectRoot, {
|
|
337
312
|
...env,
|
|
338
313
|
OH_CANVAS_SAFE_STATE_DIR: stateDir,
|
|
339
|
-
OH_CANVAS_SAFE_BACKEND_PORT:
|
|
314
|
+
OH_CANVAS_SAFE_BACKEND_PORT: preferredBackendPort.toString(),
|
|
340
315
|
OH_CANVAS_SAFE_VSCODE_PORT: vscodePort.toString(),
|
|
341
316
|
});
|
|
342
317
|
const sessionApiKey = safeConfig.sessionApiKey;
|
|
343
318
|
|
|
344
319
|
return {
|
|
345
320
|
// Ingress port (main entry point)
|
|
346
|
-
ingressPort:
|
|
321
|
+
ingressPort: preferredIngressPort,
|
|
347
322
|
|
|
348
323
|
// Service ports (internal)
|
|
349
|
-
agentServerPort:
|
|
350
|
-
autoBackendPort:
|
|
351
|
-
vitePort:
|
|
324
|
+
agentServerPort: preferredBackendPort,
|
|
325
|
+
autoBackendPort: preferredAutomationPort,
|
|
326
|
+
vitePort: preferredVitePort,
|
|
352
327
|
vscodePort,
|
|
353
328
|
|
|
354
329
|
// Paths
|