@tea-agent/loop-agent 0.40.0-next.3 → 0.40.0-next.5
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/CHANGELOG.md +21 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +46 -5
- package/dist/executors/shell-executor.js +28 -0
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/infrastructure/harness/atomic-write.js +23 -0
- package/dist/task/config-types.js +10 -0
- package/dist/worker/console/chat/chat-event-store.js +62 -4
- package/dist/worker/console/chat/context-panel.js +7 -0
- package/dist/worker/console/chat/deferred-turn.js +12 -0
- package/dist/worker/console/chat/pi-runtime.js +264 -16
- package/dist/worker/console/chat/provider-error.js +98 -0
- package/dist/worker/console/chat/routes.js +292 -29
- package/dist/worker/console/chat/session-stats.js +179 -0
- package/dist/worker/console/chat/session-store.js +93 -63
- package/dist/worker/console/chat/todos.js +115 -0
- package/dist/worker/console/chat/tool-preview.js +42 -0
- package/dist/worker/console/chat/turn-process.js +20 -55
- package/dist/worker/console/chat/user-questions.js +266 -0
- package/dist/worker/console/chat/workspace-landing.js +14 -0
- package/dist/worker/console/native-directory-picker.js +13 -0
- package/dist/worker/console/routes.js +309 -1
- package/dist/worker/console/security.js +4 -4
- package/dist/worker/console/server.js +529 -186
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-CTx1TrNa.js → abnfDiagram-N423BO3Z-CGZrpj0K.js} +1 -1
- package/dist/worker/console/static/assets/{arc-B4i9cVxX.js → arc-CwEyA8iq.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BAXE8_Rl.js → architectureDiagram-T3A2C74G-DLTkrEQ3.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C5Xh59at.js → blockDiagram-VBNYF7ZC-4w12zR3t.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B_asOzAu.js → c4Diagram-5PPSVZJV-BppqLAjk.js} +1 -1
- package/dist/worker/console/static/assets/channel-TzuwQha5.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-wRO_iRNc.js → chunk-2GRJ4B5K-DO6U26Pm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ByOaSh_G.js → chunk-2Q5K7J3B-DC7YEdOy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLzudan.js → chunk-5RXB4S5H-DtMYVQBw.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-JCNGkXYT.js → chunk-5VM5RSS4-C5Ed-57N.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BhrPXXMZ.js → chunk-6Q2QTUOP-BUY4oIzT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-TJ5JuEBT.js → chunk-GF5L2VYU-VnWHALIV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-VVthn_mD.js → chunk-JWPE2WC7-COV-RQk4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D60-Mny4.js → chunk-KBJHAD2P-dSnLtUiz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-zXv7aVye.js → chunk-RYQCIY6F-CX6FDzWh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-By9VQoi1.js → chunk-XXDRQBXY-D3mEgb6W.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DCu69b3S.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DCu69b3S.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C4zQLe-M.js → cose-bilkent-JH36ORCC-QQBMUnAf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-wh5N2ReN.js → cynefin-VYW2F7L2-BxSD-LVV.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Dlu5wIqs.js → cynefinDiagram-MW4NZA55-DxHNiNbk.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CrDgthc6.js → dagre-VZM6K2ZE-C83bYy8E.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zN1clg27.js → diagram-7IWD3JNH-BhtZu2YY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BN-o7J7-.js → diagram-B4RE2ZJO-K0P_ntRE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C8SuPt0d.js → diagram-LBJQPF4R-CXYy1Nib.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BoizuHbZ.js → diagram-Q27KOJAE-D-CtIFkU.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BzUzXixG.js → diagram-UB23O5K3-BEK9-_7c.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js → ebnfDiagram-BXEA7PRR-L0EX-9wn.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-GALUzRbs.js → erDiagram-JOGREHBK-9pt0fc_5.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CZJN65Sb.js → flowDiagram-UKHOOZJN-CZVeM_n1.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-JLB9OESl.js → ganttDiagram-PKOTCBZU-DqUKkAZP.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CcvCr4GD.js → gitGraphDiagram-DS77QQ5N-BYUuXEgp.js} +1 -1
- package/dist/worker/console/static/assets/index-B0jGzQj6.js +407 -0
- package/dist/worker/console/static/assets/index-BiYbPiTw.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-HG5z7aVg.js → infoDiagram-6WML65LV-DNzKk7ah.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BHEu9chR.js → ishikawaDiagram-WSZJBQD7-Clbyg5IP.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Cp_Up8-a.js → journeyDiagram-NVQOT4AX-zDO_gZNO.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DvVeY-em.js → kanban-definition-27J2QSJJ-x3LpRYek.js} +1 -1
- package/dist/worker/console/static/assets/{linear-aGmcjgTn.js → linear-CTpwZqKR.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Biyo-tqA.js → mermaid.core-D_WXK6C7.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DyxY2pIb.js → mindmap-definition-FAOFIHXS-CWK-OYP5.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkdGuc50.js → pegDiagram-VL7TDLO6-B9gtUPsM.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-NNQI9xVR.js → pieDiagram-7S7Q4E2Y-Dj2lzceR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-PAJkuz_G.js → quadrantDiagram-CIZ2JOQS-Nwljut7m.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-ZwrQod0O.js → railroadDiagram-AXF67PYL-BY-KNt93.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D3ZfrzM2.js → requirementDiagram-LRYGKXZP-Vx0wvpPy.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DDGFmWk5.js → sankeyDiagram-W5VNT64P-B6grNXUk.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DO6mmXTA.js → sequenceDiagram-SI44F4Z6-CteidBvh.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DMbCQiJt.js → sizeCapture-X5ZJPWSS-Du4W8mFK.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Di_YUEfC.js → stateDiagram-OKZ733FA-jyI6Yh81.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyQblISl.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Bn2PphNJ.js → swimlanes-SLNWSIFB-DYWs_fRv.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DiygmgYP.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DPh8pRsp.js → timeline-definition-Z64GVDOM-DO5LLy47.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-GMfl26pW.js → vennDiagram-T6HMQDX7-CLF2p1_L.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DTlmVrmF.js → wardleyDiagram-T6FBY63Y-AwTaYBxA.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YGBjKAS5.js → xychartDiagram-ELKLHX3M-DzbblQRw.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +29 -4
- package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +36 -6
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
- package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +2 -2
- package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
- package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -17
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
- package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
- package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
- package/dist/worker/console/workspace-context.js +234 -0
- package/dist/worker/console/workspace-registry.js +214 -0
- package/dist/worker/observe/server.js +4 -0
- package/dist/worker/observe/static/api.js +26 -6
- package/dist/worker/observe/static/app.js +18 -70
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/inspect-workspace.js +307 -0
- package/dist/worker/observe/static/operator-chrome.css +145 -20
- package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
- package/dist/worker/observe/static/operator-chrome.js +330 -100
- package/dist/worker/observe/static/router.d.ts +46 -0
- package/dist/worker/observe/static/router.js +25 -1
- package/dist/worker/observe/static/state.d.ts +50 -0
- package/dist/worker/observe/static/state.js +172 -0
- package/dist/worker/observe/static/styles.css +20 -1
- package/dist/worker/observe/static/views/dag-inspector.js +4 -3
- package/dist/worker/observe/static/views/dag.js +2 -1
- package/dist/worker/observe/static/views/dashboard.js +6 -4
- package/dist/worker/observe/static/views/night.js +2 -2
- package/dist/worker/observe/static/views/pool.js +1 -0
- package/dist/worker/observe/static/views/run.js +3 -1
- package/dist/worker/observe/static/views/task.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +19 -0
- package/dist/workflows/dag/interrupt-request.js +3 -3
- package/dist/workflows/dag/lifecycle.js +3 -3
- package/dist/workflows/dag/types.js +3 -1
- package/docs/README.md +1 -0
- package/docs/operations/local-development-environment.md +21 -0
- package/harness.json +3 -3
- package/package.json +3 -1
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +0 -1
- package/dist/worker/console/static/assets/index-CW88Fjbw.js +0 -406
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +0 -8
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { confirmationToken } from "../operator-chat/format.js";
|
|
3
|
+
import { describeMutationGateFailure, isMutationGateFailureCode, } from "../operator-chat/mutation-gate.js";
|
|
4
|
+
/**
|
|
5
|
+
* Multi-workspace registry state (2026-08-23, AC-001/AC-003).
|
|
6
|
+
*
|
|
7
|
+
* Consumes the process-level GET/POST/DELETE /api/operator/v1/workspaces
|
|
8
|
+
* surface. POST/DELETE pass the Console mutation-gate confirmation token.
|
|
9
|
+
* The selected workspaceId is remembered in localStorage and validated
|
|
10
|
+
* against the visible list. A stale local preference falls back to the first
|
|
11
|
+
* visible workspace; an explicit unknown URL id remains fail-closed in the
|
|
12
|
+
* route layer.
|
|
13
|
+
*
|
|
14
|
+
* The network/state machine lives in createWorkspacesController (headless,
|
|
15
|
+
* node-testable); useWorkspaces is a thin React subscription wrapper.
|
|
16
|
+
*/
|
|
17
|
+
export const ACTIVE_WORKSPACE_STORAGE_KEY = "loop-console.operator-shell.active-workspace.v1";
|
|
18
|
+
export function readStoredWorkspaceId(storage) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = storage?.getItem(ACTIVE_WORKSPACE_STORAGE_KEY)?.trim();
|
|
21
|
+
return raw || null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function rememberWorkspaceId(workspaceId, storage) {
|
|
28
|
+
try {
|
|
29
|
+
if (workspaceId)
|
|
30
|
+
storage?.setItem(ACTIVE_WORKSPACE_STORAGE_KEY, workspaceId);
|
|
31
|
+
else
|
|
32
|
+
storage?.removeItem(ACTIVE_WORKSPACE_STORAGE_KEY);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* localStorage unavailable: in-memory selection still works. */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function pickDefaultWorkspaceId(workspaces, storedId) {
|
|
39
|
+
if (storedId) {
|
|
40
|
+
if (workspaces.some((w) => w.workspaceId === storedId))
|
|
41
|
+
return { status: "ok", workspaceId: storedId };
|
|
42
|
+
return { status: "ok", workspaceId: workspaces[0]?.workspaceId ?? null };
|
|
43
|
+
}
|
|
44
|
+
return { status: "ok", workspaceId: workspaces[0]?.workspaceId ?? null };
|
|
45
|
+
}
|
|
46
|
+
export function describeWorkspaceAddError(status, code, message) {
|
|
47
|
+
if (status === 409 || code === "DUPLICATE") {
|
|
48
|
+
return (message ||
|
|
49
|
+
"该目录(或等价路径)已存在为 Workspace:请直接选择既有条目,或更换其他真实目录。");
|
|
50
|
+
}
|
|
51
|
+
if (status === 400 || code === "INVALID_PATH") {
|
|
52
|
+
return message || "目录不可用:请确认输入的是存在的真实目录绝对路径。";
|
|
53
|
+
}
|
|
54
|
+
if (isMutationGateFailureCode(code)) {
|
|
55
|
+
return describeMutationGateFailure(code);
|
|
56
|
+
}
|
|
57
|
+
return message || `添加 Workspace 失败(HTTP ${status})`;
|
|
58
|
+
}
|
|
59
|
+
// The headless object also exposes subscribe(); declared via intersection below.
|
|
60
|
+
/** Headless multi-workspace state machine (real request path, no React). */
|
|
61
|
+
export function createWorkspacesController(params = {}) {
|
|
62
|
+
const origin = params.origin ?? "";
|
|
63
|
+
const fetchImpl = params.fetchFn ?? fetch;
|
|
64
|
+
const storage = params.storage;
|
|
65
|
+
const token = params.confirmationTokenFn ?? confirmationToken;
|
|
66
|
+
const listeners = new Set();
|
|
67
|
+
let state = {
|
|
68
|
+
workspaces: [],
|
|
69
|
+
loading: true,
|
|
70
|
+
error: null,
|
|
71
|
+
workspaceId: readStoredWorkspaceId(storage),
|
|
72
|
+
unknownWorkspaceId: null,
|
|
73
|
+
};
|
|
74
|
+
const emit = () => {
|
|
75
|
+
for (const listener of listeners)
|
|
76
|
+
listener();
|
|
77
|
+
};
|
|
78
|
+
const setState = (patch) => {
|
|
79
|
+
state = { ...state, ...patch };
|
|
80
|
+
emit();
|
|
81
|
+
};
|
|
82
|
+
const refresh = async () => {
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetchImpl(`${origin}/api/operator/v1/workspaces`, {
|
|
85
|
+
credentials: "include",
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok)
|
|
88
|
+
return;
|
|
89
|
+
const body = (await res.json());
|
|
90
|
+
const list = body.workspaces ?? [];
|
|
91
|
+
// A stale local preference falls back to the first server-visible
|
|
92
|
+
// Workspace. Explicit route ids are validated separately and remain
|
|
93
|
+
// fail-closed.
|
|
94
|
+
const pick = pickDefaultWorkspaceId(list, state.workspaceId);
|
|
95
|
+
if (pick.status === "unknown-workspace") {
|
|
96
|
+
setState({
|
|
97
|
+
workspaces: list,
|
|
98
|
+
unknownWorkspaceId: pick.workspaceId,
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const nextId = pick.workspaceId;
|
|
103
|
+
const previousId = state.workspaceId;
|
|
104
|
+
setState({
|
|
105
|
+
workspaces: list,
|
|
106
|
+
unknownWorkspaceId: null,
|
|
107
|
+
...(nextId !== previousId ? { workspaceId: nextId } : {}),
|
|
108
|
+
});
|
|
109
|
+
if (nextId !== previousId)
|
|
110
|
+
rememberWorkspaceId(nextId, storage);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* Registry unreachable: keep last known list. */
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
if (state.loading)
|
|
117
|
+
setState({ loading: false });
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const addWorkspace = async (directoryPath, name) => {
|
|
121
|
+
setState({ error: null });
|
|
122
|
+
try {
|
|
123
|
+
const res = await fetchImpl(`${origin}/api/operator/v1/workspaces`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
credentials: "include",
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
"x-loop-console-confirmation": token(),
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({ directoryPath, ...(name ? { name } : {}) }),
|
|
131
|
+
});
|
|
132
|
+
const body = (await res.json().catch(() => ({})));
|
|
133
|
+
if (!res.ok || !body.workspace) {
|
|
134
|
+
const message = describeWorkspaceAddError(res.status, body.error?.code, body.error?.message);
|
|
135
|
+
setState({ error: message });
|
|
136
|
+
return { ok: false, error: message };
|
|
137
|
+
}
|
|
138
|
+
await refresh();
|
|
139
|
+
setState({ workspaceId: body.workspace.workspaceId });
|
|
140
|
+
rememberWorkspaceId(body.workspace.workspaceId, storage);
|
|
141
|
+
return { ok: true, workspaceId: body.workspace.workspaceId };
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
145
|
+
const display = `添加 Workspace 失败:${message}`;
|
|
146
|
+
setState({ error: display });
|
|
147
|
+
return { ok: false, error: display };
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const pickWorkspaceDirectory = async () => {
|
|
151
|
+
setState({ error: null });
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetchImpl(`${origin}/api/operator/v1/workspaces/pick`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
credentials: "include",
|
|
156
|
+
headers: {
|
|
157
|
+
"x-loop-console-confirmation": token(),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const body = (await res.json().catch(() => ({})));
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const message = isMutationGateFailureCode(body.error?.code)
|
|
163
|
+
? describeMutationGateFailure(body.error?.code ?? "")
|
|
164
|
+
: (body.error?.message ?? `打开目录选择窗口失败(HTTP ${res.status})`);
|
|
165
|
+
setState({ error: message });
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return typeof body.directoryPath === "string" && body.directoryPath.trim()
|
|
169
|
+
? body.directoryPath
|
|
170
|
+
: null;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
+
setState({ error: `打开目录选择窗口失败:${message}` });
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const removeWorkspace = async (targetId) => {
|
|
179
|
+
setState({ error: null });
|
|
180
|
+
try {
|
|
181
|
+
const res = await fetchImpl(`${origin}/api/operator/v1/workspaces/${encodeURIComponent(targetId)}`, {
|
|
182
|
+
method: "DELETE",
|
|
183
|
+
credentials: "include",
|
|
184
|
+
headers: {
|
|
185
|
+
"x-loop-console-confirmation": token(),
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const body = (await res.json().catch(() => ({})));
|
|
189
|
+
if (!res.ok) {
|
|
190
|
+
const message = res.status === 404
|
|
191
|
+
? "Workspace 已不存在(可能已被移除),列表将刷新。"
|
|
192
|
+
: isMutationGateFailureCode(body.error?.code)
|
|
193
|
+
? describeMutationGateFailure(body.error?.code ?? "")
|
|
194
|
+
: (body.error?.message ?? `移除 Workspace 失败(HTTP ${res.status})`);
|
|
195
|
+
setState({ error: message });
|
|
196
|
+
await refresh();
|
|
197
|
+
return { ok: false, error: message };
|
|
198
|
+
}
|
|
199
|
+
await refresh();
|
|
200
|
+
// Soft remove keeps historical sessions; if the removed workspace was
|
|
201
|
+
// selected, switch to a remaining one (never a startup-workspace id —
|
|
202
|
+
// only visible registry entries are ever selected).
|
|
203
|
+
if (state.workspaceId === targetId) {
|
|
204
|
+
const nextId = state.workspaces[0]?.workspaceId ?? null;
|
|
205
|
+
setState({ workspaceId: nextId });
|
|
206
|
+
rememberWorkspaceId(nextId, storage);
|
|
207
|
+
}
|
|
208
|
+
return { ok: true };
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
212
|
+
const display = `移除 Workspace 失败:${message}`;
|
|
213
|
+
setState({ error: display });
|
|
214
|
+
return { ok: false, error: display };
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const selectWorkspace = (next) => {
|
|
218
|
+
if (next === state.workspaceId && !state.unknownWorkspaceId)
|
|
219
|
+
return;
|
|
220
|
+
setState({ workspaceId: next, unknownWorkspaceId: null });
|
|
221
|
+
rememberWorkspaceId(next, storage);
|
|
222
|
+
};
|
|
223
|
+
void refresh();
|
|
224
|
+
return {
|
|
225
|
+
get workspaces() {
|
|
226
|
+
return state.workspaces;
|
|
227
|
+
},
|
|
228
|
+
get loading() {
|
|
229
|
+
return state.loading;
|
|
230
|
+
},
|
|
231
|
+
get error() {
|
|
232
|
+
return state.error;
|
|
233
|
+
},
|
|
234
|
+
get workspaceId() {
|
|
235
|
+
return state.workspaceId;
|
|
236
|
+
},
|
|
237
|
+
get unknownWorkspaceId() {
|
|
238
|
+
return state.unknownWorkspaceId;
|
|
239
|
+
},
|
|
240
|
+
refresh,
|
|
241
|
+
pickWorkspaceDirectory,
|
|
242
|
+
addWorkspace,
|
|
243
|
+
removeWorkspace,
|
|
244
|
+
selectWorkspace,
|
|
245
|
+
subscribe(listener) {
|
|
246
|
+
listeners.add(listener);
|
|
247
|
+
return () => listeners.delete(listener);
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** React binding: one controller per mount, state mirrored into useState. */
|
|
252
|
+
export function useWorkspaces(params) {
|
|
253
|
+
const controllerRef = useRef(null);
|
|
254
|
+
if (!controllerRef.current) {
|
|
255
|
+
controllerRef.current = createWorkspacesController(params);
|
|
256
|
+
}
|
|
257
|
+
const controller = controllerRef.current;
|
|
258
|
+
const [state, setState] = useState(() => ({
|
|
259
|
+
workspaces: controller.workspaces,
|
|
260
|
+
loading: controller.loading,
|
|
261
|
+
error: controller.error,
|
|
262
|
+
workspaceId: controller.workspaceId,
|
|
263
|
+
unknownWorkspaceId: controller.unknownWorkspaceId,
|
|
264
|
+
}));
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
const unsubscribe = controller.subscribe(() => {
|
|
267
|
+
setState({
|
|
268
|
+
workspaces: controller.workspaces,
|
|
269
|
+
loading: controller.loading,
|
|
270
|
+
error: controller.error,
|
|
271
|
+
workspaceId: controller.workspaceId,
|
|
272
|
+
unknownWorkspaceId: controller.unknownWorkspaceId,
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
return unsubscribe;
|
|
276
|
+
}, [controller]);
|
|
277
|
+
return useMemo(() => ({
|
|
278
|
+
get workspaces() {
|
|
279
|
+
return state.workspaces;
|
|
280
|
+
},
|
|
281
|
+
get loading() {
|
|
282
|
+
return state.loading;
|
|
283
|
+
},
|
|
284
|
+
get error() {
|
|
285
|
+
return state.error;
|
|
286
|
+
},
|
|
287
|
+
get workspaceId() {
|
|
288
|
+
return state.workspaceId;
|
|
289
|
+
},
|
|
290
|
+
get unknownWorkspaceId() {
|
|
291
|
+
return state.unknownWorkspaceId;
|
|
292
|
+
},
|
|
293
|
+
refresh: controller.refresh,
|
|
294
|
+
pickWorkspaceDirectory: controller.pickWorkspaceDirectory,
|
|
295
|
+
addWorkspace: controller.addWorkspace,
|
|
296
|
+
removeWorkspace: controller.removeWorkspace,
|
|
297
|
+
selectWorkspace: controller.selectWorkspace,
|
|
298
|
+
}), [state, controller]);
|
|
299
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
export function resolveRouteWorkspaceSelection(workspaceId, knownWorkspaceIds) {
|
|
2
|
+
const id = workspaceId?.trim();
|
|
3
|
+
if (!id)
|
|
4
|
+
return { status: "ok", workspaceId: undefined };
|
|
5
|
+
if (knownWorkspaceIds.includes(id))
|
|
6
|
+
return { status: "ok", workspaceId: id };
|
|
7
|
+
return { status: "unknown-workspace", workspaceId: id };
|
|
8
|
+
}
|
|
9
|
+
export const OPERATE_TAB_IDS = [
|
|
10
|
+
"tasks",
|
|
11
|
+
"recovery",
|
|
12
|
+
"night",
|
|
13
|
+
"models",
|
|
14
|
+
"settings",
|
|
15
|
+
];
|
|
16
|
+
export const LAST_OPERATE_TAB_STORAGE_KEY = "loop-console.operator-shell.last-operate-tab.v1";
|
|
17
|
+
export const LAST_INSPECT_HASH_STORAGE_KEY = "loop-console.operator-shell.last-inspect-hash.v1";
|
|
18
|
+
export const DEFAULT_OPERATE_TAB = "tasks";
|
|
19
|
+
export const CHAT_HREF = "/#/chat";
|
|
20
|
+
export const DEFAULT_INSPECT_HREF = "/#/inspect";
|
|
21
|
+
export const DEFAULT_OPERATE_HREF = "/#/operate/tasks";
|
|
22
|
+
const INSPECT_COLLECTION_OR_OBJECT = /^(dag|run|task|feature|batch|night|pool|failures|dags)(\/|$)/;
|
|
23
|
+
export function splitHash(hashValue) {
|
|
24
|
+
const raw = String(hashValue ?? "").replace(/^#/, "");
|
|
25
|
+
const qIndex = raw.indexOf("?");
|
|
26
|
+
const pathPart = qIndex >= 0 ? raw.slice(0, qIndex) : raw;
|
|
27
|
+
const path = pathPart
|
|
28
|
+
? pathPart.startsWith("/")
|
|
29
|
+
? pathPart
|
|
30
|
+
: `/${pathPart}`
|
|
31
|
+
: "/";
|
|
32
|
+
const query = new URLSearchParams(qIndex >= 0 ? raw.slice(qIndex + 1) : "");
|
|
33
|
+
return { path, query };
|
|
34
|
+
}
|
|
35
|
+
export function stripInspectPrefix(path) {
|
|
36
|
+
if (path === "/inspect" || path === "/inspect/")
|
|
37
|
+
return "/";
|
|
38
|
+
if (path.startsWith("/inspect/"))
|
|
39
|
+
return path.slice("/inspect".length) || "/";
|
|
40
|
+
return path;
|
|
41
|
+
}
|
|
42
|
+
function isOperateTab(value) {
|
|
43
|
+
return (value === "tasks" ||
|
|
44
|
+
value === "recovery" ||
|
|
45
|
+
value === "night" ||
|
|
46
|
+
value === "models" ||
|
|
47
|
+
value === "settings");
|
|
48
|
+
}
|
|
49
|
+
function readParam(query, key) {
|
|
50
|
+
const value = query.get(key)?.trim();
|
|
51
|
+
return value || undefined;
|
|
52
|
+
}
|
|
53
|
+
function readWorkspaceParams(query) {
|
|
54
|
+
return {
|
|
55
|
+
...(readParam(query, "taskId") ? { taskId: readParam(query, "taskId") } : {}),
|
|
56
|
+
...(readParam(query, "dagRunId")
|
|
57
|
+
? { dagRunId: readParam(query, "dagRunId") }
|
|
58
|
+
: {}),
|
|
59
|
+
...(readParam(query, "fromNodeId")
|
|
60
|
+
? { fromNodeId: readParam(query, "fromNodeId") }
|
|
61
|
+
: {}),
|
|
62
|
+
...(readParam(query, "featureId")
|
|
63
|
+
? { featureId: readParam(query, "featureId") }
|
|
64
|
+
: {}),
|
|
65
|
+
...(readParam(query, "intent") ? { intent: readParam(query, "intent") } : {}),
|
|
66
|
+
...(readParam(query, "workspaceId")
|
|
67
|
+
? { workspaceId: readParam(query, "workspaceId") }
|
|
68
|
+
: {}),
|
|
69
|
+
...(readParam(query, "sessionId")
|
|
70
|
+
? { sessionId: readParam(query, "sessionId") }
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function paramsQuery(params) {
|
|
75
|
+
const query = new URLSearchParams();
|
|
76
|
+
for (const key of [
|
|
77
|
+
"taskId",
|
|
78
|
+
"dagRunId",
|
|
79
|
+
"fromNodeId",
|
|
80
|
+
"featureId",
|
|
81
|
+
"intent",
|
|
82
|
+
"workspaceId",
|
|
83
|
+
"sessionId",
|
|
84
|
+
]) {
|
|
85
|
+
const value = params[key]?.trim();
|
|
86
|
+
if (value)
|
|
87
|
+
query.set(key, value);
|
|
88
|
+
}
|
|
89
|
+
return query.toString();
|
|
90
|
+
}
|
|
91
|
+
export function readLastOperateTab(storage) {
|
|
92
|
+
try {
|
|
93
|
+
const raw = storage?.getItem(LAST_OPERATE_TAB_STORAGE_KEY);
|
|
94
|
+
if (isOperateTab(raw))
|
|
95
|
+
return raw;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* ignore */
|
|
99
|
+
}
|
|
100
|
+
return DEFAULT_OPERATE_TAB;
|
|
101
|
+
}
|
|
102
|
+
export function rememberOperateTab(tab, storage) {
|
|
103
|
+
try {
|
|
104
|
+
storage?.setItem(LAST_OPERATE_TAB_STORAGE_KEY, tab);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* ignore */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function readLastInspectHref(storage) {
|
|
111
|
+
try {
|
|
112
|
+
const raw = storage?.getItem(LAST_INSPECT_HASH_STORAGE_KEY)?.trim();
|
|
113
|
+
if (raw?.startsWith("/#/inspect"))
|
|
114
|
+
return raw;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* ignore */
|
|
118
|
+
}
|
|
119
|
+
return DEFAULT_INSPECT_HREF;
|
|
120
|
+
}
|
|
121
|
+
export function rememberInspectHref(href, storage) {
|
|
122
|
+
const canonical = href.startsWith("/#/inspect")
|
|
123
|
+
? href
|
|
124
|
+
: rewriteInspectHrefToCanonical(href);
|
|
125
|
+
if (!canonical.startsWith("/#/inspect"))
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
storage?.setItem(LAST_INSPECT_HASH_STORAGE_KEY, canonical);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* ignore */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function buildOperateHref(tab = DEFAULT_OPERATE_TAB, params = {}) {
|
|
135
|
+
const qs = paramsQuery(params);
|
|
136
|
+
return `/#/operate/${tab}${qs ? `?${qs}` : ""}`;
|
|
137
|
+
}
|
|
138
|
+
export function buildInspectHref(inspectPath = "/", query) {
|
|
139
|
+
const normalized = stripInspectPrefix(inspectPath.startsWith("/") ? inspectPath : `/${inspectPath}`);
|
|
140
|
+
const path = normalized === "/" || normalized === ""
|
|
141
|
+
? "/inspect"
|
|
142
|
+
: `/inspect${normalized}`;
|
|
143
|
+
const qs = typeof query === "string"
|
|
144
|
+
? query.replace(/^\?/, "")
|
|
145
|
+
: query?.toString() ?? "";
|
|
146
|
+
return `/#${path}${qs ? `?${qs}` : ""}`;
|
|
147
|
+
}
|
|
148
|
+
export function buildChatHref(params = {}) {
|
|
149
|
+
const qs = paramsQuery(params);
|
|
150
|
+
return qs ? `${CHAT_HREF}?${qs}` : CHAT_HREF;
|
|
151
|
+
}
|
|
152
|
+
/** Keep the selected Workspace on any canonical Console destination without
|
|
153
|
+
* changing its page-specific path or query state. */
|
|
154
|
+
export function withWorkspaceId(href, workspaceId) {
|
|
155
|
+
const canonical = href.startsWith("/#")
|
|
156
|
+
? href
|
|
157
|
+
: href.startsWith("#")
|
|
158
|
+
? `/${href}`
|
|
159
|
+
: rewriteInspectHrefToCanonical(href);
|
|
160
|
+
const { path, query } = splitHash(canonical.slice(2));
|
|
161
|
+
const id = workspaceId?.trim();
|
|
162
|
+
if (id)
|
|
163
|
+
query.set("workspaceId", id);
|
|
164
|
+
else
|
|
165
|
+
query.delete("workspaceId");
|
|
166
|
+
const qs = query.toString();
|
|
167
|
+
return `/#${path}${qs ? `?${qs}` : ""}`;
|
|
168
|
+
}
|
|
169
|
+
export function buildCanonicalHref(input) {
|
|
170
|
+
if (input.workspace === "chat")
|
|
171
|
+
return buildChatHref(input);
|
|
172
|
+
if (input.workspace === "operate") {
|
|
173
|
+
return buildOperateHref(input.operateTab ?? DEFAULT_OPERATE_TAB, input);
|
|
174
|
+
}
|
|
175
|
+
return buildInspectHref(input.inspectPath ?? "/", input.inspectQuery);
|
|
176
|
+
}
|
|
177
|
+
function finishInspect(inspectPath, query, replace) {
|
|
178
|
+
const qs = query.toString();
|
|
179
|
+
const canonicalHref = buildInspectHref(inspectPath, qs);
|
|
180
|
+
return {
|
|
181
|
+
workspace: "inspect",
|
|
182
|
+
operateTab: DEFAULT_OPERATE_TAB,
|
|
183
|
+
inspectPath: stripInspectPrefix(inspectPath),
|
|
184
|
+
canonicalHash: canonicalHref.slice(2),
|
|
185
|
+
canonicalHref,
|
|
186
|
+
replace,
|
|
187
|
+
...readWorkspaceParams(query),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function finishOperate(tab, query, replace) {
|
|
191
|
+
const canonicalHref = buildOperateHref(tab, readWorkspaceParams(query));
|
|
192
|
+
return {
|
|
193
|
+
workspace: "operate",
|
|
194
|
+
operateTab: tab,
|
|
195
|
+
inspectPath: "/",
|
|
196
|
+
canonicalHash: canonicalHref.slice(2),
|
|
197
|
+
canonicalHref,
|
|
198
|
+
replace,
|
|
199
|
+
...readWorkspaceParams(query),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function finishChat(replace, params) {
|
|
203
|
+
const effective = params ?? {};
|
|
204
|
+
const canonicalHref = buildChatHref(effective);
|
|
205
|
+
return {
|
|
206
|
+
workspace: "chat",
|
|
207
|
+
operateTab: DEFAULT_OPERATE_TAB,
|
|
208
|
+
inspectPath: "/",
|
|
209
|
+
canonicalHash: canonicalHref.slice(2),
|
|
210
|
+
canonicalHref,
|
|
211
|
+
replace,
|
|
212
|
+
...effective,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export function parseWorkspaceRoute(location, options = {}) {
|
|
216
|
+
const pathname = String(location.pathname ?? "/").replace(/\/+$/, "") || "/";
|
|
217
|
+
const lastTab = options.lastOperateTab ?? DEFAULT_OPERATE_TAB;
|
|
218
|
+
if (pathname === "/inspect") {
|
|
219
|
+
const { path, query } = splitHash(location.hash ?? "#/");
|
|
220
|
+
return finishInspect(path, query, true);
|
|
221
|
+
}
|
|
222
|
+
const { path, query } = splitHash(location.hash ?? "");
|
|
223
|
+
if (!location.hash || path === "/")
|
|
224
|
+
return null;
|
|
225
|
+
if (path === "/inspect" || path.startsWith("/inspect/")) {
|
|
226
|
+
return finishInspect(stripInspectPrefix(path), query, false);
|
|
227
|
+
}
|
|
228
|
+
if (path === "/chat")
|
|
229
|
+
return finishChat(false, readWorkspaceParams(query));
|
|
230
|
+
if (path === "/operate") {
|
|
231
|
+
return finishOperate(lastTab, query, true);
|
|
232
|
+
}
|
|
233
|
+
if (path.startsWith("/operate/")) {
|
|
234
|
+
const tab = path.slice("/operate/".length).split("/")[0] ?? "";
|
|
235
|
+
if (isOperateTab(tab))
|
|
236
|
+
return finishOperate(tab, query, false);
|
|
237
|
+
return finishOperate(lastTab, query, true);
|
|
238
|
+
}
|
|
239
|
+
if (isOperateTab(path.replace(/^\//, "").split("/")[0] ?? "")) {
|
|
240
|
+
const tab = path.replace(/^\//, "").split("/")[0];
|
|
241
|
+
return finishOperate(tab, query, true);
|
|
242
|
+
}
|
|
243
|
+
const inspectInternal = stripInspectPrefix(path).replace(/^\//, "");
|
|
244
|
+
if (INSPECT_COLLECTION_OR_OBJECT.test(inspectInternal)) {
|
|
245
|
+
return finishInspect(path, query, true);
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
export function rewriteInspectHrefToCanonical(href, base = typeof globalThis.location?.href === "string"
|
|
250
|
+
? globalThis.location.href
|
|
251
|
+
: "http://local.invalid/") {
|
|
252
|
+
const raw = String(href ?? "").trim();
|
|
253
|
+
if (!raw)
|
|
254
|
+
return DEFAULT_INSPECT_HREF;
|
|
255
|
+
if (raw.startsWith("/#/inspect"))
|
|
256
|
+
return raw;
|
|
257
|
+
if (raw.startsWith("#/inspect"))
|
|
258
|
+
return `/${raw}`;
|
|
259
|
+
if (raw.startsWith("/#/operate") || raw.startsWith("/#/chat"))
|
|
260
|
+
return raw;
|
|
261
|
+
if (raw.startsWith("#/operate") || raw.startsWith("#/chat"))
|
|
262
|
+
return `/${raw}`;
|
|
263
|
+
try {
|
|
264
|
+
const url = new URL(raw, base);
|
|
265
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
266
|
+
if (pathname === "/inspect") {
|
|
267
|
+
const { path, query } = splitHash(url.hash || "#/");
|
|
268
|
+
const merged = new URLSearchParams(url.search);
|
|
269
|
+
for (const [key, value] of query)
|
|
270
|
+
merged.set(key, value);
|
|
271
|
+
return buildInspectHref(path, merged);
|
|
272
|
+
}
|
|
273
|
+
const { path, query } = splitHash(url.hash || "");
|
|
274
|
+
if (path === "/inspect" || path.startsWith("/inspect/")) {
|
|
275
|
+
return buildInspectHref(stripInspectPrefix(path), query);
|
|
276
|
+
}
|
|
277
|
+
const internal = path.replace(/^\//, "");
|
|
278
|
+
if (INSPECT_COLLECTION_OR_OBJECT.test(internal)) {
|
|
279
|
+
return buildInspectHref(path, query);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
/* fall through */
|
|
284
|
+
}
|
|
285
|
+
if (raw.startsWith("#/") && INSPECT_COLLECTION_OR_OBJECT.test(raw.slice(2))) {
|
|
286
|
+
return buildInspectHref(raw.slice(1));
|
|
287
|
+
}
|
|
288
|
+
return DEFAULT_INSPECT_HREF;
|
|
289
|
+
}
|
|
290
|
+
export function isInspectDocumentHref(href, base = typeof globalThis.location?.href === "string"
|
|
291
|
+
? globalThis.location.href
|
|
292
|
+
: "http://local.invalid/") {
|
|
293
|
+
try {
|
|
294
|
+
const url = new URL(String(href ?? ""), base);
|
|
295
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
296
|
+
if (pathname !== "/inspect")
|
|
297
|
+
return false;
|
|
298
|
+
return !/\.[a-z0-9]+$/i.test(url.pathname);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
export function hrefToLocationHash(href) {
|
|
305
|
+
const canonical = href.startsWith("/#")
|
|
306
|
+
? href
|
|
307
|
+
: href.startsWith("#")
|
|
308
|
+
? `/${href}`
|
|
309
|
+
: rewriteInspectHrefToCanonical(href);
|
|
310
|
+
return canonical.startsWith("/#") ? canonical.slice(1) : canonical;
|
|
311
|
+
}
|
|
312
|
+
export function applyCanonicalHref(href) {
|
|
313
|
+
const hash = hrefToLocationHash(href);
|
|
314
|
+
if (typeof window === "undefined")
|
|
315
|
+
return hash;
|
|
316
|
+
const current = window.location.hash || "";
|
|
317
|
+
const normalizedCurrent = current.startsWith("#") ? current : `#${current}`;
|
|
318
|
+
const normalizedNext = hash.startsWith("#") ? hash : `#${hash}`;
|
|
319
|
+
if (normalizedCurrent !== normalizedNext) {
|
|
320
|
+
window.location.hash = normalizedNext;
|
|
321
|
+
}
|
|
322
|
+
return normalizedNext;
|
|
323
|
+
}
|
|
324
|
+
export function shouldCanonicalizeInspectDocument(location) {
|
|
325
|
+
const pathname = String(location.pathname ?? "/").replace(/\/+$/, "") || "/";
|
|
326
|
+
return pathname === "/inspect";
|
|
327
|
+
}
|
|
328
|
+
export function canonicalConsoleInspectHref(location) {
|
|
329
|
+
const { path, query } = splitHash(location.hash ?? "#/");
|
|
330
|
+
return buildInspectHref(path, query);
|
|
331
|
+
}
|
|
332
|
+
export function inspectInternalHash(hashValue) {
|
|
333
|
+
const { path, query } = splitHash(hashValue);
|
|
334
|
+
const internal = stripInspectPrefix(path);
|
|
335
|
+
const qs = query.toString();
|
|
336
|
+
return `${internal}${qs ? `?${qs}` : ""}`;
|
|
337
|
+
}
|