121 lines
5.2 KiB
TypeScript
121 lines
5.2 KiB
TypeScript
/*
|
|
* SPEC: pikasTech/HWLAB#2359 Workbench session detail metadata-only contract.
|
|
* Responsibility: Build Workbench session detail payloads and parse detail payload mode flags.
|
|
*/
|
|
|
|
export function includeMessagesForSessionDetail(params: URLSearchParams): boolean {
|
|
if (params.get("includeMessages") !== null) return truthyQueryFlag(params.get("includeMessages"));
|
|
if (params.get("messages") !== null) return truthyQueryFlag(params.get("messages"));
|
|
return false;
|
|
}
|
|
|
|
export function buildWorkbenchSessionDetail(input: {
|
|
summary: Record<string, unknown>;
|
|
session: Record<string, unknown> | null | undefined;
|
|
sessionId: string | null | undefined;
|
|
includeMessages: boolean;
|
|
messages: unknown[];
|
|
}): Record<string, unknown> {
|
|
const detail = {
|
|
...input.summary,
|
|
metadata: {
|
|
startedAt: input.session?.startedAt ?? input.session?.createdAt ?? null,
|
|
endedAt: input.session?.endedAt ?? null,
|
|
ownerUserId: input.session?.ownerUserId ?? null,
|
|
launchContext: compactLaunchContext(sessionJson(input.session)?.launchContext)
|
|
},
|
|
messagePageUrl: input.sessionId ? `/v1/workbench/sessions/${encodeURIComponent(input.sessionId)}/messages` : null,
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
};
|
|
return input.includeMessages ? { ...detail, messages: input.messages } : detail;
|
|
}
|
|
|
|
export function compactLaunchContext(value: unknown): Record<string, unknown> | null {
|
|
const record = objectRecord(value);
|
|
if (!record) return null;
|
|
const projectId = textValue(record.projectId) || null;
|
|
const taskRef = textValue(record.taskRef) || null;
|
|
if (!projectId && !taskRef) return null;
|
|
const execution = compactLaunchExecutionContext(record.executionContext ?? record.execution ?? record);
|
|
return {
|
|
source: textValue(record.source) || "project-management",
|
|
projectId,
|
|
taskRef,
|
|
sourceId: textValue(record.sourceId) || null,
|
|
sourceKind: textValue(record.sourceKind) || execution?.sourceKind || null,
|
|
fileRef: textValue(record.fileRef) || null,
|
|
relativePath: textValue(record.relativePath) || execution?.relativePath || null,
|
|
taskId: textValue(record.taskId) || null,
|
|
title: textValue(record.title) || null,
|
|
status: textValue(record.status) || null,
|
|
hwpodId: (execution?.hwpodId ?? textValue(record.hwpodId)) || null,
|
|
nodeId: (execution?.nodeId ?? textValue(record.nodeId)) || null,
|
|
mdtodoRootRef: (execution?.mdtodoRootRef ?? textValue(record.mdtodoRootRef)) || null,
|
|
workspaceRootHash: (execution?.workspaceRootHash ?? textValue(record.workspaceRootHash)) || null,
|
|
workspaceRootLabel: (execution?.workspaceRootLabel ?? textValue(record.workspaceRootLabel)) || null,
|
|
hwpodWorkspaceArgs: (execution?.hwpodWorkspaceArgs ?? textValue(record.hwpodWorkspaceArgs)) || null,
|
|
contextFingerprint: (execution?.contextFingerprint ?? textValue(record.contextFingerprint)) || null,
|
|
capabilities: compactPlainObject(record.capabilities),
|
|
executionContext: execution,
|
|
route: textValue(record.route) || "/projects/mdtodo",
|
|
valuesRedacted: true
|
|
};
|
|
}
|
|
|
|
function compactLaunchExecutionContext(value: unknown): Record<string, unknown> | null {
|
|
const record = objectRecord(value);
|
|
if (!record) return null;
|
|
const sourceId = textValue(record.sourceId) || null;
|
|
const taskRef = textValue(record.taskRef) || null;
|
|
const hwpodId = textValue(record.hwpodId) || null;
|
|
if (!sourceId && !taskRef && !hwpodId) return null;
|
|
return {
|
|
sourceId,
|
|
sourceKind: textValue(record.sourceKind) || null,
|
|
projectId: textValue(record.projectId) || null,
|
|
taskRef,
|
|
fileRef: textValue(record.fileRef) || null,
|
|
relativePath: textValue(record.relativePath) || null,
|
|
taskId: textValue(record.taskId) || null,
|
|
hwpodId,
|
|
nodeId: textValue(record.nodeId) || null,
|
|
mdtodoRootRef: textValue(record.mdtodoRootRef) || null,
|
|
workspaceRootRef: textValue(record.workspaceRootRef) || null,
|
|
workspaceRootHash: textValue(record.workspaceRootHash) || null,
|
|
workspaceRootLabel: textValue(record.workspaceRootLabel) || null,
|
|
hwpodWorkspaceArgs: textValue(record.hwpodWorkspaceArgs) || null,
|
|
contextFingerprint: textValue(record.contextFingerprint) || null,
|
|
capabilities: compactPlainObject(record.capabilities),
|
|
valuesRedacted: true
|
|
};
|
|
}
|
|
|
|
function compactPlainObject(value: unknown): Record<string, unknown> {
|
|
const record = objectRecord(value);
|
|
if (!record) return {};
|
|
const out: Record<string, unknown> = {};
|
|
for (const [key, item] of Object.entries(record)) {
|
|
if (!/^[A-Za-z0-9_.:-]{1,80}$/u.test(key)) continue;
|
|
if (["string", "number", "boolean"].includes(typeof item) || item === null) out[key] = item;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function sessionJson(session: Record<string, unknown> | null | undefined): Record<string, unknown> | null {
|
|
return objectRecord(session?.sessionJson);
|
|
}
|
|
|
|
function objectRecord(value: unknown): Record<string, unknown> | null {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
}
|
|
|
|
function textValue(value: unknown): string {
|
|
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
}
|
|
|
|
function truthyQueryFlag(value: string | null): boolean {
|
|
const normalized = textValue(value).toLowerCase();
|
|
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
}
|