源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
This commit is contained in:
commit
2ed3415fb7
8018 files changed
+1936476
No files matched your search
+415
@@ -0,0 +1,415 @@
|
||||
import {
|
||||
useCallback,
|
||||
type Dispatch,
|
||||
type RefObject,
|
||||
type SetStateAction
|
||||
} from "react";
|
||||
import {
|
||||
isPendingActivationViable,
|
||||
dispatchSessionForkThroughTurn,
|
||||
selectLatestActivationForSession,
|
||||
selectEngineSession,
|
||||
type AgentSessionEngine
|
||||
} from "@tutti-os/agent-activity-core";
|
||||
import { areWorkspaceUserProjectPathsEqual } from "@tutti-os/workspace-user-project/core";
|
||||
import type { AgentGUIRuntime } from "../../../agentActivityRuntime";
|
||||
import type { useAgentHostApi } from "../../../agentActivityHost";
|
||||
import type { AgentHostUserProject } from "../../../host/agentHostApi";
|
||||
import { translate } from "../../../i18n/index";
|
||||
import type { AgentGUINodeData } from "../../../types";
|
||||
import {
|
||||
getAgentGUIErrorCode,
|
||||
getAgentGUIErrorMessage,
|
||||
getAgentGUIErrorReason,
|
||||
isSessionNotFoundErrorCode
|
||||
} from "./agentGuiController.errors";
|
||||
import {
|
||||
reportAgentGUIRuntimeError,
|
||||
showAgentGUIControllerErrorToast
|
||||
} from "./agentGuiController.reporting";
|
||||
|
||||
export interface UseAgentGUIConversationMetadataActionsInput {
|
||||
agentHostApi: ReturnType<typeof useAgentHostApi>;
|
||||
setListError: Dispatch<SetStateAction<string | null>>;
|
||||
setUserProjectsSnapshot: (projects: readonly AgentHostUserProject[]) => void;
|
||||
userProjectsRef: RefObject<AgentHostUserProject[]>;
|
||||
setDetailError: Dispatch<SetStateAction<string | null>>;
|
||||
agentActivityRuntime: AgentGUIRuntime;
|
||||
dataRef: RefObject<AgentGUINodeData>;
|
||||
workspaceId: string;
|
||||
sessionEngine: AgentSessionEngine;
|
||||
currentUserId: string | null | undefined;
|
||||
selectConversation?: (agentSessionId: string) => void;
|
||||
}
|
||||
|
||||
const CANONICAL_SESSION_WAIT_TIMEOUT_MS = 30_000;
|
||||
|
||||
type CanonicalSessionWaitResult = "ready" | "unavailable" | "timed_out";
|
||||
|
||||
async function waitForCanonicalSession(
|
||||
sessionEngine: AgentSessionEngine,
|
||||
agentSessionId: string
|
||||
): Promise<CanonicalSessionWaitResult> {
|
||||
const hasCanonicalSession = (): boolean =>
|
||||
selectEngineSession(sessionEngine.getSnapshot(), agentSessionId) !== null;
|
||||
if (hasCanonicalSession()) {
|
||||
return "ready";
|
||||
}
|
||||
|
||||
const pendingActivation = selectLatestActivationForSession(
|
||||
sessionEngine.getSnapshot(),
|
||||
agentSessionId
|
||||
);
|
||||
if (!isPendingActivationViable(pendingActivation)) {
|
||||
return "unavailable";
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
let active = true;
|
||||
const timeoutSignal = AbortSignal.timeout(
|
||||
CANONICAL_SESSION_WAIT_TIMEOUT_MS
|
||||
);
|
||||
const finish = (result: CanonicalSessionWaitResult): void => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
active = false;
|
||||
timeoutSignal.removeEventListener("abort", onTimeout);
|
||||
unsubscribe?.();
|
||||
unsubscribe = null;
|
||||
resolve(result);
|
||||
};
|
||||
const check = (): void => {
|
||||
if (hasCanonicalSession()) {
|
||||
finish("ready");
|
||||
return;
|
||||
}
|
||||
const activation = selectLatestActivationForSession(
|
||||
sessionEngine.getSnapshot(),
|
||||
agentSessionId
|
||||
);
|
||||
if (!isPendingActivationViable(activation)) {
|
||||
finish("unavailable");
|
||||
}
|
||||
};
|
||||
function onTimeout(): void {
|
||||
finish("timed_out");
|
||||
}
|
||||
timeoutSignal.addEventListener("abort", onTimeout, { once: true });
|
||||
const registeredUnsubscribe = sessionEngine.subscribe(check);
|
||||
if (active) {
|
||||
unsubscribe = registeredUnsubscribe;
|
||||
} else {
|
||||
registeredUnsubscribe();
|
||||
}
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
export function useAgentGUIConversationMetadataActions(
|
||||
input: UseAgentGUIConversationMetadataActionsInput
|
||||
) {
|
||||
const {
|
||||
agentHostApi,
|
||||
setListError,
|
||||
setUserProjectsSnapshot,
|
||||
userProjectsRef,
|
||||
setDetailError,
|
||||
agentActivityRuntime,
|
||||
dataRef,
|
||||
workspaceId,
|
||||
sessionEngine,
|
||||
currentUserId
|
||||
} = input;
|
||||
|
||||
const removeProject = useCallback(
|
||||
async (path: string): Promise<boolean> => {
|
||||
const normalizedPath = path.trim();
|
||||
const remove = agentHostApi.userProjects?.remove;
|
||||
if (!normalizedPath || !remove) {
|
||||
return false;
|
||||
}
|
||||
setListError(null);
|
||||
// Filter the visible list only after the backend confirms the delete
|
||||
// (mirroring registerProjectPath's "backend confirm -> local store
|
||||
// update" ordering). Filtering optimistically before the delete
|
||||
// resolves would flip userProjectPathKey early and race the runtime
|
||||
// rail sections refetch against the in-flight backend delete: if the
|
||||
// section list query lands before the delete commits, it still
|
||||
// reports the removed project's section, and nothing re-triggers a
|
||||
// refetch afterwards, so the row stays visible until an unrelated
|
||||
// remount forces a fresh fetch.
|
||||
const handleRemoveError = (error: unknown): false => {
|
||||
const message = getAgentGUIErrorMessage(error);
|
||||
setListError(message);
|
||||
showAgentGUIControllerErrorToast(agentHostApi.toast, message);
|
||||
return false;
|
||||
};
|
||||
try {
|
||||
await remove({ path: normalizedPath });
|
||||
setUserProjectsSnapshot(
|
||||
userProjectsRef.current.filter(
|
||||
(project) =>
|
||||
!areWorkspaceUserProjectPathsEqual(project.path, normalizedPath)
|
||||
)
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return handleRemoveError(error);
|
||||
}
|
||||
},
|
||||
[agentHostApi.toast, agentHostApi.userProjects, setUserProjectsSnapshot]
|
||||
);
|
||||
|
||||
const moveProject = useCallback(
|
||||
async (projectId: string, beforeProjectId: string | null) => {
|
||||
const move = agentHostApi.userProjects?.move;
|
||||
if (!move) return;
|
||||
agentHostApi.debug?.logRuntimeDiagnostics?.({
|
||||
beforeProjectId,
|
||||
phase: "move_user_project_requested",
|
||||
projectId
|
||||
});
|
||||
try {
|
||||
await move({ beforeProjectId, projectId });
|
||||
agentHostApi.debug?.logRuntimeDiagnostics?.({
|
||||
beforeProjectId,
|
||||
phase: "move_user_project_succeeded",
|
||||
projectId
|
||||
});
|
||||
} catch (error) {
|
||||
agentHostApi.debug?.logRuntimeDiagnostics?.({
|
||||
beforeProjectId,
|
||||
error: getAgentGUIErrorMessage(error),
|
||||
phase: "move_user_project_failed",
|
||||
projectId
|
||||
});
|
||||
}
|
||||
},
|
||||
[agentHostApi.debug, agentHostApi.userProjects]
|
||||
);
|
||||
|
||||
const toggleProjectPinned = useCallback(
|
||||
async (projectId: string, pinned: boolean) => {
|
||||
const normalizedProjectId = projectId.trim();
|
||||
const pin = agentHostApi.userProjects?.pin;
|
||||
if (!normalizedProjectId || !pin) return;
|
||||
agentHostApi.debug?.logRuntimeDiagnostics?.({
|
||||
phase: "pin_user_project_requested",
|
||||
pinned,
|
||||
projectId: normalizedProjectId
|
||||
});
|
||||
try {
|
||||
await pin({ projectId: normalizedProjectId, pinned });
|
||||
agentHostApi.debug?.logRuntimeDiagnostics?.({
|
||||
phase: "pin_user_project_succeeded",
|
||||
pinned,
|
||||
projectId: normalizedProjectId
|
||||
});
|
||||
} catch (error) {
|
||||
agentHostApi.debug?.logRuntimeDiagnostics?.({
|
||||
error: getAgentGUIErrorMessage(error),
|
||||
phase: "pin_user_project_failed",
|
||||
pinned,
|
||||
projectId: normalizedProjectId
|
||||
});
|
||||
}
|
||||
},
|
||||
[agentHostApi.debug, agentHostApi.userProjects]
|
||||
);
|
||||
|
||||
const toggleConversationPinned = useCallback(
|
||||
(agentSessionId: string, pinned: boolean) => {
|
||||
const normalizedAgentSessionId = agentSessionId.trim();
|
||||
if (!normalizedAgentSessionId) {
|
||||
return;
|
||||
}
|
||||
setDetailError(null);
|
||||
void agentActivityRuntime
|
||||
.setSessionPinned({
|
||||
workspaceId,
|
||||
agentSessionId: normalizedAgentSessionId,
|
||||
pinned
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = getAgentGUIErrorMessage(error);
|
||||
reportAgentGUIRuntimeError({
|
||||
agentSessionId: normalizedAgentSessionId,
|
||||
context: { pinned },
|
||||
error,
|
||||
phase: "toggle_conversation_pinned",
|
||||
provider: dataRef.current.provider,
|
||||
runtime: agentActivityRuntime,
|
||||
workspaceId
|
||||
});
|
||||
showAgentGUIControllerErrorToast(agentHostApi.toast, message);
|
||||
});
|
||||
},
|
||||
[agentActivityRuntime, agentHostApi.toast, workspaceId]
|
||||
);
|
||||
|
||||
const markConversationUnread = useCallback(
|
||||
(agentSessionId: string) => {
|
||||
const normalizedAgentSessionId = agentSessionId.trim();
|
||||
if (!normalizedAgentSessionId) {
|
||||
return;
|
||||
}
|
||||
sessionEngine.dispatch({
|
||||
type: "attention/unreadRequested",
|
||||
agentSessionId: normalizedAgentSessionId,
|
||||
userId: currentUserId?.trim() ?? ""
|
||||
});
|
||||
},
|
||||
[currentUserId, sessionEngine]
|
||||
);
|
||||
|
||||
const renameConversation = useCallback(
|
||||
async (agentSessionId: string, title: string) => {
|
||||
const normalizedAgentSessionId = agentSessionId.trim();
|
||||
const normalizedTitle = title.trim();
|
||||
if (!normalizedAgentSessionId) {
|
||||
return;
|
||||
}
|
||||
setDetailError(null);
|
||||
try {
|
||||
const canonicalSessionWait = await waitForCanonicalSession(
|
||||
sessionEngine,
|
||||
normalizedAgentSessionId
|
||||
);
|
||||
if (canonicalSessionWait !== "ready") {
|
||||
throw Object.assign(
|
||||
new Error(translate("agentHost.agentGui.sessionActionUnavailable")),
|
||||
{
|
||||
reason: `agent_gui_rename_session_${canonicalSessionWait}`
|
||||
}
|
||||
);
|
||||
}
|
||||
await sessionEngine.renameSession({
|
||||
agentSessionId: normalizedAgentSessionId,
|
||||
title: normalizedTitle
|
||||
});
|
||||
} catch (error) {
|
||||
const message = getAgentGUIErrorMessage(error);
|
||||
reportAgentGUIRuntimeError({
|
||||
agentSessionId: normalizedAgentSessionId,
|
||||
context: { titleLength: normalizedTitle.length },
|
||||
error,
|
||||
phase: "rename_conversation",
|
||||
provider: dataRef.current.provider,
|
||||
runtime: agentActivityRuntime,
|
||||
workspaceId
|
||||
});
|
||||
showAgentGUIControllerErrorToast(agentHostApi.toast, message);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[
|
||||
agentActivityRuntime,
|
||||
agentHostApi.toast,
|
||||
dataRef,
|
||||
sessionEngine,
|
||||
setDetailError,
|
||||
workspaceId
|
||||
]
|
||||
);
|
||||
|
||||
const forkConversationThroughTurn = useCallback(
|
||||
async (sourceAgentSessionId: string, turnId: string) => {
|
||||
const normalizedSourceAgentSessionId = sourceAgentSessionId.trim();
|
||||
const normalizedTurnId = turnId.trim();
|
||||
if (!normalizedSourceAgentSessionId || !normalizedTurnId) {
|
||||
return;
|
||||
}
|
||||
setDetailError(null);
|
||||
try {
|
||||
const result = await dispatchSessionForkThroughTurn(sessionEngine, {
|
||||
sourceAgentSessionId: normalizedSourceAgentSessionId,
|
||||
turnId: normalizedTurnId,
|
||||
workspaceId
|
||||
});
|
||||
input.selectConversation?.(result.targetAgentSessionId);
|
||||
} catch (error) {
|
||||
const message = getAgentGUIErrorMessage(error);
|
||||
reportAgentGUIRuntimeError({
|
||||
agentSessionId: normalizedSourceAgentSessionId,
|
||||
context: { turnId: normalizedTurnId },
|
||||
error,
|
||||
phase: "fork_conversation_through_turn",
|
||||
provider: dataRef.current.provider,
|
||||
runtime: agentActivityRuntime,
|
||||
workspaceId
|
||||
});
|
||||
showAgentGUIControllerErrorToast(agentHostApi.toast, message);
|
||||
}
|
||||
},
|
||||
[
|
||||
agentActivityRuntime,
|
||||
agentHostApi.toast,
|
||||
dataRef,
|
||||
input.selectConversation,
|
||||
sessionEngine,
|
||||
setDetailError,
|
||||
workspaceId
|
||||
]
|
||||
);
|
||||
|
||||
const openForkSourceConversation = useCallback(
|
||||
async (sourceAgentSessionId: string) => {
|
||||
const normalizedSourceAgentSessionId = sourceAgentSessionId.trim();
|
||||
if (!normalizedSourceAgentSessionId) {
|
||||
return;
|
||||
}
|
||||
setDetailError(null);
|
||||
try {
|
||||
const sourceSession = await agentActivityRuntime.getSession(
|
||||
workspaceId,
|
||||
normalizedSourceAgentSessionId
|
||||
);
|
||||
if (
|
||||
sourceSession.agentSessionId.trim() !== normalizedSourceAgentSessionId
|
||||
) {
|
||||
throw new Error(
|
||||
"source agent Session query returned a different Session"
|
||||
);
|
||||
}
|
||||
input.selectConversation?.(normalizedSourceAgentSessionId);
|
||||
} catch (error) {
|
||||
const reason = getAgentGUIErrorReason(error);
|
||||
const message =
|
||||
isSessionNotFoundErrorCode(getAgentGUIErrorCode(error)) ||
|
||||
reason === "workspace_agent_session_not_found"
|
||||
? translate("agentHost.agentGui.sourceConversationNotFound")
|
||||
: getAgentGUIErrorMessage(error);
|
||||
reportAgentGUIRuntimeError({
|
||||
agentSessionId: normalizedSourceAgentSessionId,
|
||||
error,
|
||||
phase: "open_fork_source_conversation",
|
||||
provider: dataRef.current.provider,
|
||||
runtime: agentActivityRuntime,
|
||||
workspaceId
|
||||
});
|
||||
showAgentGUIControllerErrorToast(agentHostApi.toast, message);
|
||||
}
|
||||
},
|
||||
[
|
||||
agentActivityRuntime,
|
||||
agentHostApi.toast,
|
||||
dataRef,
|
||||
input.selectConversation,
|
||||
setDetailError,
|
||||
workspaceId
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
forkConversationThroughTurn,
|
||||
openForkSourceConversation,
|
||||
moveProject,
|
||||
removeProject,
|
||||
toggleProjectPinned,
|
||||
toggleConversationPinned,
|
||||
markConversationUnread,
|
||||
renameConversation
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user