blob: e68a1a74f35776186615e1703bc840e8162e1391 [file]
/**
* What a `sessions:create` request resolves to.
*
* #1433: these fields used to be derived in two places. `sessions:create`
* took them from the renderer, and a second IPC (`quickChat:start`, built for
* the first-run Quick Chat panel) derived them from a product `mode`. The
* panel is gone, and what remained of the second IPC was a duplicate of the
* first — same readiness gate, same connection resolution, same
* `emitSessionsChanged('created')` — so only the derivation survived.
*
* It lives here as a pure function rather than inside the handler because the
* handler is an `ipcMain.handle` closure no test can call. Every invariant
* below — the mode's boundary outranking both the renderer's request and the
* configured default, the refusal of a directly-requested `explore`, the
* settings-backed fallback that must never reject — would otherwise only be
* assertable by regex over the handler's source.
*/
import type {
AppSettings,
CollaborationMode,
OrchestrationMode,
PermissionMode,
SessionStartMode,
} from '@maka/core';
import {
DEEP_RESEARCH_SESSION_LABEL,
DEFAULT_SESSION_NAME,
isChatDefaultPermissionMode,
isCollaborationMode,
isOrchestrationMode,
} from '@maka/core';
import { resolveDefaultPermissionMode } from './permission-mode-default.js';
/**
* The renderer names a product intent; main stays the sole authority on what
* that intent means. A closed mapping rather than a passthrough, so nothing
* the renderer sends can reach `explore` except the mode that earns it.
*/
interface SessionModeSeed {
permissionMode: PermissionMode;
name: string;
labels: string[];
}
/**
* Closed over `SessionStartMode`, not over an `if`: adding a member without a
* seed is a compile error here, which is the only place that catches it. A
* missing arm would otherwise degrade silently into "no mode at all" — a plain
* session at the configured default, with the new mode's name, label and
* boundary all quietly absent.
*/
const SESSION_MODE_SEEDS = {
deep_research: {
// Deep Research is a read-only exploration boundary, so it overrides the
// user's configured default rather than seeding from it.
permissionMode: 'explore',
name: 'Deep Research',
labels: [DEEP_RESEARCH_SESSION_LABEL],
},
} satisfies Record<SessionStartMode, SessionModeSeed>;
/**
* `unknown`, because this is an IPC boundary and the renderer's type is a
* promise, not a guarantee. An unrecognized value confers nothing — it is not
* a mode — and the caller falls through to an ordinary session, which is the
* same session it would have got by not naming one.
*/
function sessionModeSeed(mode: unknown): SessionModeSeed | undefined {
return typeof mode === 'string' && Object.hasOwn(SESSION_MODE_SEEDS, mode)
? SESSION_MODE_SEEDS[mode as SessionStartMode]
: undefined;
}
/**
* The part of a create request this module resolves. Everything else — cwd,
* backend, connection, model, thinking level — stays the handler's.
*/
export interface CreateSessionRequest {
mode?: SessionStartMode;
permissionMode?: PermissionMode;
collaborationMode?: CollaborationMode;
orchestrationMode?: OrchestrationMode;
name?: string;
labels?: string[];
}
export interface ResolvedCreateSessionInput {
permissionMode: PermissionMode;
collaborationMode: CollaborationMode;
orchestrationMode: OrchestrationMode;
name: string;
labels: string[] | undefined;
}
export async function resolveCreateSessionInput(
input: CreateSessionRequest | undefined,
deps: { readSettings: () => Promise<AppSettings> },
): Promise<ResolvedCreateSessionInput> {
const modeSeed = sessionModeSeed(input?.mode);
const collaborationMode = input?.collaborationMode ?? 'agent';
if (!isCollaborationMode(collaborationMode)) {
throw new TypeError('Invalid collaboration mode.');
}
const orchestrationMode = input?.orchestrationMode ?? 'default';
if (!isOrchestrationMode(orchestrationMode)) {
throw new TypeError('Invalid orchestration mode.');
}
// `explore` is a boundary a mode confers, never one a caller may open a
// session at — core already spells that out as `ChatDefaultPermissionMode`
// (the modes a user can pick). Refusing it here is what makes the seed the
// only way in; `sessions:setPermissionMode` stays the separate, deliberate
// path for moving an existing session (the quote companion uses it).
if (input?.permissionMode !== undefined && !isChatDefaultPermissionMode(input.permissionMode)) {
throw new TypeError('Invalid permission mode.');
}
return {
permissionMode:
modeSeed?.permissionMode ??
input?.permissionMode ??
(await resolveDefaultPermissionMode(deps.readSettings)),
collaborationMode,
orchestrationMode,
name: modeSeed?.name ?? input?.name ?? DEFAULT_SESSION_NAME,
// Merged, not replaced: a mode adds a label, it does not own the set. No
// caller sends both today, and silently dropping the caller's would be the
// surprising half of that.
labels: modeSeed ? [...new Set([...(input?.labels ?? []), ...modeSeed.labels])] : input?.labels,
};
}