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
506 lines
16 KiB
TypeScript
506 lines
16 KiB
TypeScript
import type {
|
|
WorkbenchHostExternalStateLookupInput,
|
|
WorkbenchHostExternalStateSource
|
|
} from "@tutti-os/workbench-surface";
|
|
import {
|
|
agentGuiWorkbenchProviders,
|
|
normalizeAgentGuiWorkbenchProvider
|
|
} from "./providerCatalog.ts";
|
|
import type {
|
|
AgentGuiWorkbenchComposerOverrides,
|
|
AgentGuiWorkbenchComposerOverridesByAgentTargetId,
|
|
AgentGuiWorkbenchComposerOverridesByProvider,
|
|
AgentGuiWorkbenchNodeState,
|
|
AgentGuiWorkbenchProvider,
|
|
AgentGuiWorkbenchState,
|
|
AgentGuiWorkbenchWorkspaceState
|
|
} from "./types.ts";
|
|
|
|
type AgentGuiWorkbenchStateLookupRequest = Pick<
|
|
WorkbenchHostExternalStateLookupInput,
|
|
"instanceId" | "typeId"
|
|
> &
|
|
Partial<Pick<WorkbenchHostExternalStateLookupInput, "nodeId">>;
|
|
|
|
export function createDefaultAgentGuiWorkbenchNodeState(
|
|
provider: AgentGuiWorkbenchProvider = "codex"
|
|
): AgentGuiWorkbenchNodeState {
|
|
return {
|
|
agentTargetId: null,
|
|
composerOverrides: null,
|
|
composerOverridesByAgentTargetId: null,
|
|
composerOverridesByProvider: null,
|
|
conversationCount: null,
|
|
conversationRailCollapsed: false,
|
|
conversationRailWidthPx: null,
|
|
lastActiveAgentSessionId: null,
|
|
lastActiveAgentSessionIdByAgentTargetId: null,
|
|
provider
|
|
};
|
|
}
|
|
|
|
export function normalizeAgentGuiWorkbenchState(
|
|
state: unknown
|
|
): AgentGuiWorkbenchState {
|
|
if (!isRecord(state)) {
|
|
return createDefaultAgentGuiWorkbenchState();
|
|
}
|
|
const agentTargetId = normalizeOptionalNonEmptyString(state.agentTargetId);
|
|
const lastActiveAgentSessionIdByAgentTargetId =
|
|
normalizeAgentGuiWorkbenchLastActiveSessionByAgentTargetId(
|
|
state.lastActiveAgentSessionIdByAgentTargetId
|
|
);
|
|
return {
|
|
...(agentTargetId ? { agentTargetId } : {}),
|
|
conversationRailCollapsed: state.conversationRailCollapsed === true,
|
|
conversationRailWidthPx: normalizeOptionalPositiveNumber(
|
|
state.conversationRailWidthPx
|
|
),
|
|
lastActiveAgentSessionId:
|
|
typeof state.lastActiveAgentSessionId === "string"
|
|
? state.lastActiveAgentSessionId
|
|
: null,
|
|
...(lastActiveAgentSessionIdByAgentTargetId
|
|
? { lastActiveAgentSessionIdByAgentTargetId }
|
|
: {})
|
|
};
|
|
}
|
|
|
|
/**
|
|
* One-time persisted-state reader for snapshots written before agentTargetId
|
|
* became the canonical AgentGUI selection key. Current-state normalizers stay
|
|
* strict so live callers cannot continue using the removed alias.
|
|
*/
|
|
export function migrateLegacyAgentGuiWorkbenchState(state: unknown): unknown {
|
|
if (
|
|
!isRecord(state) ||
|
|
normalizeOptionalNonEmptyString(state.agentTargetId)
|
|
) {
|
|
return state;
|
|
}
|
|
const legacyAgentTargetId = normalizeOptionalNonEmptyString(
|
|
state.providerTargetId
|
|
);
|
|
return legacyAgentTargetId
|
|
? { ...state, agentTargetId: legacyAgentTargetId }
|
|
: state;
|
|
}
|
|
|
|
export function projectAgentGuiWorkbenchState(
|
|
state: AgentGuiWorkbenchNodeState
|
|
): AgentGuiWorkbenchState {
|
|
const agentTargetId = normalizeOptionalNonEmptyString(state.agentTargetId);
|
|
const lastActiveAgentSessionIdByAgentTargetId =
|
|
normalizeAgentGuiWorkbenchLastActiveSessionByAgentTargetId(
|
|
state.lastActiveAgentSessionIdByAgentTargetId
|
|
);
|
|
return {
|
|
...(agentTargetId ? { agentTargetId } : {}),
|
|
conversationRailCollapsed: state.conversationRailCollapsed === true,
|
|
conversationRailWidthPx: normalizeOptionalPositiveNumber(
|
|
state.conversationRailWidthPx
|
|
),
|
|
lastActiveAgentSessionId: state.lastActiveAgentSessionId ?? null,
|
|
...(lastActiveAgentSessionIdByAgentTargetId
|
|
? { lastActiveAgentSessionIdByAgentTargetId }
|
|
: {})
|
|
};
|
|
}
|
|
|
|
export function areAgentGuiWorkbenchStatesEqual(
|
|
left: AgentGuiWorkbenchState,
|
|
right: AgentGuiWorkbenchState
|
|
): boolean {
|
|
return (
|
|
(left.agentTargetId ?? null) === (right.agentTargetId ?? null) &&
|
|
left.conversationRailCollapsed === right.conversationRailCollapsed &&
|
|
left.conversationRailWidthPx === right.conversationRailWidthPx &&
|
|
left.lastActiveAgentSessionId === right.lastActiveAgentSessionId &&
|
|
stringRecordsEqual(
|
|
left.lastActiveAgentSessionIdByAgentTargetId,
|
|
right.lastActiveAgentSessionIdByAgentTargetId
|
|
)
|
|
);
|
|
}
|
|
|
|
export function normalizeAgentGuiWorkbenchNodeState(
|
|
state: unknown,
|
|
fallbackProvider: AgentGuiWorkbenchProvider = "codex"
|
|
): AgentGuiWorkbenchNodeState {
|
|
const persistedState = isRecord(state) ? state : {};
|
|
const provider =
|
|
persistedState.provider === undefined || persistedState.provider === null
|
|
? fallbackProvider
|
|
: normalizeAgentGuiWorkbenchProvider(persistedState.provider);
|
|
return {
|
|
...createDefaultAgentGuiWorkbenchNodeState(provider),
|
|
agentTargetId: normalizeOptionalNonEmptyString(
|
|
persistedState.agentTargetId
|
|
),
|
|
composerOverrides: normalizeAgentGuiWorkbenchComposerOverrides(
|
|
persistedState.composerOverrides
|
|
),
|
|
composerOverridesByAgentTargetId:
|
|
normalizeAgentGuiWorkbenchComposerOverridesByAgentTargetId(
|
|
persistedState.composerOverridesByAgentTargetId
|
|
),
|
|
composerOverridesByProvider:
|
|
normalizeAgentGuiWorkbenchComposerOverridesByProvider(
|
|
persistedState.composerOverridesByProvider
|
|
),
|
|
conversationCount: normalizeOptionalNonNegativeNumber(
|
|
persistedState.conversationCount
|
|
),
|
|
conversationRailCollapsed:
|
|
persistedState.conversationRailCollapsed === true,
|
|
conversationRailWidthPx: normalizeOptionalPositiveNumber(
|
|
persistedState.conversationRailWidthPx
|
|
),
|
|
lastActiveAgentSessionId:
|
|
typeof persistedState.lastActiveAgentSessionId === "string"
|
|
? persistedState.lastActiveAgentSessionId
|
|
: null,
|
|
lastActiveAgentSessionIdByAgentTargetId:
|
|
normalizeAgentGuiWorkbenchLastActiveSessionByAgentTargetId(
|
|
persistedState.lastActiveAgentSessionIdByAgentTargetId
|
|
),
|
|
provider
|
|
};
|
|
}
|
|
|
|
export function areAgentGuiWorkbenchNodeStatesEqual(
|
|
left: AgentGuiWorkbenchNodeState,
|
|
right: AgentGuiWorkbenchNodeState
|
|
): boolean {
|
|
return (
|
|
(left.agentTargetId ?? null) === (right.agentTargetId ?? null) &&
|
|
composerOverridesEqual(left.composerOverrides, right.composerOverrides) &&
|
|
composerOverridesByAgentTargetIdEqual(
|
|
left.composerOverridesByAgentTargetId,
|
|
right.composerOverridesByAgentTargetId
|
|
) &&
|
|
composerOverridesByProviderEqual(
|
|
left.composerOverridesByProvider,
|
|
right.composerOverridesByProvider
|
|
) &&
|
|
left.conversationCount === right.conversationCount &&
|
|
left.conversationRailCollapsed === right.conversationRailCollapsed &&
|
|
left.conversationRailWidthPx === right.conversationRailWidthPx &&
|
|
left.lastActiveAgentSessionId === right.lastActiveAgentSessionId &&
|
|
stringRecordsEqual(
|
|
left.lastActiveAgentSessionIdByAgentTargetId,
|
|
right.lastActiveAgentSessionIdByAgentTargetId
|
|
) &&
|
|
left.provider === right.provider &&
|
|
(left.agentTargetId ?? null) === (right.agentTargetId ?? null)
|
|
);
|
|
}
|
|
|
|
export function createAgentGuiWorkbenchNodeStateSource(input: {
|
|
typeId?: string;
|
|
workspaceId: string;
|
|
}): {
|
|
externalStateSource: WorkbenchHostExternalStateSource<
|
|
AgentGuiWorkbenchState | null,
|
|
AgentGuiWorkbenchWorkspaceState
|
|
>;
|
|
readNodeState: (
|
|
request: AgentGuiWorkbenchStateLookupRequest
|
|
) => AgentGuiWorkbenchState | null;
|
|
writeNodeState: (
|
|
request: AgentGuiWorkbenchStateLookupRequest & {
|
|
state: AgentGuiWorkbenchState;
|
|
}
|
|
) => void;
|
|
/**
|
|
* Returns the launch instanceId of an open node currently showing the given
|
|
* agent session, or null when none is found. Used to focus an existing
|
|
* conversation instead of launching a duplicate node.
|
|
*/
|
|
findInstanceIdByAgentSessionId: (agentSessionId: string) => string | null;
|
|
} {
|
|
const typeId = input.typeId ?? "agent-gui";
|
|
const nodeStateByKey = new Map<string, AgentGuiWorkbenchState>();
|
|
// Tracks the launch instanceId behind each state key so a node can be located
|
|
// by the session it is currently showing, even when its key is node-scoped.
|
|
const instanceIdByKey = new Map<string, string>();
|
|
const listeners = new Set<() => void>();
|
|
const workspaceState = { workspaceId: input.workspaceId };
|
|
|
|
const lookupState = (request: AgentGuiWorkbenchStateLookupRequest) => {
|
|
const nodeState = request.nodeId
|
|
? nodeStateByKey.get(agentGuiWorkbenchNodeStateKey(request))
|
|
: null;
|
|
const state =
|
|
nodeState ??
|
|
nodeStateByKey.get(agentGuiWorkbenchInstanceStateKey(request));
|
|
return state ?? null;
|
|
};
|
|
|
|
const notify = () => {
|
|
for (const listener of listeners) {
|
|
listener();
|
|
}
|
|
};
|
|
|
|
return {
|
|
externalStateSource: {
|
|
getNodeState(request) {
|
|
if (request.typeId !== typeId) {
|
|
return null;
|
|
}
|
|
return lookupState(request);
|
|
},
|
|
getSnapshotNodeState(request) {
|
|
if (request.typeId !== typeId) {
|
|
return null;
|
|
}
|
|
return lookupState(request);
|
|
},
|
|
getWorkspaceState() {
|
|
return workspaceState;
|
|
},
|
|
subscribeNodeState(request, listener) {
|
|
if (request.typeId !== typeId) {
|
|
return () => {};
|
|
}
|
|
listeners.add(listener);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
};
|
|
}
|
|
},
|
|
readNodeState(request) {
|
|
if (request.typeId !== typeId) {
|
|
return null;
|
|
}
|
|
return lookupState(request);
|
|
},
|
|
findInstanceIdByAgentSessionId(agentSessionId) {
|
|
const target = agentSessionId.trim();
|
|
if (!target) {
|
|
return null;
|
|
}
|
|
for (const [key, state] of nodeStateByKey) {
|
|
if (state.lastActiveAgentSessionId?.trim() === target) {
|
|
return instanceIdByKey.get(key) ?? null;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
writeNodeState(request) {
|
|
if (request.typeId !== typeId) {
|
|
return;
|
|
}
|
|
const key = agentGuiWorkbenchNodeStateKey(request);
|
|
instanceIdByKey.set(key, request.instanceId);
|
|
const previous = nodeStateByKey.get(key);
|
|
let clearedInstanceSeed = false;
|
|
if (request.nodeId) {
|
|
const instanceKey = agentGuiWorkbenchInstanceStateKey(request);
|
|
instanceIdByKey.delete(instanceKey);
|
|
clearedInstanceSeed = nodeStateByKey.delete(instanceKey);
|
|
}
|
|
const next = {
|
|
...normalizeAgentGuiWorkbenchState(request.state)
|
|
};
|
|
if (
|
|
!clearedInstanceSeed &&
|
|
previous &&
|
|
areAgentGuiWorkbenchMemoryStatesEqual(previous, next)
|
|
) {
|
|
return;
|
|
}
|
|
nodeStateByKey.set(key, next);
|
|
notify();
|
|
}
|
|
};
|
|
}
|
|
|
|
function areAgentGuiWorkbenchMemoryStatesEqual(
|
|
left: AgentGuiWorkbenchState,
|
|
right: AgentGuiWorkbenchState
|
|
): boolean {
|
|
return areAgentGuiWorkbenchStatesEqual(left, right);
|
|
}
|
|
|
|
function agentGuiWorkbenchNodeStateKey(
|
|
request: Pick<WorkbenchHostExternalStateLookupInput, "instanceId"> &
|
|
Partial<Pick<WorkbenchHostExternalStateLookupInput, "nodeId">>
|
|
): string {
|
|
return request.nodeId
|
|
? `node:${request.nodeId}`
|
|
: agentGuiWorkbenchInstanceStateKey(request);
|
|
}
|
|
|
|
function agentGuiWorkbenchInstanceStateKey(
|
|
request: Pick<WorkbenchHostExternalStateLookupInput, "instanceId">
|
|
): string {
|
|
return `instance:${request.instanceId}`;
|
|
}
|
|
|
|
function createDefaultAgentGuiWorkbenchState(): AgentGuiWorkbenchState {
|
|
return {
|
|
conversationRailCollapsed: false,
|
|
conversationRailWidthPx: null,
|
|
lastActiveAgentSessionId: null
|
|
};
|
|
}
|
|
|
|
function normalizeOptionalNonEmptyString(value: unknown): string | null {
|
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
}
|
|
|
|
function normalizeAgentGuiWorkbenchComposerOverrides(
|
|
value: unknown
|
|
): AgentGuiWorkbenchComposerOverrides | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
const composerOverrides: AgentGuiWorkbenchComposerOverrides = {};
|
|
if (typeof value.model === "string" && value.model.trim()) {
|
|
composerOverrides.model = value.model.trim();
|
|
}
|
|
if (
|
|
typeof value.reasoningEffort === "string" &&
|
|
value.reasoningEffort.trim()
|
|
) {
|
|
composerOverrides.reasoningEffort = value.reasoningEffort.trim();
|
|
}
|
|
if (
|
|
typeof value.permissionModeId === "string" &&
|
|
value.permissionModeId.trim()
|
|
) {
|
|
composerOverrides.permissionModeId = value.permissionModeId.trim();
|
|
}
|
|
if (typeof value.planMode === "boolean") {
|
|
composerOverrides.planMode = value.planMode;
|
|
}
|
|
return Object.keys(composerOverrides).length > 0 ? composerOverrides : null;
|
|
}
|
|
|
|
function normalizeAgentGuiWorkbenchComposerOverridesByProvider(
|
|
value: unknown
|
|
): AgentGuiWorkbenchComposerOverridesByProvider | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
const result: AgentGuiWorkbenchComposerOverridesByProvider = {};
|
|
for (const provider of agentGuiWorkbenchProviders) {
|
|
const overrides = normalizeAgentGuiWorkbenchComposerOverrides(
|
|
value[provider]
|
|
);
|
|
if (overrides) {
|
|
result[provider] = overrides;
|
|
}
|
|
}
|
|
return Object.keys(result).length > 0 ? result : null;
|
|
}
|
|
|
|
function normalizeAgentGuiWorkbenchComposerOverridesByAgentTargetId(
|
|
value: unknown
|
|
): AgentGuiWorkbenchComposerOverridesByAgentTargetId | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
const result: AgentGuiWorkbenchComposerOverridesByAgentTargetId = {};
|
|
for (const [rawAgentTargetId, rawOverrides] of Object.entries(value)) {
|
|
const agentTargetId = normalizeOptionalNonEmptyString(rawAgentTargetId);
|
|
if (!agentTargetId) {
|
|
continue;
|
|
}
|
|
const overrides = normalizeAgentGuiWorkbenchComposerOverrides(rawOverrides);
|
|
if (overrides) {
|
|
result[agentTargetId] = overrides;
|
|
}
|
|
}
|
|
return Object.keys(result).length > 0 ? result : null;
|
|
}
|
|
|
|
function normalizeAgentGuiWorkbenchLastActiveSessionByAgentTargetId(
|
|
value: unknown
|
|
): Record<string, string> | null {
|
|
if (!isRecord(value)) {
|
|
return null;
|
|
}
|
|
const result: Record<string, string> = {};
|
|
for (const [rawAgentTargetId, rawAgentSessionId] of Object.entries(value)) {
|
|
const agentTargetId = normalizeOptionalNonEmptyString(rawAgentTargetId);
|
|
const agentSessionId = normalizeOptionalNonEmptyString(rawAgentSessionId);
|
|
if (agentTargetId && agentSessionId) {
|
|
result[agentTargetId] = agentSessionId;
|
|
}
|
|
}
|
|
return Object.keys(result).length > 0 ? result : null;
|
|
}
|
|
|
|
function normalizeOptionalPositiveNumber(value: unknown): number | null {
|
|
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
? Math.round(value)
|
|
: null;
|
|
}
|
|
|
|
function normalizeOptionalNonNegativeNumber(value: unknown): number | null {
|
|
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
? Math.round(value)
|
|
: null;
|
|
}
|
|
|
|
function composerOverridesEqual(
|
|
left: AgentGuiWorkbenchComposerOverrides | null | undefined,
|
|
right: AgentGuiWorkbenchComposerOverrides | null | undefined
|
|
): boolean {
|
|
return (
|
|
(left?.model ?? null) === (right?.model ?? null) &&
|
|
(left?.permissionModeId ?? null) === (right?.permissionModeId ?? null) &&
|
|
(left?.planMode ?? null) === (right?.planMode ?? null) &&
|
|
(left?.reasoningEffort ?? null) === (right?.reasoningEffort ?? null)
|
|
);
|
|
}
|
|
|
|
function composerOverridesByProviderEqual(
|
|
left: AgentGuiWorkbenchComposerOverridesByProvider | null | undefined,
|
|
right: AgentGuiWorkbenchComposerOverridesByProvider | null | undefined
|
|
): boolean {
|
|
for (const provider of agentGuiWorkbenchProviders) {
|
|
if (!composerOverridesEqual(left?.[provider], right?.[provider])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function composerOverridesByAgentTargetIdEqual(
|
|
left: AgentGuiWorkbenchComposerOverridesByAgentTargetId | null | undefined,
|
|
right: AgentGuiWorkbenchComposerOverridesByAgentTargetId | null | undefined
|
|
): boolean {
|
|
const leftKeys = Object.keys(left ?? {}).sort();
|
|
const rightKeys = Object.keys(right ?? {}).sort();
|
|
if (leftKeys.length !== rightKeys.length) {
|
|
return false;
|
|
}
|
|
return leftKeys.every(
|
|
(key, index) =>
|
|
key === rightKeys[index] &&
|
|
composerOverridesEqual(left?.[key], right?.[key])
|
|
);
|
|
}
|
|
|
|
function stringRecordsEqual(
|
|
left: Record<string, string> | null | undefined,
|
|
right: Record<string, string> | null | undefined
|
|
): boolean {
|
|
const leftKeys = Object.keys(left ?? {}).sort();
|
|
const rightKeys = Object.keys(right ?? {}).sort();
|
|
return (
|
|
leftKeys.length === rightKeys.length &&
|
|
leftKeys.every(
|
|
(key, index) => key === rightKeys[index] && left?.[key] === right?.[key]
|
|
)
|
|
);
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null;
|
|
}
|