Merge pull request #948 from pikasTech/fix/issue932-project-refresh-path
fix: preserve workbench project refresh path
This commit is contained in:
@@ -11,7 +11,11 @@
|
|||||||
var path = window.location.pathname.replace(/\/+$/g, "") || "/";
|
var path = window.location.pathname.replace(/\/+$/g, "") || "/";
|
||||||
if (path !== "/" && path !== "/workspace") return;
|
if (path !== "/" && path !== "/workspace") return;
|
||||||
if (hash && hash !== "#/workspace" && hash !== "#/") return;
|
if (hash && hash !== "#/workspace" && hash !== "#/") return;
|
||||||
var projectId = "prj_device_pod_workbench";
|
var projectId = new URLSearchParams(window.location.search).get("projectId");
|
||||||
|
if (!projectId) {
|
||||||
|
try { projectId = window.localStorage.getItem("hwlab.workbench.projectId.v1"); } catch (_) { projectId = null; }
|
||||||
|
}
|
||||||
|
if (!/^prj_[A-Za-z0-9_.:-]{3,96}$/.test(projectId || "")) projectId = "prj_hwpod_workbench";
|
||||||
window.HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP = fetch("/auth/workspace-bootstrap?projectId=" + encodeURIComponent(projectId), {
|
window.HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP = fetch("/auth/workspace-bootstrap?projectId=" + encodeURIComponent(projectId), {
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
headers: { accept: "application/json" }
|
headers: { accept: "application/json" }
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ const css = readWeb("src/styles/workbench.css");
|
|||||||
assert.match(html, /<div id="root"><\/div>/u, "index.html must only expose React mount root");
|
assert.match(html, /<div id="root"><\/div>/u, "index.html must only expose React mount root");
|
||||||
assert.match(html, /src="\/src\/main\.tsx"/u, "index.html must load React TSX entry");
|
assert.match(html, /src="\/src\/main\.tsx"/u, "index.html must load React TSX entry");
|
||||||
assert.match(html, /HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP/u, "workspace bootstrap must start during HTML parse for the workspace first screen");
|
assert.match(html, /HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP/u, "workspace bootstrap must start during HTML parse for the workspace first screen");
|
||||||
|
assert.match(html, /new URLSearchParams\(window\.location\.search\)\.get\("projectId"\)/u, "workspace bootstrap must honor the URL projectId before the React bundle loads");
|
||||||
|
assert.doesNotMatch(html, /prj_device_pod_workbench/u, "workspace bootstrap must not hard-code the retired device-pod project id");
|
||||||
assert.doesNotMatch(html, /workbench-shell|hwpod-node-sidebar|command-input|conversation-list|session-tabs/u, "index.html must not contain business DOM shell");
|
assert.doesNotMatch(html, /workbench-shell|hwpod-node-sidebar|command-input|conversation-list|session-tabs/u, "index.html must not contain business DOM shell");
|
||||||
for (const removed of ["app.ts", "app-conversation.ts", "app-helpers.ts", "app-skills.ts", "styles.css", "web-types.d.ts"]) {
|
for (const removed of ["app.ts", "app-conversation.ts", "app-helpers.ts", "app-skills.ts", "styles.css", "web-types.d.ts"]) {
|
||||||
assert.equal(fs.existsSync(path.join(rootDir, removed)), false, `${removed} must not remain as a legacy runtime path`);
|
assert.equal(fs.existsSync(path.join(rootDir, removed)), false, `${removed} must not remain as a legacy runtime path`);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const tscArgs = ["--noEmit", "--project", path.join(rootDir, "tsconfig.json"), "
|
|||||||
const tscResult = spawnSync(tscBin, tscArgs, { cwd: rootDir, encoding: "utf8" });
|
const tscResult = spawnSync(tscBin, tscArgs, { cwd: rootDir, encoding: "utf8" });
|
||||||
if (tscResult.stdout) process.stdout.write(tscResult.stdout);
|
if (tscResult.stdout) process.stdout.write(tscResult.stdout);
|
||||||
if (tscResult.stderr) process.stderr.write(tscResult.stderr);
|
if (tscResult.stderr) process.stderr.write(tscResult.stderr);
|
||||||
|
if (tscResult.error) console.error(`hwlab-cloud-web tsc-check: failed to start ${tscBin}: ${tscResult.error.message}`);
|
||||||
if (tscResult.status !== 0) {
|
if (tscResult.status !== 0) {
|
||||||
console.error(`hwlab-cloud-web tsc-check: strict TypeScript failed with exit ${tscResult.status ?? "unknown"}`);
|
console.error(`hwlab-cloud-web tsc-check: strict TypeScript failed with exit ${tscResult.status ?? "unknown"}`);
|
||||||
process.exit(tscResult.status ?? 2);
|
process.exit(tscResult.status ?? 2);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { WorkbenchBuildSummary } from "./components/workbench/WorkbenchBuildSumm
|
|||||||
import { WorkbenchProbe } from "./components/workbench/WorkbenchProbe";
|
import { WorkbenchProbe } from "./components/workbench/WorkbenchProbe";
|
||||||
import { fetchText } from "./services/api/client";
|
import { fetchText } from "./services/api/client";
|
||||||
import { installWebPerformanceTelemetry } from "./services/performance/rum";
|
import { installWebPerformanceTelemetry } from "./services/performance/rum";
|
||||||
|
import { rememberWorkbenchProjectId, resolveInitialWorkbenchProjectId, workspaceProjectId } from "./state/constants";
|
||||||
import { firstNonEmptyString } from "./utils";
|
import { firstNonEmptyString } from "./utils";
|
||||||
|
|
||||||
const AccessView = lazy(() => import("./components/access/AccessView").then((module) => ({ default: module.AccessView })));
|
const AccessView = lazy(() => import("./components/access/AccessView").then((module) => ({ default: module.AccessView })));
|
||||||
@@ -46,13 +47,14 @@ const routeLabels: Record<RouteId, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function App(): ReactElement {
|
export function App(): ReactElement {
|
||||||
const auth = useAuth();
|
const [projectId, setProjectId] = useState(() => resolveInitialWorkbenchProjectId());
|
||||||
|
const auth = useAuth(projectId);
|
||||||
const [route, setRoute] = useState<RouteId>(() => routeFromLocation());
|
const [route, setRoute] = useState<RouteId>(() => routeFromLocation());
|
||||||
const workspaceRoute = route === "workspace";
|
const workspaceRoute = route === "workspace";
|
||||||
const authReady = auth.authState === "authenticated";
|
const authReady = auth.authState === "authenticated";
|
||||||
const authChecking = auth.authState === "checking";
|
const authChecking = auth.authState === "checking";
|
||||||
const workspaceShellPendingAuth = authChecking && workspaceRoute;
|
const workspaceShellPendingAuth = authChecking && workspaceRoute;
|
||||||
const store = useWorkbenchStore(authReady && workspaceRoute);
|
const store = useWorkbenchStore(authReady && workspaceRoute, projectId);
|
||||||
const [pickedDraft, setPickedDraft] = useState<string | null>(null);
|
const [pickedDraft, setPickedDraft] = useState<string | null>(null);
|
||||||
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
||||||
const [rightCollapsed, setRightCollapsed] = useState(false);
|
const [rightCollapsed, setRightCollapsed] = useState(false);
|
||||||
@@ -64,6 +66,13 @@ export function App(): ReactElement {
|
|||||||
installWebPerformanceTelemetry();
|
installWebPerformanceTelemetry();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const next = workspaceProjectId(store.state.workspace, projectId);
|
||||||
|
if (next === projectId) return;
|
||||||
|
rememberWorkbenchProjectId(next);
|
||||||
|
setProjectId(next);
|
||||||
|
}, [projectId, store.state.workspace]);
|
||||||
|
|
||||||
const sessionResize = useSidebarResize({
|
const sessionResize = useSidebarResize({
|
||||||
storageKey: SESSION_SIDEBAR_STORAGE_KEY,
|
storageKey: SESSION_SIDEBAR_STORAGE_KEY,
|
||||||
defaultWidth: SESSION_SIDEBAR_DEFAULT_WIDTH,
|
defaultWidth: SESSION_SIDEBAR_DEFAULT_WIDTH,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { api } from "../services/api/client";
|
import { api } from "../services/api/client";
|
||||||
import { WORKBENCH_PROJECT_ID } from "../state/constants";
|
import { DEFAULT_WORKBENCH_PROJECT_ID, normalizeWorkbenchProjectId } from "../state/constants";
|
||||||
import type { AuthSession, AuthState, AuthUser, WorkspaceRecord } from "../types/domain";
|
import type { AuthSession, AuthState, AuthUser, WorkspaceRecord } from "../types/domain";
|
||||||
import type { WorkbenchEarlyWorkspaceBootstrap } from "../types/config";
|
import type { WorkbenchEarlyWorkspaceBootstrap } from "../types/config";
|
||||||
import { asRecord, nonEmptyString } from "../utils";
|
import { asRecord, nonEmptyString } from "../utils";
|
||||||
@@ -17,7 +17,7 @@ export interface UseAuthResult {
|
|||||||
logout(): Promise<void>;
|
logout(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAuth(): UseAuthResult {
|
export function useAuth(projectId: string = DEFAULT_WORKBENCH_PROJECT_ID): UseAuthResult {
|
||||||
const [authState, setAuthState] = useState<AuthState>("checking");
|
const [authState, setAuthState] = useState<AuthState>("checking");
|
||||||
const [session, setSession] = useState<AuthSession | null>(null);
|
const [session, setSession] = useState<AuthSession | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -27,12 +27,12 @@ export function useAuth(): UseAuthResult {
|
|||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
const config = window.HWLAB_CLOUD_WEB_CONFIG?.auth;
|
const config = window.HWLAB_CLOUD_WEB_CONFIG?.auth;
|
||||||
const mode = config?.mode ?? "auto";
|
const mode = config?.mode ?? "auto";
|
||||||
const current = mode === "auto" ? await resolveWorkspaceBootstrap() : await api.session();
|
const current = mode === "auto" ? await resolveWorkspaceBootstrap(projectId) : await api.session();
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
if (current.ok && current.data?.authenticated === true) {
|
if (current.ok && current.data?.authenticated === true) {
|
||||||
const workspace = workspaceFromPayload(current.data);
|
const workspace = workspaceFromPayload(current.data);
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
window.HWLAB_CLOUD_WEB_WORKSPACE_BOOTSTRAP = { projectId: WORKBENCH_PROJECT_ID, workspace };
|
window.HWLAB_CLOUD_WEB_WORKSPACE_BOOTSTRAP = { projectId: payloadProjectId(current.data, workspace, projectId), workspace };
|
||||||
}
|
}
|
||||||
const next = sessionFromPayload(current.data);
|
const next = sessionFromPayload(current.data);
|
||||||
setSession(next);
|
setSession(next);
|
||||||
@@ -56,7 +56,7 @@ export function useAuth(): UseAuthResult {
|
|||||||
return () => {
|
return () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [projectId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.dataset.authState = authState;
|
document.body.dataset.authState = authState;
|
||||||
@@ -83,18 +83,19 @@ export function useAuth(): UseAuthResult {
|
|||||||
return useMemo(() => ({ authState, session, error, login, logout }), [authState, session, error, login, logout]);
|
return useMemo(() => ({ authState, session, error, login, logout }), [authState, session, error, login, logout]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveWorkspaceBootstrap(): ReturnType<typeof api.workspaceBootstrap> {
|
async function resolveWorkspaceBootstrap(projectId: string): ReturnType<typeof api.workspaceBootstrap> {
|
||||||
const early = await consumeEarlyWorkspaceBootstrap();
|
const normalizedProjectId = normalizeWorkbenchProjectId(projectId) ?? DEFAULT_WORKBENCH_PROJECT_ID;
|
||||||
|
const early = await consumeEarlyWorkspaceBootstrap(normalizedProjectId);
|
||||||
if (early) return early;
|
if (early) return early;
|
||||||
return api.workspaceBootstrap(WORKBENCH_PROJECT_ID);
|
return api.workspaceBootstrap(normalizedProjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function consumeEarlyWorkspaceBootstrap(): Promise<Awaited<ReturnType<typeof api.workspaceBootstrap>> | null> {
|
async function consumeEarlyWorkspaceBootstrap(projectId: string): Promise<Awaited<ReturnType<typeof api.workspaceBootstrap>> | null> {
|
||||||
const pending = window.HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP;
|
const pending = window.HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP;
|
||||||
if (!pending) return null;
|
if (!pending) return null;
|
||||||
window.HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP = undefined;
|
window.HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP = undefined;
|
||||||
const early = await pending;
|
const early = await pending;
|
||||||
if (early.projectId !== WORKBENCH_PROJECT_ID) return null;
|
if ((normalizeWorkbenchProjectId(early.projectId) ?? DEFAULT_WORKBENCH_PROJECT_ID) !== projectId) return null;
|
||||||
return normalizeEarlyWorkspaceBootstrap(early);
|
return normalizeEarlyWorkspaceBootstrap(early);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +115,15 @@ function workspaceFromPayload(payload: unknown): WorkspaceRecord | null {
|
|||||||
return typeof workspace?.workspaceId === "string" && workspace.workspaceId.trim() ? workspace as unknown as WorkspaceRecord : null;
|
return typeof workspace?.workspaceId === "string" && workspace.workspaceId.trim() ? workspace as unknown as WorkspaceRecord : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function payloadProjectId(payload: unknown, workspace: WorkspaceRecord | null, fallback: string): string {
|
||||||
|
const record = asRecord(payload);
|
||||||
|
return normalizeWorkbenchProjectId(record?.projectId)
|
||||||
|
?? normalizeWorkbenchProjectId(workspace?.projectId)
|
||||||
|
?? normalizeWorkbenchProjectId(workspace?.workspace?.projectId)
|
||||||
|
?? normalizeWorkbenchProjectId(fallback)
|
||||||
|
?? DEFAULT_WORKBENCH_PROJECT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
function sessionFromPayload(payload: { authenticated?: boolean; user?: unknown; actor?: unknown; expiresAt?: string }): AuthSession {
|
function sessionFromPayload(payload: { authenticated?: boolean; user?: unknown; actor?: unknown; expiresAt?: string }): AuthSession {
|
||||||
return {
|
return {
|
||||||
authenticated: payload.authenticated === true,
|
authenticated: payload.authenticated === true,
|
||||||
|
|||||||
@@ -1 +1,45 @@
|
|||||||
export const WORKBENCH_PROJECT_ID = "prj_hwpod_workbench";
|
export const DEFAULT_WORKBENCH_PROJECT_ID = "prj_hwpod_workbench";
|
||||||
|
export const WORKBENCH_PROJECT_ID = DEFAULT_WORKBENCH_PROJECT_ID;
|
||||||
|
export const WORKBENCH_PROJECT_ID_STORAGE_KEY = "hwlab.workbench.projectId.v1";
|
||||||
|
|
||||||
|
const WORKBENCH_PROJECT_ID_PATTERN = /^prj_[A-Za-z0-9_.:-]{3,96}$/u;
|
||||||
|
|
||||||
|
export function normalizeWorkbenchProjectId(value: unknown): string | null {
|
||||||
|
if (typeof value !== "string") return null;
|
||||||
|
const text = value.trim();
|
||||||
|
return WORKBENCH_PROJECT_ID_PATTERN.test(text) ? text : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveInitialWorkbenchProjectId(): string {
|
||||||
|
if (typeof window === "undefined") return DEFAULT_WORKBENCH_PROJECT_ID;
|
||||||
|
const fromUrl = normalizeWorkbenchProjectId(new URLSearchParams(window.location.search).get("projectId"));
|
||||||
|
const fromBootstrap = normalizeWorkbenchProjectId(window.HWLAB_CLOUD_WEB_WORKSPACE_BOOTSTRAP?.projectId);
|
||||||
|
let fromStorage: string | null = null;
|
||||||
|
try {
|
||||||
|
fromStorage = normalizeWorkbenchProjectId(window.localStorage.getItem(WORKBENCH_PROJECT_ID_STORAGE_KEY));
|
||||||
|
} catch {
|
||||||
|
fromStorage = null;
|
||||||
|
}
|
||||||
|
return fromUrl ?? fromBootstrap ?? fromStorage ?? DEFAULT_WORKBENCH_PROJECT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rememberWorkbenchProjectId(projectId: string): void {
|
||||||
|
const normalized = normalizeWorkbenchProjectId(projectId);
|
||||||
|
if (!normalized || typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(WORKBENCH_PROJECT_ID_STORAGE_KEY, normalized);
|
||||||
|
} catch {
|
||||||
|
// localStorage may be unavailable in private / embedded contexts.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function workspaceProjectId(value: unknown, fallback = DEFAULT_WORKBENCH_PROJECT_ID): string {
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
const record = value as { projectId?: unknown; workspace?: { projectId?: unknown } | null; selectedConversation?: { projectId?: unknown } | null };
|
||||||
|
return normalizeWorkbenchProjectId(record.projectId)
|
||||||
|
?? normalizeWorkbenchProjectId(record.workspace?.projectId)
|
||||||
|
?? normalizeWorkbenchProjectId(record.selectedConversation?.projectId)
|
||||||
|
?? fallback;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { api } from "../services/api/client";
|
import { api } from "../services/api/client";
|
||||||
import type { AgentChatReply, AgentChatResponse, AgentChatResultResponse, ChatMessage, ConversationRecord, SessionTab, WorkspaceRecord } from "../types/domain";
|
import type { AgentChatReply, AgentChatResponse, AgentChatResultResponse, ChatMessage, ConversationRecord, SessionTab, WorkspaceRecord } from "../types/domain";
|
||||||
import { firstNonEmptyString, nextProtocolId, nonEmptyString, relativeUpdatedLabel, shortToken } from "../utils";
|
import { firstNonEmptyString, nextProtocolId, nonEmptyString, relativeUpdatedLabel, shortToken } from "../utils";
|
||||||
import { WORKBENCH_PROJECT_ID } from "./constants";
|
import { WORKBENCH_PROJECT_ID, workspaceProjectId } from "./constants";
|
||||||
|
|
||||||
export function messagesFromWorkspace(workspace: WorkspaceRecord | null): ChatMessage[] {
|
export function messagesFromWorkspace(workspace: WorkspaceRecord | null): ChatMessage[] {
|
||||||
const selected = workspace?.selectedConversation?.messages;
|
const selected = workspace?.selectedConversation?.messages;
|
||||||
@@ -119,17 +119,18 @@ function isTerminalAssistantEvent(event: Record<string, unknown>): boolean {
|
|||||||
return type === "assistant_message" && (event.status === "completed" || event.final === true || event.terminal === true);
|
return type === "assistant_message" && (event.status === "completed" || event.final === true || event.terminal === true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureWorkspace(current: WorkspaceRecord | null): Promise<WorkspaceRecord | null> {
|
export async function ensureWorkspace(current: WorkspaceRecord | null, projectId = WORKBENCH_PROJECT_ID): Promise<WorkspaceRecord | null> {
|
||||||
if (current?.workspaceId) return current;
|
if (current?.workspaceId && workspaceProjectId(current, projectId) === projectId) return current;
|
||||||
const response = await api.workspace(WORKBENCH_PROJECT_ID);
|
const response = await api.workspace(projectId);
|
||||||
return response.ok ? response.data?.workspace ?? null : null;
|
return response.ok ? response.data?.workspace ?? null : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function persistConversation(input: { workspace: WorkspaceRecord | null; conversationId: string; sessionId: string | null; threadId: string | null; messages: ChatMessage[] }): Promise<WorkspaceRecord | null> {
|
export async function persistConversation(input: { workspace: WorkspaceRecord | null; projectId?: string; conversationId: string; sessionId: string | null; threadId: string | null; messages: ChatMessage[] }): Promise<WorkspaceRecord | null> {
|
||||||
|
const projectId = workspaceProjectId(input.workspace, input.projectId ?? WORKBENCH_PROJECT_ID);
|
||||||
const messages = mergeConversationMessages(input.messages);
|
const messages = mergeConversationMessages(input.messages);
|
||||||
const latest = messages.at(-1);
|
const latest = messages.at(-1);
|
||||||
await api.saveConversation(input.conversationId, {
|
await api.saveConversation(input.conversationId, {
|
||||||
projectId: WORKBENCH_PROJECT_ID,
|
projectId,
|
||||||
conversationId: input.conversationId,
|
conversationId: input.conversationId,
|
||||||
sessionId: input.sessionId,
|
sessionId: input.sessionId,
|
||||||
threadId: input.threadId,
|
threadId: input.threadId,
|
||||||
@@ -140,7 +141,7 @@ export async function persistConversation(input: { workspace: WorkspaceRecord |
|
|||||||
});
|
});
|
||||||
if (input.workspace?.workspaceId) {
|
if (input.workspace?.workspaceId) {
|
||||||
const response = await api.updateWorkspace(input.workspace.workspaceId, {
|
const response = await api.updateWorkspace(input.workspace.workspaceId, {
|
||||||
projectId: WORKBENCH_PROJECT_ID,
|
projectId,
|
||||||
expectedRevision: input.workspace.revision,
|
expectedRevision: input.workspace.revision,
|
||||||
selectedConversationId: input.conversationId,
|
selectedConversationId: input.conversationId,
|
||||||
selectedAgentSessionId: input.sessionId,
|
selectedAgentSessionId: input.sessionId,
|
||||||
@@ -166,7 +167,7 @@ function conversationToTab(conversation: ConversationRecord, activeConversationI
|
|||||||
const status = firstNonEmptyString(conversation.snapshot?.sessionStatus, conversation.status, conversation.messages?.at(-1)?.status) ?? "source";
|
const status = firstNonEmptyString(conversation.snapshot?.sessionStatus, conversation.status, conversation.messages?.at(-1)?.status) ?? "source";
|
||||||
const trace = firstNonEmptyString(conversation.lastTraceId, conversation.messages?.at(-1)?.traceId);
|
const trace = firstNonEmptyString(conversation.lastTraceId, conversation.messages?.at(-1)?.traceId);
|
||||||
const preview = firstNonEmptyString(conversation.firstUserMessagePreview, conversation.snapshot?.firstUserMessagePreview, conversation.messages?.find((message) => message.role === "user")?.text);
|
const preview = firstNonEmptyString(conversation.firstUserMessagePreview, conversation.snapshot?.firstUserMessagePreview, conversation.messages?.find((message) => message.role === "user")?.text);
|
||||||
return { key: sessionId ?? conversationId, label: truncateLabel(preview) ?? `Session ${shortToken(sessionId ?? conversationId, 8)}`, subtitle: `${relativeUpdatedLabel(timestampMs(updatedAt))} · ${status === "running" ? "处理中" : trace ? `trace ${shortToken(trace, 8)}` : "未开始"}`, conversationId, sessionId, threadId: firstNonEmptyString(conversation.threadId, conversation.session?.threadId), lastTraceId: trace, status, messageCount: Number.isFinite(Number(conversation.messageCount)) ? Number(conversation.messageCount) : conversation.messages?.length ?? 0, active: conversationId === activeConversationId, updatedAt, userPreview: preview };
|
return { key: sessionId ?? conversationId, label: truncateLabel(preview) ?? `Session ${shortToken(sessionId ?? conversationId, 8)}`, subtitle: `${relativeUpdatedLabel(timestampMs(updatedAt))} · ${status === "running" ? "处理中" : trace ? `trace ${shortToken(trace, 8)}` : "未开始"}`, projectId: conversation.projectId, conversationId, sessionId, threadId: firstNonEmptyString(conversation.threadId, conversation.session?.threadId), lastTraceId: trace, status, messageCount: Number.isFinite(Number(conversation.messageCount)) ? Number(conversation.messageCount) : conversation.messages?.length ?? 0, active: conversationId === activeConversationId, updatedAt, userPreview: preview };
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncateLabel(value: string | null): string | null {
|
function truncateLabel(value: string | null): string | null {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ test("issue 853 persists submitted user and running agent messages before long C
|
|||||||
|
|
||||||
test("issue 853 submit failure persists the same trace before refresh", () => {
|
test("issue 853 submit failure persists the same trace before refresh", () => {
|
||||||
const source = fs.readFileSync(path.join(srcRoot, "state/workbench.ts"), "utf8");
|
const source = fs.readFileSync(path.join(srcRoot, "state/workbench.ts"), "utf8");
|
||||||
const failureIndex = source.indexOf('const traceSnapshot = await fetchTraceSnapshot(traceId, traceId, Math.max(8000, state.codeAgentTimeoutMs));');
|
const failureIndex = source.indexOf('const traceSnapshot = await fetchTraceSnapshot(traceId, traceId, Math.max(8000, state.codeAgentTimeoutMs), undefined, activeProjectId);');
|
||||||
const failedWithTraceIndex = source.indexOf('const failedWithTrace = runnerTrace ? { ...failed, runnerTrace } : failed;', failureIndex);
|
const failedWithTraceIndex = source.indexOf('const failedWithTrace = runnerTrace ? { ...failed, runnerTrace } : failed;', failureIndex);
|
||||||
const persistIndex = source.indexOf('messages: [...state.messages, user, failedWithTrace]', failedWithTraceIndex);
|
const persistIndex = source.indexOf('messages: [...state.messages, user, failedWithTrace]', failedWithTraceIndex);
|
||||||
assert.ok(failureIndex > 0);
|
assert.ok(failureIndex > 0);
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { api } from "../services/api/client";
|
|||||||
import type { ChatMessage, ConversationRecord, WorkspaceRecord } from "../types/domain";
|
import type { ChatMessage, ConversationRecord, WorkspaceRecord } from "../types/domain";
|
||||||
import { firstNonEmptyString } from "../utils";
|
import { firstNonEmptyString } from "../utils";
|
||||||
import { replayFullTrace, snapshotToRunnerTrace } from "./runner-trace";
|
import { replayFullTrace, snapshotToRunnerTrace } from "./runner-trace";
|
||||||
|
import { WORKBENCH_PROJECT_ID, workspaceProjectId } from "./constants";
|
||||||
|
|
||||||
export interface RunnerActionsOptions {
|
export interface RunnerActionsOptions {
|
||||||
activeConversationId: string | null;
|
activeConversationId: string | null;
|
||||||
|
projectId?: string;
|
||||||
workspace: WorkspaceRecord | null;
|
workspace: WorkspaceRecord | null;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
codeAgentTimeoutMs: number;
|
codeAgentTimeoutMs: number;
|
||||||
@@ -21,6 +23,7 @@ export async function cancelAgentMessageAction(
|
|||||||
const message = options.messages.find((item) => item.id === messageId);
|
const message = options.messages.find((item) => item.id === messageId);
|
||||||
if (!message?.traceId) return;
|
if (!message?.traceId) return;
|
||||||
const response = await api.cancelAgentMessage({
|
const response = await api.cancelAgentMessage({
|
||||||
|
projectId: workspaceProjectId(options.workspace, options.projectId ?? WORKBENCH_PROJECT_ID),
|
||||||
traceId: message.traceId,
|
traceId: message.traceId,
|
||||||
conversationId: message.conversationId ?? options.activeConversationId ?? undefined,
|
conversationId: message.conversationId ?? options.activeConversationId ?? undefined,
|
||||||
sessionId: message.sessionId
|
sessionId: message.sessionId
|
||||||
@@ -74,7 +77,7 @@ export async function replayAgentTraceAction(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const message = options.messages.find((item) => item.id === messageId);
|
const message = options.messages.find((item) => item.id === messageId);
|
||||||
if (!message?.traceId) return;
|
if (!message?.traceId) return;
|
||||||
const snapshot = await replayFullTrace(message.traceId, Math.max(8000, options.codeAgentTimeoutMs));
|
const snapshot = await replayFullTrace(message.traceId, Math.max(8000, options.codeAgentTimeoutMs), workspaceProjectId(options.workspace, options.projectId ?? WORKBENCH_PROJECT_ID));
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
options.onTraceStatus(messageId, "trace 回放失败");
|
options.onTraceStatus(messageId, "trace 回放失败");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -125,9 +125,9 @@ test("subscribeToTrace keeps polling result when trace terminal lacks final resp
|
|||||||
assert.equal(resultPolls, 2);
|
assert.equal(resultPolls, 2);
|
||||||
assert.deepEqual(fetches, [
|
assert.deepEqual(fetches, [
|
||||||
"/v1/agent/chat/result/trc_terminal_first",
|
"/v1/agent/chat/result/trc_terminal_first",
|
||||||
"/v1/agent/chat/trace/trc_terminal_first",
|
"/v1/agent/chat/trace/trc_terminal_first?projectId=prj_hwpod_workbench",
|
||||||
"/v1/agent/chat/result/trc_terminal_first",
|
"/v1/agent/chat/result/trc_terminal_first",
|
||||||
"/v1/agent/chat/trace/trc_terminal_first"
|
"/v1/agent/chat/trace/trc_terminal_first?projectId=prj_hwpod_workbench"
|
||||||
]);
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
@@ -184,7 +184,7 @@ test("subscribeToTrace preserves trace when terminal result with text arrives be
|
|||||||
assert.equal(completed.traceEvents?.length, 3);
|
assert.equal(completed.traceEvents?.length, 3);
|
||||||
assert.deepEqual(fetches, [
|
assert.deepEqual(fetches, [
|
||||||
"/v1/agent/chat/result/trc_failed_fast",
|
"/v1/agent/chat/result/trc_failed_fast",
|
||||||
"/v1/agent/chat/trace/trc_failed_fast"
|
"/v1/agent/chat/trace/trc_failed_fast?projectId=prj_hwpod_workbench"
|
||||||
]);
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
@@ -280,7 +280,7 @@ test("subscribeToTrace merges terminal result without text with trace assistant
|
|||||||
assert.equal(completed.runnerTrace?.events?.length, 3);
|
assert.equal(completed.runnerTrace?.events?.length, 3);
|
||||||
assert.deepEqual(fetches, [
|
assert.deepEqual(fetches, [
|
||||||
"/v1/agent/chat/result/trc_result_first",
|
"/v1/agent/chat/result/trc_result_first",
|
||||||
"/v1/agent/chat/trace/trc_result_first"
|
"/v1/agent/chat/trace/trc_result_first?projectId=prj_hwpod_workbench"
|
||||||
]);
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
@@ -379,6 +379,43 @@ test("fetchTraceSnapshot retries until fast-fail trace events are readable", asy
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("fetchTraceSnapshot and replayFullTrace use the active workbench project id", async () => {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
const originalSetTimeout = globalThis.window?.setTimeout;
|
||||||
|
const originalClearTimeout = globalThis.window?.clearTimeout;
|
||||||
|
const fetches: string[] = [];
|
||||||
|
globalThis.window = {
|
||||||
|
...(globalThis.window ?? {}),
|
||||||
|
setTimeout: ((callback: () => void) => { callback(); return 1; }) as typeof window.setTimeout,
|
||||||
|
clearTimeout: (() => undefined) as typeof window.clearTimeout
|
||||||
|
} as typeof window;
|
||||||
|
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||||
|
const url = String(input);
|
||||||
|
fetches.push(url);
|
||||||
|
return jsonResponse({
|
||||||
|
status: "completed",
|
||||||
|
traceId: url.includes("custom_replay") ? "trc_custom_replay" : "trc_custom_snapshot",
|
||||||
|
events: [{ seq: 1, label: "agentrun:result:completed", status: "completed" }],
|
||||||
|
eventCount: 1
|
||||||
|
});
|
||||||
|
}) as typeof fetch;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchTraceSnapshot("trc_custom_snapshot", "trc_custom_snapshot", 1000, undefined, "prj_v02_code_agent");
|
||||||
|
await replayFullTrace("trc_custom_replay", 1000, "prj_v02_code_agent");
|
||||||
|
assert.deepEqual(fetches, [
|
||||||
|
"/v1/agent/chat/trace/trc_custom_snapshot?projectId=prj_v02_code_agent",
|
||||||
|
"/v1/agent/chat/trace/trc_custom_replay?projectId=prj_v02_code_agent"
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
if (globalThis.window) {
|
||||||
|
globalThis.window.setTimeout = originalSetTimeout ?? globalThis.window.setTimeout;
|
||||||
|
globalThis.window.clearTimeout = originalClearTimeout ?? globalThis.window.clearTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("fetchTraceSnapshot accepts expired trace fallback without waiting for raw events", async () => {
|
test("fetchTraceSnapshot accepts expired trace fallback without waiting for raw events", async () => {
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
const fetches: string[] = [];
|
const fetches: string[] = [];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
} from "../types/domain";
|
} from "../types/domain";
|
||||||
import { api } from "../services/api/client";
|
import { api } from "../services/api/client";
|
||||||
import type { ActivityRef, ActivityRefSource } from "../services/api/client";
|
import type { ActivityRef, ActivityRefSource } from "../services/api/client";
|
||||||
|
import { WORKBENCH_PROJECT_ID, normalizeWorkbenchProjectId } from "./constants";
|
||||||
|
|
||||||
export interface TraceSnapshot {
|
export interface TraceSnapshot {
|
||||||
traceId?: string;
|
traceId?: string;
|
||||||
@@ -41,8 +42,10 @@ export function isResultUrlStatus(response: AgentChatResponse): boolean {
|
|||||||
return Boolean(response.resultUrl) && (response.status === "running" || response.status === "accepted" || isTerminalStatus(response.status));
|
return Boolean(response.resultUrl) && (response.status === "running" || response.status === "accepted" || isTerminalStatus(response.status));
|
||||||
}
|
}
|
||||||
|
|
||||||
function traceUrlFromResultUrl(resultUrl: string, traceId: string): string {
|
function traceUrlFromResultUrl(resultUrl: string, traceId: string, projectId = WORKBENCH_PROJECT_ID): string {
|
||||||
return resultUrl.replace(/\/v1\/agent\/chat\/result\/[^/?#]+/u, `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`);
|
const url = resultUrl.replace(/\/v1\/agent\/chat\/result\/[^/?#]+/u, `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`);
|
||||||
|
if (/[?&]projectId=/u.test(url)) return url;
|
||||||
|
return `${url}${url.includes("?") ? "&" : "?"}projectId=${encodeURIComponent(normalizeWorkbenchProjectId(projectId) ?? WORKBENCH_PROJECT_ID)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeRunnerTrace(previous: ChatMessage["runnerTrace"], next: NonNullable<ChatMessage["runnerTrace"]>): NonNullable<ChatMessage["runnerTrace"]> {
|
export function mergeRunnerTrace(previous: ChatMessage["runnerTrace"], next: NonNullable<ChatMessage["runnerTrace"]>): NonNullable<ChatMessage["runnerTrace"]> {
|
||||||
@@ -190,6 +193,7 @@ function firstNonEmptyString(...values: unknown[]): string | null {
|
|||||||
*/
|
*/
|
||||||
export interface TraceSubscriptionConfig {
|
export interface TraceSubscriptionConfig {
|
||||||
traceId: string;
|
traceId: string;
|
||||||
|
projectId?: string;
|
||||||
initial: AgentChatResponse;
|
initial: AgentChatResponse;
|
||||||
onActivity: () => void;
|
onActivity: () => void;
|
||||||
onSnapshot: (snapshot: TraceSnapshot) => void;
|
onSnapshot: (snapshot: TraceSnapshot) => void;
|
||||||
@@ -201,12 +205,13 @@ export interface TraceSubscriptionConfig {
|
|||||||
|
|
||||||
export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise<void> {
|
export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise<void> {
|
||||||
const { traceId, initial, onActivity, onSnapshot, onComplete, onInfrastructureError, signal, inactivityTimeoutMs } = config;
|
const { traceId, initial, onActivity, onSnapshot, onComplete, onInfrastructureError, signal, inactivityTimeoutMs } = config;
|
||||||
|
const projectId = normalizeWorkbenchProjectId(config.projectId) ?? WORKBENCH_PROJECT_ID;
|
||||||
|
|
||||||
// If the initial response is already terminal, still fetch the trace once.
|
// If the initial response is already terminal, still fetch the trace once.
|
||||||
// Provider fast-fail responses can include final text immediately while the
|
// Provider fast-fail responses can include final text immediately while the
|
||||||
// trace endpoint already has the event list needed for refresh persistence.
|
// trace endpoint already has the event list needed for refresh persistence.
|
||||||
if (isTerminalStatus(initial.status)) {
|
if (isTerminalStatus(initial.status)) {
|
||||||
const traceSnapshot = await fetchTraceSnapshot(traceId, traceId, inactivityTimeoutMs, signal);
|
const traceSnapshot = await fetchTraceSnapshot(traceId, traceId, inactivityTimeoutMs, signal, projectId);
|
||||||
if (signal.aborted) return;
|
if (signal.aborted) return;
|
||||||
if (traceSnapshot) onSnapshot(traceSnapshot);
|
if (traceSnapshot) onSnapshot(traceSnapshot);
|
||||||
onComplete(mergeTraceResults(initial as AgentChatResultResponse, traceSnapshot));
|
onComplete(mergeTraceResults(initial as AgentChatResultResponse, traceSnapshot));
|
||||||
@@ -218,7 +223,7 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resultUrl = initial.resultUrl;
|
const resultUrl = initial.resultUrl;
|
||||||
const traceUrl = traceUrlFromResultUrl(resultUrl, traceId);
|
const traceUrl = traceUrlFromResultUrl(resultUrl, traceId, projectId);
|
||||||
let lastEventCount = -1;
|
let lastEventCount = -1;
|
||||||
let lastStatus: string | null = null;
|
let lastStatus: string | null = null;
|
||||||
let lastUpdatedAt: string | null = null;
|
let lastUpdatedAt: string | null = null;
|
||||||
@@ -336,9 +341,9 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTraceSnapshot(traceId: string, traceUrlOrId: string, inactivityTimeoutMs: number, signal?: AbortSignal): Promise<TraceSnapshot | null> {
|
export async function fetchTraceSnapshot(traceId: string, traceUrlOrId: string, inactivityTimeoutMs: number, signal?: AbortSignal, projectId = WORKBENCH_PROJECT_ID): Promise<TraceSnapshot | null> {
|
||||||
if (signal?.aborted) return null;
|
if (signal?.aborted) return null;
|
||||||
const url = traceUrlOrId.startsWith("/") ? traceUrlOrId : `/v1/agent/chat/trace/${encodeURIComponent(traceUrlOrId)}?projectId=prj_hwpod_workbench`;
|
const url = traceUrlOrId.startsWith("/") ? traceUrlOrId : traceUrl(traceUrlOrId, projectId);
|
||||||
const totalTimeoutMs = Math.max(1000, Math.min(inactivityTimeoutMs, TRACE_SNAPSHOT_MAX_TIMEOUT_MS));
|
const totalTimeoutMs = Math.max(1000, Math.min(inactivityTimeoutMs, TRACE_SNAPSHOT_MAX_TIMEOUT_MS));
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const maxAttempts = Math.max(1, Math.ceil(totalTimeoutMs / TRACE_SNAPSHOT_RETRY_INTERVAL_MS));
|
const maxAttempts = Math.max(1, Math.ceil(totalTimeoutMs / TRACE_SNAPSHOT_RETRY_INTERVAL_MS));
|
||||||
@@ -406,8 +411,8 @@ function snapshotToTraceSnapshot(result: AgentChatResultResponse): TraceSnapshot
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function replayFullTrace(traceId: string, totalTimeoutMs = 15000): Promise<TraceSnapshot | null> {
|
export async function replayFullTrace(traceId: string, totalTimeoutMs = 15000, projectId = WORKBENCH_PROJECT_ID): Promise<TraceSnapshot | null> {
|
||||||
const url = `/v1/agent/chat/trace/${encodeURIComponent(traceId)}?projectId=prj_hwpod_workbench`;
|
const url = traceUrl(traceId, projectId);
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
let polled = await api.getAgentChatResult(url, totalTimeoutMs);
|
let polled = await api.getAgentChatResult(url, totalTimeoutMs);
|
||||||
while ((!polled.ok || !polled.data) && Date.now() - startedAt < totalTimeoutMs) {
|
while ((!polled.ok || !polled.data) && Date.now() - startedAt < totalTimeoutMs) {
|
||||||
@@ -418,6 +423,10 @@ export async function replayFullTrace(traceId: string, totalTimeoutMs = 15000):
|
|||||||
return { ...snapshotFromTracePoll(traceId, polled.data), eventsCompacted: true };
|
return { ...snapshotFromTracePoll(traceId, polled.data), eventsCompacted: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function traceUrl(traceId: string, projectId: string): string {
|
||||||
|
return `/v1/agent/chat/trace/${encodeURIComponent(traceId)}?projectId=${encodeURIComponent(normalizeWorkbenchProjectId(projectId) ?? WORKBENCH_PROJECT_ID)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function traceSnapshotEventCount(result: AgentChatResultResponse, events: TraceEvent[]): number {
|
function traceSnapshotEventCount(result: AgentChatResultResponse, events: TraceEvent[]): number {
|
||||||
const explicit = positiveNumber(result.eventCount);
|
const explicit = positiveNumber(result.eventCount);
|
||||||
if (explicit > 0 || events.length > 0) return explicit || events.length;
|
if (explicit > 0 || events.length > 0) return explicit || events.length;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { AgentChatResultResponse, ChatMessage, WorkspaceRecord } from "../t
|
|||||||
|
|
||||||
export interface UseTraceReattachConfig {
|
export interface UseTraceReattachConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
projectId: string;
|
||||||
activeTraceId: string | null;
|
activeTraceId: string | null;
|
||||||
codeAgentTimeoutMs: number;
|
codeAgentTimeoutMs: number;
|
||||||
chatPending: boolean;
|
chatPending: boolean;
|
||||||
@@ -19,7 +20,7 @@ export interface UseTraceReattachConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useTraceReattach(config: UseTraceReattachConfig): void {
|
export function useTraceReattach(config: UseTraceReattachConfig): void {
|
||||||
const { enabled, activeTraceId, codeAgentTimeoutMs, chatPending, workspace, messages, onActivity, onDispatch, onPersist } = config;
|
const { enabled, projectId, activeTraceId, codeAgentTimeoutMs, chatPending, workspace, messages, onActivity, onDispatch, onPersist } = config;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled || !activeTraceId || chatPending) return;
|
if (!enabled || !activeTraceId || chatPending) return;
|
||||||
const resultUrl = `/v1/agent/chat/result/${encodeURIComponent(activeTraceId)}`;
|
const resultUrl = `/v1/agent/chat/result/${encodeURIComponent(activeTraceId)}`;
|
||||||
@@ -38,6 +39,7 @@ export function useTraceReattach(config: UseTraceReattachConfig): void {
|
|||||||
const ws: WorkspaceRecord | null = workspace;
|
const ws: WorkspaceRecord | null = workspace;
|
||||||
void subscribeToTrace({
|
void subscribeToTrace({
|
||||||
traceId: activeTraceId,
|
traceId: activeTraceId,
|
||||||
|
projectId,
|
||||||
initial: { status: "running", resultUrl, traceId: activeTraceId } as Parameters<typeof subscribeToTrace>[0]["initial"],
|
initial: { status: "running", resultUrl, traceId: activeTraceId } as Parameters<typeof subscribeToTrace>[0]["initial"],
|
||||||
inactivityTimeoutMs: codeAgentTimeoutMs,
|
inactivityTimeoutMs: codeAgentTimeoutMs,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
@@ -50,16 +52,16 @@ export function useTraceReattach(config: UseTraceReattachConfig): void {
|
|||||||
const stub = messages.find((m) => m.id === placeholderId) ?? ({ id: placeholderId, role: "agent" as const, text: "", status: "running" as const, traceId: activeTraceId, conversationId, sessionId, threadId, createdAt: new Date().toISOString() } as ChatMessage);
|
const stub = messages.find((m) => m.id === placeholderId) ?? ({ id: placeholderId, role: "agent" as const, text: "", status: "running" as const, traceId: activeTraceId, conversationId, sessionId, threadId, createdAt: new Date().toISOString() } as ChatMessage);
|
||||||
const completed = messageFromAgentResponse(placeholderId, stub, mergeTraceResults(result, lastSnapshot));
|
const completed = messageFromAgentResponse(placeholderId, stub, mergeTraceResults(result, lastSnapshot));
|
||||||
onDispatch({ type: "message:complete", messageId: placeholderId, message: completed, availability: result.availability ?? null, workspace: ws ?? undefined });
|
onDispatch({ type: "message:complete", messageId: placeholderId, message: completed, availability: result.availability ?? null, workspace: ws ?? undefined });
|
||||||
void onPersist({ workspace: ws, conversationId, sessionId: result.sessionId ?? sessionId, threadId: result.threadId ?? threadId, messages: mergeConversationMessages(messages, [completed]) });
|
void onPersist({ workspace: ws, projectId, conversationId, sessionId: result.sessionId ?? sessionId, threadId: result.threadId ?? threadId, messages: mergeConversationMessages(messages, [completed]) });
|
||||||
controller.abort();
|
controller.abort();
|
||||||
},
|
},
|
||||||
onInfrastructureError: (error) => {
|
onInfrastructureError: (error) => {
|
||||||
const failed = makeMessage("agent", `Code Agent re-attach ${codeAgentTimeoutMs}ms 无活动(${error})`, "failed", { traceId: activeTraceId, conversationId, sessionId, threadId, title: "Code Agent 超时" });
|
const failed = makeMessage("agent", `Code Agent re-attach ${codeAgentTimeoutMs}ms 无活动(${error})`, "failed", { traceId: activeTraceId, conversationId, sessionId, threadId, title: "Code Agent 超时" });
|
||||||
onDispatch({ type: "message:fail", messageId: placeholderId, message: failed });
|
onDispatch({ type: "message:fail", messageId: placeholderId, message: failed });
|
||||||
void onPersist({ workspace: ws, conversationId, sessionId, threadId, messages: mergeConversationMessages(messages, [failed]) });
|
void onPersist({ workspace: ws, projectId, conversationId, sessionId, threadId, messages: mergeConversationMessages(messages, [failed]) });
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}
|
}
|
||||||
}).finally(() => controller.abort());
|
}).finally(() => controller.abort());
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [enabled, activeTraceId, chatPending, codeAgentTimeoutMs, workspace, messages, onActivity, onDispatch, onPersist]);
|
}, [enabled, projectId, activeTraceId, chatPending, codeAgentTimeoutMs, workspace, messages, onActivity, onDispatch, onPersist]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
WorkspaceRecord
|
WorkspaceRecord
|
||||||
} from "../types/domain";
|
} from "../types/domain";
|
||||||
import { firstNonEmptyString, nextProtocolId } from "../utils";
|
import { firstNonEmptyString, nextProtocolId } from "../utils";
|
||||||
import { WORKBENCH_PROJECT_ID } from "./constants";
|
import { WORKBENCH_PROJECT_ID, rememberWorkbenchProjectId, workspaceProjectId } from "./constants";
|
||||||
import {
|
import {
|
||||||
availabilityFromLive,
|
availabilityFromLive,
|
||||||
composerFromState,
|
composerFromState,
|
||||||
@@ -91,8 +91,9 @@ export interface WorkbenchStore {
|
|||||||
setGatewayShellTimeoutMs(value: number): void;
|
setGatewayShellTimeoutMs(value: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
export function useWorkbenchStore(enabled: boolean, projectId = WORKBENCH_PROJECT_ID): WorkbenchStore {
|
||||||
const [state, dispatch] = useReducer(workbenchReducer, initialState);
|
const [state, dispatch] = useReducer(workbenchReducer, initialState);
|
||||||
|
const activeProjectId = workspaceProjectId(state.workspace, projectId);
|
||||||
// Single shared activity clock so submit, trace polling, and user-typing
|
// Single shared activity clock so submit, trace polling, and user-typing
|
||||||
// all reset the same inactivity window. Kept in a ref (not state) so
|
// all reset the same inactivity window. Kept in a ref (not state) so
|
||||||
// updates never trigger re-renders.
|
// updates never trigger re-renders.
|
||||||
@@ -129,14 +130,15 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
const hydrate = useCallback(async () => {
|
const hydrate = useCallback(async () => {
|
||||||
dispatch({ type: "hydrate:start" });
|
dispatch({ type: "hydrate:start" });
|
||||||
const [workspaceResult, conversationsResult] = await Promise.all([
|
const [workspaceResult, conversationsResult] = await Promise.all([
|
||||||
api.workspace(WORKBENCH_PROJECT_ID),
|
api.workspace(projectId),
|
||||||
api.conversations(WORKBENCH_PROJECT_ID)
|
api.conversations(projectId)
|
||||||
]);
|
]);
|
||||||
if (!workspaceResult.ok) {
|
if (!workspaceResult.ok) {
|
||||||
dispatch({ type: "hydrate:error", error: workspaceResult.error ?? "workspace unavailable" });
|
dispatch({ type: "hydrate:error", error: workspaceResult.error ?? "workspace unavailable" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspace = workspaceResult.data?.workspace ?? null;
|
const workspace = workspaceResult.data?.workspace ?? null;
|
||||||
|
rememberWorkbenchProjectId(workspaceProjectId(workspace, projectId));
|
||||||
const conversations = conversationsResult.ok ? conversationsResult.data?.conversations ?? [] : [];
|
const conversations = conversationsResult.ok ? conversationsResult.data?.conversations ?? [] : [];
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "hydrate:done",
|
type: "hydrate:done",
|
||||||
@@ -144,7 +146,7 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
conversations,
|
conversations,
|
||||||
messages: messagesFromWorkspace(workspace)
|
messages: messagesFromWorkspace(workspace)
|
||||||
});
|
});
|
||||||
}, []);
|
}, [projectId]);
|
||||||
|
|
||||||
const refreshLive = useCallback(async () => {
|
const refreshLive = useCallback(async () => {
|
||||||
const [healthLive, health, restIndex, adapter, hwpodNodeOps, liveBuilds] = await Promise.all([
|
const [healthLive, health, restIndex, adapter, hwpodNodeOps, liveBuilds] = await Promise.all([
|
||||||
@@ -172,25 +174,26 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
}, [enabled, refreshLive]);
|
}, [enabled, refreshLive]);
|
||||||
|
|
||||||
// HWLAB #802: re-attach to a running trace on hydrate.
|
// HWLAB #802: re-attach to a running trace on hydrate.
|
||||||
useTraceReattach({ enabled, activeTraceId: firstNonEmptyString(state.workspace?.activeTraceId, state.workspace?.workspace?.activeTraceId), codeAgentTimeoutMs: state.codeAgentTimeoutMs, chatPending: state.chatPending, workspace: state.workspace, messages: state.messages, onActivity: updateActivity, onDispatch: dispatch, onPersist: persistConversation });
|
useTraceReattach({ enabled, projectId: activeProjectId, activeTraceId: firstNonEmptyString(state.workspace?.activeTraceId, state.workspace?.workspace?.activeTraceId), codeAgentTimeoutMs: state.codeAgentTimeoutMs, chatPending: state.chatPending, workspace: state.workspace, messages: state.messages, onActivity: updateActivity, onDispatch: dispatch, onPersist: persistConversation });
|
||||||
|
|
||||||
const createSession = useCallback(async () => {
|
const createSession = useCallback(async () => {
|
||||||
const workspace = await ensureWorkspace(state.workspace);
|
const workspace = await ensureWorkspace(state.workspace, activeProjectId);
|
||||||
if (!workspace) return;
|
if (!workspace) return;
|
||||||
const response = await api.selectConversation(workspace.workspaceId, { projectId: WORKBENCH_PROJECT_ID, create: true, updatedByClient: "cloud-web-react" });
|
const response = await api.selectConversation(workspace.workspaceId, { projectId: activeProjectId, create: true, updatedByClient: "cloud-web-react" });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const nextWorkspace = response.data?.workspace ?? workspace;
|
const nextWorkspace = response.data?.workspace ?? workspace;
|
||||||
dispatch({ type: "conversation:select", conversation: nextWorkspace.selectedConversation ?? null, workspace: nextWorkspace });
|
dispatch({ type: "conversation:select", conversation: nextWorkspace.selectedConversation ?? null, workspace: nextWorkspace });
|
||||||
const conversations = await api.conversations(WORKBENCH_PROJECT_ID);
|
const conversations = await api.conversations(workspaceProjectId(nextWorkspace, activeProjectId));
|
||||||
if (conversations.ok) dispatch({ type: "conversation:list", conversations: conversations.data?.conversations ?? [] });
|
if (conversations.ok) dispatch({ type: "conversation:list", conversations: conversations.data?.conversations ?? [] });
|
||||||
}
|
}
|
||||||
}, [state.workspace]);
|
}, [activeProjectId, state.workspace]);
|
||||||
|
|
||||||
const selectConversation = useCallback(async (tab: SessionTab) => {
|
const selectConversation = useCallback(async (tab: SessionTab) => {
|
||||||
const workspace = await ensureWorkspace(state.workspace);
|
const tabProjectId = tab.projectId ?? activeProjectId;
|
||||||
|
const workspace = await ensureWorkspace(state.workspace, tabProjectId);
|
||||||
if (!workspace || !tab.conversationId) return;
|
if (!workspace || !tab.conversationId) return;
|
||||||
const response = await api.selectConversation(workspace.workspaceId, {
|
const response = await api.selectConversation(workspace.workspaceId, {
|
||||||
projectId: WORKBENCH_PROJECT_ID,
|
projectId: tabProjectId,
|
||||||
conversationId: tab.conversationId,
|
conversationId: tab.conversationId,
|
||||||
sessionId: tab.sessionId,
|
sessionId: tab.sessionId,
|
||||||
threadId: tab.threadId,
|
threadId: tab.threadId,
|
||||||
@@ -198,21 +201,22 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const nextWorkspace = response.data?.workspace ?? workspace;
|
const nextWorkspace = response.data?.workspace ?? workspace;
|
||||||
|
rememberWorkbenchProjectId(workspaceProjectId(nextWorkspace, tabProjectId));
|
||||||
dispatch({ type: "conversation:select", conversation: nextWorkspace.selectedConversation ?? conversationFromTab(tab), workspace: nextWorkspace });
|
dispatch({ type: "conversation:select", conversation: nextWorkspace.selectedConversation ?? conversationFromTab(tab), workspace: nextWorkspace });
|
||||||
}
|
}
|
||||||
}, [state.workspace]);
|
}, [activeProjectId, state.workspace]);
|
||||||
|
|
||||||
const deleteCurrentSession = useCallback(async () => {
|
const deleteCurrentSession = useCallback(async () => {
|
||||||
const conversationId = activeConversationId;
|
const conversationId = activeConversationId;
|
||||||
if (!conversationId) return;
|
if (!conversationId) return;
|
||||||
const workspaceId = state.workspace?.workspaceId;
|
const workspaceId = state.workspace?.workspaceId;
|
||||||
const response = await api.deleteConversation(conversationId, { projectId: WORKBENCH_PROJECT_ID, workspaceId, updatedByClient: "cloud-web-react" });
|
const response = await api.deleteConversation(conversationId, { projectId: activeProjectId, workspaceId, updatedByClient: "cloud-web-react" });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
dispatch({ type: "conversation:select", conversation: response.data?.workspace?.selectedConversation ?? null, workspace: response.data?.workspace ?? state.workspace });
|
dispatch({ type: "conversation:select", conversation: response.data?.workspace?.selectedConversation ?? null, workspace: response.data?.workspace ?? state.workspace });
|
||||||
const conversations = await api.conversations(WORKBENCH_PROJECT_ID);
|
const conversations = await api.conversations(activeProjectId);
|
||||||
if (conversations.ok) dispatch({ type: "conversation:list", conversations: conversations.data?.conversations ?? [] });
|
if (conversations.ok) dispatch({ type: "conversation:list", conversations: conversations.data?.conversations ?? [] });
|
||||||
}
|
}
|
||||||
}, [activeConversationId, state.workspace]);
|
}, [activeConversationId, activeProjectId, state.workspace]);
|
||||||
|
|
||||||
const submitMessage = useCallback(async (text: string) => {
|
const submitMessage = useCallback(async (text: string) => {
|
||||||
const value = text.trim();
|
const value = text.trim();
|
||||||
@@ -227,13 +231,13 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
const pending = makeMessage("agent", "正在处理这次 Code Agent 请求;复杂问题可能需要几分钟。", "running", { traceId, conversationId, sessionId, threadId, title: "Code Agent 处理中" });
|
const pending = makeMessage("agent", "正在处理这次 Code Agent 请求;复杂问题可能需要几分钟。", "running", { traceId, conversationId, sessionId, threadId, title: "Code Agent 处理中" });
|
||||||
const pendingWithRetry: ChatMessage = { ...pending, retryInput: value };
|
const pendingWithRetry: ChatMessage = { ...pending, retryInput: value };
|
||||||
dispatch({ type: "message:pending", user, pending: pendingWithRetry, request: { traceId, conversationId, sessionId, threadId } });
|
dispatch({ type: "message:pending", user, pending: pendingWithRetry, request: { traceId, conversationId, sessionId, threadId } });
|
||||||
let currentWorkspace = await persistConversation({ workspace: state.workspace, conversationId, sessionId, threadId, messages: [...state.messages, user, pendingWithRetry] });
|
let currentWorkspace = await persistConversation({ workspace: state.workspace, projectId: activeProjectId, conversationId, sessionId, threadId, messages: [...state.messages, user, pendingWithRetry] });
|
||||||
if (currentWorkspace) dispatch({ type: "workspace:sync", workspace: currentWorkspace });
|
if (currentWorkspace) dispatch({ type: "workspace:sync", workspace: currentWorkspace });
|
||||||
const route = steerMode ? api.steerAgentMessage : api.sendAgentMessage;
|
const route = steerMode ? api.steerAgentMessage : api.sendAgentMessage;
|
||||||
updateActivity();
|
updateActivity();
|
||||||
const submitActivityRef = buildActivityRef("code-agent-submit", null);
|
const submitActivityRef = buildActivityRef("code-agent-submit", null);
|
||||||
const response = await route({
|
const response = await route({
|
||||||
projectId: WORKBENCH_PROJECT_ID,
|
projectId: activeProjectId,
|
||||||
message: value,
|
message: value,
|
||||||
prompt: value,
|
prompt: value,
|
||||||
conversationId,
|
conversationId,
|
||||||
@@ -250,9 +254,9 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
const finalize = async (result: AgentChatResultResponse, availability: CodeAgentAvailability | null): Promise<void> => {
|
const finalize = async (result: AgentChatResultResponse, availability: CodeAgentAvailability | null): Promise<void> => {
|
||||||
const completed = messageFromAgentResponse(pending.id, pending, result);
|
const completed = messageFromAgentResponse(pending.id, pending, result);
|
||||||
dispatch({ type: "message:complete", messageId: pending.id, message: completed, availability: availability ?? result.availability ?? null });
|
dispatch({ type: "message:complete", messageId: pending.id, message: completed, availability: availability ?? result.availability ?? null });
|
||||||
currentWorkspace = await persistConversation({ workspace: currentWorkspace ?? state.workspace, conversationId, sessionId: completed.sessionId ?? sessionId, threadId: completed.threadId ?? threadId, messages: [...state.messages, user, completed] });
|
currentWorkspace = await persistConversation({ workspace: currentWorkspace ?? state.workspace, projectId: activeProjectId, conversationId, sessionId: completed.sessionId ?? sessionId, threadId: completed.threadId ?? threadId, messages: [...state.messages, user, completed] });
|
||||||
if (currentWorkspace) dispatch({ type: "workspace:sync", workspace: currentWorkspace });
|
if (currentWorkspace) dispatch({ type: "workspace:sync", workspace: currentWorkspace });
|
||||||
const conversations = await api.conversations(WORKBENCH_PROJECT_ID);
|
const conversations = await api.conversations(activeProjectId);
|
||||||
if (conversations.ok) dispatch({ type: "conversation:list", conversations: conversations.data?.conversations ?? [] });
|
if (conversations.ok) dispatch({ type: "conversation:list", conversations: conversations.data?.conversations ?? [] });
|
||||||
};
|
};
|
||||||
if (response.ok && response.data) {
|
if (response.ok && response.data) {
|
||||||
@@ -262,6 +266,7 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
try {
|
try {
|
||||||
await subscribeToTrace({
|
await subscribeToTrace({
|
||||||
traceId,
|
traceId,
|
||||||
|
projectId: activeProjectId,
|
||||||
initial: response.data,
|
initial: response.data,
|
||||||
inactivityTimeoutMs: state.codeAgentTimeoutMs,
|
inactivityTimeoutMs: state.codeAgentTimeoutMs,
|
||||||
signal: subscriptionController.signal,
|
signal: subscriptionController.signal,
|
||||||
@@ -278,7 +283,7 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
// the frontend-reported failure, not a silent "running" state.
|
// the frontend-reported failure, not a silent "running" state.
|
||||||
const failed = makeMessage("agent", `Code Agent 在 ${state.codeAgentTimeoutMs}ms 无活动后中断(${error});可以继续 steer 或查看 trace。`, "failed", { traceId, conversationId, sessionId, threadId, title: "Code Agent 超时" });
|
const failed = makeMessage("agent", `Code Agent 在 ${state.codeAgentTimeoutMs}ms 无活动后中断(${error});可以继续 steer 或查看 trace。`, "failed", { traceId, conversationId, sessionId, threadId, title: "Code Agent 超时" });
|
||||||
dispatch({ type: "message:fail", messageId: pending.id, message: failed });
|
dispatch({ type: "message:fail", messageId: pending.id, message: failed });
|
||||||
void persistConversation({ workspace: currentWorkspace ?? state.workspace, conversationId, sessionId, threadId, messages: [...state.messages, user, failed] })
|
void persistConversation({ workspace: currentWorkspace ?? state.workspace, projectId: activeProjectId, conversationId, sessionId, threadId, messages: [...state.messages, user, failed] })
|
||||||
.then((workspace) => { if (workspace) dispatch({ type: "workspace:sync", workspace }); });
|
.then((workspace) => { if (workspace) dispatch({ type: "workspace:sync", workspace }); });
|
||||||
subscriptionController.abort();
|
subscriptionController.abort();
|
||||||
}
|
}
|
||||||
@@ -294,21 +299,22 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
await finalize(mergeTraceResults(response.data, traceTerminal), response.data.availability ?? null);
|
await finalize(mergeTraceResults(response.data, traceTerminal), response.data.availability ?? null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const traceSnapshot = await fetchTraceSnapshot(traceId, traceId, Math.max(8000, state.codeAgentTimeoutMs));
|
const traceSnapshot = await fetchTraceSnapshot(traceId, traceId, Math.max(8000, state.codeAgentTimeoutMs), undefined, activeProjectId);
|
||||||
const failed = makeMessage("agent", response.error ?? "Code Agent 请求失败", "failed", { traceId, conversationId, sessionId, threadId, title: "Code Agent 请求失败" });
|
const failed = makeMessage("agent", response.error ?? "Code Agent 请求失败", "failed", { traceId, conversationId, sessionId, threadId, title: "Code Agent 请求失败" });
|
||||||
const runnerTrace = traceSnapshot ? snapshotToRunnerTrace(traceSnapshot) : null;
|
const runnerTrace = traceSnapshot ? snapshotToRunnerTrace(traceSnapshot) : null;
|
||||||
const failedWithTrace = runnerTrace ? { ...failed, runnerTrace } : failed;
|
const failedWithTrace = runnerTrace ? { ...failed, runnerTrace } : failed;
|
||||||
if (runnerTrace) dispatch({ type: "message:trace", messageId: pending.id, trace: runnerTrace });
|
if (runnerTrace) dispatch({ type: "message:trace", messageId: pending.id, trace: runnerTrace });
|
||||||
dispatch({ type: "message:fail", messageId: pending.id, message: failedWithTrace });
|
dispatch({ type: "message:fail", messageId: pending.id, message: failedWithTrace });
|
||||||
currentWorkspace = await persistConversation({ workspace: currentWorkspace ?? state.workspace, conversationId, sessionId, threadId, messages: [...state.messages, user, failedWithTrace] });
|
currentWorkspace = await persistConversation({ workspace: currentWorkspace ?? state.workspace, projectId: activeProjectId, conversationId, sessionId, threadId, messages: [...state.messages, user, failedWithTrace] });
|
||||||
if (currentWorkspace) dispatch({ type: "workspace:sync", workspace: currentWorkspace });
|
if (currentWorkspace) dispatch({ type: "workspace:sync", workspace: currentWorkspace });
|
||||||
}
|
}
|
||||||
dispatch({ type: "chat:done" });
|
dispatch({ type: "chat:done" });
|
||||||
}, [activeConversationId, composer.conversationId, composer.sessionId, composer.submitMode, composer.targetTraceId, composer.threadId, state.codeAgentTimeoutMs, state.gatewayShellTimeoutMs, state.messages, state.providerProfile, state.workspace]);
|
}, [activeConversationId, activeProjectId, composer.conversationId, composer.sessionId, composer.submitMode, composer.targetTraceId, composer.threadId, state.codeAgentTimeoutMs, state.gatewayShellTimeoutMs, state.messages, state.providerProfile, state.workspace]);
|
||||||
|
|
||||||
const cancelAgentMessage = useCallback(async (messageId: string) => {
|
const cancelAgentMessage = useCallback(async (messageId: string) => {
|
||||||
return cancelAgentMessageAction(messageId, {
|
return cancelAgentMessageAction(messageId, {
|
||||||
activeConversationId,
|
activeConversationId,
|
||||||
|
projectId: activeProjectId,
|
||||||
workspace: state.workspace,
|
workspace: state.workspace,
|
||||||
messages: state.messages,
|
messages: state.messages,
|
||||||
codeAgentTimeoutMs: state.codeAgentTimeoutMs,
|
codeAgentTimeoutMs: state.codeAgentTimeoutMs,
|
||||||
@@ -317,11 +323,12 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
onTrace: (id, trace) => dispatch({ type: "message:trace", messageId: id, trace }),
|
onTrace: (id, trace) => dispatch({ type: "message:trace", messageId: id, trace }),
|
||||||
submitMessage
|
submitMessage
|
||||||
});
|
});
|
||||||
}, [activeConversationId, state.workspace, state.messages, state.codeAgentTimeoutMs, submitMessage]);
|
}, [activeConversationId, activeProjectId, state.workspace, state.messages, state.codeAgentTimeoutMs, submitMessage]);
|
||||||
|
|
||||||
const retryAgentMessage = useCallback(async (messageId: string) => {
|
const retryAgentMessage = useCallback(async (messageId: string) => {
|
||||||
return retryAgentMessageAction(messageId, {
|
return retryAgentMessageAction(messageId, {
|
||||||
activeConversationId,
|
activeConversationId,
|
||||||
|
projectId: activeProjectId,
|
||||||
workspace: state.workspace,
|
workspace: state.workspace,
|
||||||
messages: state.messages,
|
messages: state.messages,
|
||||||
codeAgentTimeoutMs: state.codeAgentTimeoutMs,
|
codeAgentTimeoutMs: state.codeAgentTimeoutMs,
|
||||||
@@ -330,11 +337,12 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
onTrace: (id, trace) => dispatch({ type: "message:trace", messageId: id, trace }),
|
onTrace: (id, trace) => dispatch({ type: "message:trace", messageId: id, trace }),
|
||||||
submitMessage
|
submitMessage
|
||||||
});
|
});
|
||||||
}, [activeConversationId, state.workspace, state.messages, state.codeAgentTimeoutMs, submitMessage]);
|
}, [activeConversationId, activeProjectId, state.workspace, state.messages, state.codeAgentTimeoutMs, submitMessage]);
|
||||||
|
|
||||||
const replayAgentTrace = useCallback(async (messageId: string) => {
|
const replayAgentTrace = useCallback(async (messageId: string) => {
|
||||||
return replayAgentTraceAction(messageId, {
|
return replayAgentTraceAction(messageId, {
|
||||||
activeConversationId,
|
activeConversationId,
|
||||||
|
projectId: activeProjectId,
|
||||||
workspace: state.workspace,
|
workspace: state.workspace,
|
||||||
messages: state.messages,
|
messages: state.messages,
|
||||||
codeAgentTimeoutMs: state.codeAgentTimeoutMs,
|
codeAgentTimeoutMs: state.codeAgentTimeoutMs,
|
||||||
@@ -343,7 +351,7 @@ export function useWorkbenchStore(enabled: boolean): WorkbenchStore {
|
|||||||
onTrace: (id, trace) => dispatch({ type: "message:trace", messageId: id, trace }),
|
onTrace: (id, trace) => dispatch({ type: "message:trace", messageId: id, trace }),
|
||||||
submitMessage
|
submitMessage
|
||||||
});
|
});
|
||||||
}, [activeConversationId, state.workspace, state.messages, state.codeAgentTimeoutMs, submitMessage]);
|
}, [activeConversationId, activeProjectId, state.workspace, state.messages, state.codeAgentTimeoutMs, submitMessage]);
|
||||||
|
|
||||||
const clearConversation = useCallback(() => dispatch({ type: "conversation:clear" }), []);
|
const clearConversation = useCallback(() => dispatch({ type: "conversation:clear" }), []);
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface WorkbenchTimeoutConfig {
|
|||||||
export interface WorkbenchConfig {
|
export interface WorkbenchConfig {
|
||||||
auth?: WorkbenchAuthConfig;
|
auth?: WorkbenchAuthConfig;
|
||||||
timeouts?: WorkbenchTimeoutConfig;
|
timeouts?: WorkbenchTimeoutConfig;
|
||||||
|
projectId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkbenchWorkspaceBootstrap {
|
export interface WorkbenchWorkspaceBootstrap {
|
||||||
|
|||||||
@@ -109,12 +109,14 @@ export interface CodeAgentAvailability {
|
|||||||
|
|
||||||
export interface WorkspaceRecord {
|
export interface WorkspaceRecord {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
projectId?: string | null;
|
||||||
revision?: number;
|
revision?: number;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
selectedConversationId?: string | null;
|
selectedConversationId?: string | null;
|
||||||
selectedAgentSessionId?: string | null;
|
selectedAgentSessionId?: string | null;
|
||||||
activeTraceId?: string | null;
|
activeTraceId?: string | null;
|
||||||
workspace?: {
|
workspace?: {
|
||||||
|
projectId?: string | null;
|
||||||
selectedConversationId?: string | null;
|
selectedConversationId?: string | null;
|
||||||
selectedAgentSessionId?: string | null;
|
selectedAgentSessionId?: string | null;
|
||||||
threadId?: string | null;
|
threadId?: string | null;
|
||||||
@@ -128,6 +130,7 @@ export interface WorkspaceRecord {
|
|||||||
|
|
||||||
export interface ConversationRecord {
|
export interface ConversationRecord {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
projectId?: string | null;
|
||||||
sessionId?: string | null;
|
sessionId?: string | null;
|
||||||
threadId?: string | null;
|
threadId?: string | null;
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
@@ -373,6 +376,7 @@ export interface SessionTab {
|
|||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
projectId?: string | null;
|
||||||
conversationId?: string | null;
|
conversationId?: string | null;
|
||||||
sessionId?: string | null;
|
sessionId?: string | null;
|
||||||
threadId?: string | null;
|
threadId?: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user