源码快照 @ 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:
+220
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
normalizeWorkspaceUserProjectPath,
|
||||
resolveWorkspaceUserProjectDisplayLabel,
|
||||
workspaceUserProjectPathIdentityKey
|
||||
} from "@tutti-os/workspace-user-project/core";
|
||||
import type { AgentHostUserProject } from "../../../host/agentHostApi";
|
||||
|
||||
const AGENT_GUI_CONVERSATION_PROJECT_SUMMARY_CACHE_LIMIT = 512;
|
||||
const agentGUIConversationProjectSummaryCache = new Map<
|
||||
string,
|
||||
AgentGUIConversationProjectSummary
|
||||
>();
|
||||
|
||||
export interface AgentGUIConversationProjectSummary {
|
||||
id: string;
|
||||
path: string;
|
||||
label: string;
|
||||
sectionKey?: string;
|
||||
createdAtUnixMs?: number;
|
||||
updatedAtUnixMs?: number;
|
||||
lastUsedAtUnixMs?: number;
|
||||
pinnedAtUnixMs: number;
|
||||
}
|
||||
|
||||
export type AgentGUIConversationUserProject = Pick<
|
||||
AgentHostUserProject,
|
||||
| "id"
|
||||
| "path"
|
||||
| "label"
|
||||
| "sectionKey"
|
||||
| "createdAtUnixMs"
|
||||
| "updatedAtUnixMs"
|
||||
| "lastUsedAtUnixMs"
|
||||
| "pinnedAtUnixMs"
|
||||
>;
|
||||
|
||||
export interface AgentGUIConversationProjectResolver {
|
||||
resolveSectionKey: (
|
||||
railSectionKey: string | null | undefined
|
||||
) => AgentGUIConversationProjectSummary | null;
|
||||
}
|
||||
|
||||
export function createAgentGUIConversationProjectResolver(
|
||||
userProjects: readonly AgentGUIConversationUserProject[] = []
|
||||
): AgentGUIConversationProjectResolver {
|
||||
const projectBySectionKey =
|
||||
buildAgentGUIConversationProjectIndex(userProjects);
|
||||
return {
|
||||
resolveSectionKey: (railSectionKey) => {
|
||||
const sectionKey = normalizeAgentGUIProjectSectionKey(railSectionKey);
|
||||
if (!sectionKey || sectionKey === "conversations") {
|
||||
return null;
|
||||
}
|
||||
const project = projectBySectionKey.get(sectionKey);
|
||||
return project
|
||||
? agentGUIConversationProjectSummaryFromProject(project)
|
||||
: null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAgentGUIConversationProjectBySectionKey(
|
||||
railSectionKey: string | null | undefined,
|
||||
userProjects: readonly AgentGUIConversationUserProject[] = []
|
||||
): AgentGUIConversationProjectSummary | null {
|
||||
return createAgentGUIConversationProjectResolver(
|
||||
userProjects
|
||||
).resolveSectionKey(railSectionKey);
|
||||
}
|
||||
|
||||
export function resolveAgentGUISelectedUserProject(
|
||||
selectedPath: string | null | undefined,
|
||||
userProjects: readonly AgentGUIConversationUserProject[] = []
|
||||
): AgentGUIConversationProjectSummary | null {
|
||||
const normalizedSelectedPath =
|
||||
workspaceUserProjectPathIdentityKey(selectedPath);
|
||||
if (!normalizedSelectedPath) {
|
||||
return null;
|
||||
}
|
||||
const projectByNormalizedPath =
|
||||
buildAgentGUISelectedProjectPathIndex(userProjects);
|
||||
const project = lookupAgentGUISelectedProject(
|
||||
normalizedSelectedPath,
|
||||
projectByNormalizedPath
|
||||
);
|
||||
return project
|
||||
? agentGUIConversationProjectSummaryFromProject(project)
|
||||
: null;
|
||||
}
|
||||
|
||||
function buildAgentGUIConversationProjectIndex(
|
||||
userProjects: readonly AgentGUIConversationUserProject[]
|
||||
): ReadonlyMap<string, AgentGUIConversationUserProject> {
|
||||
const projectBySectionKey = new Map<
|
||||
string,
|
||||
AgentGUIConversationUserProject
|
||||
>();
|
||||
for (const project of userProjects) {
|
||||
const sectionKey = normalizeAgentGUIProjectSectionKey(project.sectionKey);
|
||||
if (
|
||||
!sectionKey ||
|
||||
sectionKey === "conversations" ||
|
||||
projectBySectionKey.has(sectionKey)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
projectBySectionKey.set(sectionKey, project);
|
||||
}
|
||||
return projectBySectionKey;
|
||||
}
|
||||
|
||||
function normalizeAgentGUIProjectSectionKey(
|
||||
sectionKey: string | null | undefined
|
||||
): string {
|
||||
return sectionKey?.trim() ?? "";
|
||||
}
|
||||
|
||||
function agentGUIConversationProjectSummaryFromProject(
|
||||
matchedProject: AgentGUIConversationUserProject
|
||||
): AgentGUIConversationProjectSummary {
|
||||
const sectionKey = normalizeAgentGUIProjectSectionKey(
|
||||
matchedProject.sectionKey
|
||||
);
|
||||
const summary: AgentGUIConversationProjectSummary = {
|
||||
id: matchedProject.id,
|
||||
path: matchedProject.path,
|
||||
label: resolveWorkspaceUserProjectDisplayLabel(matchedProject),
|
||||
pinnedAtUnixMs: matchedProject.pinnedAtUnixMs
|
||||
};
|
||||
if (sectionKey) {
|
||||
summary.sectionKey = sectionKey;
|
||||
}
|
||||
if (matchedProject.createdAtUnixMs !== undefined) {
|
||||
summary.createdAtUnixMs = matchedProject.createdAtUnixMs;
|
||||
}
|
||||
if (matchedProject.updatedAtUnixMs !== undefined) {
|
||||
summary.updatedAtUnixMs = matchedProject.updatedAtUnixMs;
|
||||
}
|
||||
if (matchedProject.lastUsedAtUnixMs !== undefined) {
|
||||
summary.lastUsedAtUnixMs = matchedProject.lastUsedAtUnixMs;
|
||||
}
|
||||
return cachedAgentGUIConversationProjectSummary(summary);
|
||||
}
|
||||
|
||||
function buildAgentGUISelectedProjectPathIndex(
|
||||
userProjects: readonly AgentGUIConversationUserProject[]
|
||||
): ReadonlyMap<string, AgentGUIConversationUserProject> {
|
||||
const projectByNormalizedPath = new Map<
|
||||
string,
|
||||
AgentGUIConversationUserProject
|
||||
>();
|
||||
for (const project of userProjects) {
|
||||
const projectPath = workspaceUserProjectPathIdentityKey(project.path);
|
||||
if (!projectPath || projectByNormalizedPath.has(projectPath)) {
|
||||
continue;
|
||||
}
|
||||
projectByNormalizedPath.set(projectPath, project);
|
||||
}
|
||||
return projectByNormalizedPath;
|
||||
}
|
||||
|
||||
function lookupAgentGUISelectedProject(
|
||||
normalizedSelectedPath: string,
|
||||
projectByNormalizedPath: ReadonlyMap<string, AgentGUIConversationUserProject>
|
||||
): AgentGUIConversationUserProject | null {
|
||||
let currentPath = normalizedSelectedPath;
|
||||
while (currentPath) {
|
||||
const project = projectByNormalizedPath.get(currentPath);
|
||||
if (project) {
|
||||
return project;
|
||||
}
|
||||
const slashIndex = currentPath.lastIndexOf("/");
|
||||
if (slashIndex <= 0) {
|
||||
break;
|
||||
}
|
||||
currentPath = currentPath.slice(0, slashIndex);
|
||||
}
|
||||
if (normalizedSelectedPath === "/") {
|
||||
return projectByNormalizedPath.get("/") ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeAgentGUIProjectPath(
|
||||
path: string | null | undefined
|
||||
): string {
|
||||
return normalizeWorkspaceUserProjectPath(path);
|
||||
}
|
||||
|
||||
function cachedAgentGUIConversationProjectSummary(
|
||||
summary: AgentGUIConversationProjectSummary
|
||||
): AgentGUIConversationProjectSummary {
|
||||
const key = [
|
||||
summary.id,
|
||||
summary.path,
|
||||
summary.label,
|
||||
summary.sectionKey ?? "",
|
||||
summary.createdAtUnixMs ?? "",
|
||||
summary.updatedAtUnixMs ?? "",
|
||||
summary.lastUsedAtUnixMs ?? "",
|
||||
summary.pinnedAtUnixMs ?? ""
|
||||
].join("\u001f");
|
||||
const cached = agentGUIConversationProjectSummaryCache.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
if (
|
||||
agentGUIConversationProjectSummaryCache.size >=
|
||||
AGENT_GUI_CONVERSATION_PROJECT_SUMMARY_CACHE_LIMIT
|
||||
) {
|
||||
const oldestKey = agentGUIConversationProjectSummaryCache
|
||||
.keys()
|
||||
.next().value;
|
||||
if (oldestKey) {
|
||||
agentGUIConversationProjectSummaryCache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
agentGUIConversationProjectSummaryCache.set(key, summary);
|
||||
return summary;
|
||||
}
|
||||
Reference in New Issue
Block a user