feat: update Makelore modules and conversations
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-07-31 10:08:41 +08:00
parent b8ca3f8eea
commit 80e8386fa6
175 changed files with 8036 additions and 10581 deletions

View File

@@ -0,0 +1,521 @@
import type {
OpencodeNativePartKind,
OpencodeNativeToolState,
OpencodeNormalizedSessionMessage,
OpencodeNormalizedSessionPart,
OpencodeNormalizedToolState,
OpencodeSessionExecutionState,
OpencodeSessionTranscriptState,
} from '@/types/opencode';
export interface OpenCodeEventEnvelope {
type: string;
payload: unknown;
}
const PART_TYPES = new Set<OpencodeNativePartKind>([
'text',
'reasoning',
'tool',
'step-start',
'step-finish',
'retry',
'subtask',
'agent',
'patch',
'file',
'snapshot',
'compaction',
]);
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function nonEmptyString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value : undefined;
}
function getSessionId(value: unknown): string | undefined {
const record = asRecord(value);
if (!record) return undefined;
return nonEmptyString(record?.sessionID)
?? nonEmptyString(record?.sessionId)
?? getSessionId(record?.part)
?? getSessionId(record?.info)
?? getSessionId(record?.message);
}
function getDeltaText(value: unknown): string {
if (typeof value === 'string') return value;
const record = asRecord(value);
if (!record) return '';
for (const key of ['text', 'reasoning', 'reasoning_content', 'reasoning_text']) {
const text = nonEmptyString(record[key]);
if (text) return text;
}
return '';
}
function getMessageId(value: unknown): string | undefined {
const record = asRecord(value);
return nonEmptyString(record?.messageID)
?? nonEmptyString(record?.messageId)
?? nonEmptyString(asRecord(record?.info)?.id)
?? nonEmptyString(asRecord(record?.message)?.id)
?? nonEmptyString(asRecord(record?.part)?.messageID);
}
function getPartId(value: unknown): string | undefined {
const record = asRecord(value);
return nonEmptyString(record?.partID)
?? nonEmptyString(record?.partId)
?? nonEmptyString(asRecord(record?.part)?.id)
?? nonEmptyString(asRecord(record?.part)?.partID);
}
function toMilliseconds(value: unknown): number | undefined {
if (typeof value !== 'number' || !Number.isFinite(value)) return undefined;
return Math.abs(value) < 1_000_000_000_000 ? value * 1_000 : value;
}
function getTime(value: unknown): { start?: number; end?: number } | undefined {
const record = asRecord(value);
if (!record) return undefined;
const start = toMilliseconds(record.start);
const end = toMilliseconds(record.end);
return start === undefined && end === undefined ? undefined : { start, end };
}
function stringifyDetail(value: unknown, limit = 8_000): string | undefined {
if (value === undefined || value === null) return undefined;
const text = typeof value === 'string' ? value : (() => {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
})();
const trimmed = text.trim();
if (!trimmed) return undefined;
return trimmed.length > limit ? `${trimmed.slice(0, limit)}\n…` : trimmed;
}
function normalizeToolState(value: unknown): OpencodeNormalizedToolState | undefined {
const record = asRecord(value);
if (!record) return undefined;
const rawStatus = nonEmptyString(record.status);
const status: OpencodeNativeToolState = rawStatus === 'pending'
|| rawStatus === 'running'
|| rawStatus === 'completed'
|| rawStatus === 'error'
? rawStatus
: 'running';
const time = getTime(record.time);
return {
status,
...(record.input !== undefined ? { input: record.input } : {}),
...(record.output !== undefined ? { output: record.output } : {}),
...(record.error !== undefined ? { error: record.error } : {}),
...(asRecord(record.metadata) ? { metadata: asRecord(record.metadata)! } : {}),
...(time ? { time } : {}),
};
}
function normalizePartType(rawType: string): OpencodeNativePartKind {
return PART_TYPES.has(rawType as OpencodeNativePartKind)
? rawType as OpencodeNativePartKind
: 'unknown';
}
function pickPartRecord(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
return asRecord(record?.part) ?? record;
}
function pickMessageInfo(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
return asRecord(record?.info) ?? asRecord(record?.message) ?? record;
}
function normalizeMessageRole(value: unknown): string {
const role = nonEmptyString(value);
return role === 'user' || role === 'assistant' || role === 'system' || role === 'tool'
? role
: 'assistant';
}
function normalizePart(
input: unknown,
sessionID: string,
messageID: string,
fallbackId?: string,
): OpencodeNormalizedSessionPart | null {
const record = pickPartRecord(input);
if (!record) return null;
const id = nonEmptyString(record.id) ?? nonEmptyString(record.partID) ?? fallbackId;
const rawType = nonEmptyString(record.type) ?? 'unknown';
if (!id) return null;
const type = normalizePartType(rawType);
const state = normalizeToolState(record.state);
const time = getTime(record.time) ?? state?.time;
const text = typeof record.text === 'string'
? record.text
: typeof record.reasoning === 'string'
? record.reasoning
: typeof record.reasoning_content === 'string'
? record.reasoning_content
: typeof record.reasoning_text === 'string'
? record.reasoning_text
: undefined;
const detail = stringifyDetail(
type === 'tool'
? state?.output ?? state?.error ?? state?.input
: type === 'retry'
? record.message ?? record.error ?? record
: type === 'step-finish'
? {
reason: record.reason,
cost: record.cost,
tokens: record.tokens,
}
: type === 'text' || type === 'reasoning'
? undefined
: record,
);
return {
id,
sessionID,
messageID,
type,
rawType,
...(text !== undefined ? { text } : {}),
...(record.synthetic === true ? { synthetic: true } : {}),
...(typeof record.tool === 'string' ? { tool: record.tool } : {}),
...(typeof record.callID === 'string' ? { callID: record.callID } : {}),
...(state ? { state } : {}),
...(detail ? { detail } : {}),
...(typeof record.parentID === 'string' ? { parentID: record.parentID } : {}),
...(typeof record.sessionID === 'string' ? { childSessionID: record.sessionID } : {}),
...(time ? { time } : {}),
raw: record,
};
}
function createEmptyState(sessionID: string): OpencodeSessionTranscriptState {
return {
sessionID,
status: 'idle',
messagesById: {},
messageOrder: [],
partsById: {},
partOrderByMessageId: {},
};
}
function ensureMessage(
state: OpencodeSessionTranscriptState,
input: unknown,
sessionID: string,
messageID: string,
): OpencodeSessionTranscriptState {
const info = pickMessageInfo(input) ?? {};
const existing = state.messagesById[messageID];
const next: OpencodeNormalizedSessionMessage = {
id: messageID,
sessionID,
role: normalizeMessageRole(info.role),
...(toMilliseconds(asRecord(info.time)?.created ?? info.createdAt) !== undefined
? { createdAt: toMilliseconds(asRecord(info.time)?.created ?? info.createdAt) }
: existing?.createdAt !== undefined ? { createdAt: existing.createdAt } : {}),
...(toMilliseconds(asRecord(info.time)?.completed ?? info.completedAt) !== undefined
? { completedAt: toMilliseconds(asRecord(info.time)?.completed ?? info.completedAt) }
: existing?.completedAt !== undefined ? { completedAt: existing.completedAt } : {}),
partIDs: existing?.partIDs ?? [],
};
return {
...state,
messagesById: { ...state.messagesById, [messageID]: next },
messageOrder: state.messageOrder.includes(messageID)
? state.messageOrder
: [...state.messageOrder, messageID],
partOrderByMessageId: state.partOrderByMessageId[messageID]
? state.partOrderByMessageId
: { ...state.partOrderByMessageId, [messageID]: [] },
};
}
function upsertPart(
state: OpencodeSessionTranscriptState,
part: OpencodeNormalizedSessionPart,
): OpencodeSessionTranscriptState {
const message = state.messagesById[part.messageID];
const currentOrder = state.partOrderByMessageId[part.messageID] ?? [];
return {
...state,
partsById: {
...state.partsById,
[part.id]: part,
},
messagesById: message
? {
...state.messagesById,
[part.messageID]: {
...message,
partIDs: message.partIDs.includes(part.id)
? message.partIDs
: [...message.partIDs, part.id],
},
}
: state.messagesById,
partOrderByMessageId: {
...state.partOrderByMessageId,
[part.messageID]: currentOrder.includes(part.id)
? currentOrder
: [...currentOrder, part.id],
},
};
}
function removePart(
state: OpencodeSessionTranscriptState,
partID: string,
): OpencodeSessionTranscriptState {
const part = state.partsById[partID];
if (!part) return state;
const message = state.messagesById[part.messageID];
const nextParts = { ...state.partsById };
delete nextParts[partID];
return {
...state,
partsById: nextParts,
messagesById: message
? {
...state.messagesById,
[part.messageID]: {
...message,
partIDs: message.partIDs.filter((id) => id !== partID),
},
}
: state.messagesById,
partOrderByMessageId: {
...state.partOrderByMessageId,
[part.messageID]: (state.partOrderByMessageId[part.messageID] ?? [])
.filter((id) => id !== partID),
},
};
}
function eventKey(event: OpenCodeEventEnvelope, sessionID?: string): string | undefined {
const record = asRecord(event.payload);
const sequence = typeof record?.sequence === 'number' && Number.isFinite(record.sequence)
? String(record.sequence)
: nonEmptyString(record?.sequence);
const id = nonEmptyString(record?.eventID)
?? nonEmptyString(record?.eventId)
?? sequence
// Message/Part ids identify the resource, not the event. They must not be
// treated as a delta event id or every later chunk would be discarded.
?? (event.type === 'message.part.delta' ? undefined : nonEmptyString(record?.id));
return id ? `${sessionID ?? getSessionId(event.payload) ?? ''}:${event.type}:${id}` : undefined;
}
function rememberEventKey(
state: OpencodeSessionTranscriptState,
key: string | undefined,
): OpencodeSessionTranscriptState {
if (!key) return state;
const previous = state.seenEventKeys ?? [];
if (previous.includes(key)) return state;
return {
...state,
lastEventKey: key,
seenEventKeys: [...previous, key].slice(-512),
};
}
function applyDelta(
state: OpencodeSessionTranscriptState,
event: OpenCodeEventEnvelope,
): OpencodeSessionTranscriptState {
const record = asRecord(event.payload);
const delta = getDeltaText(record?.delta);
const sessionID = getSessionId(event.payload) ?? state.sessionID;
const messageID = getMessageId(event.payload);
const partID = getPartId(event.payload);
if (!delta || !messageID || !partID) return state;
const field = nonEmptyString(record?.field) ?? nonEmptyString(asRecord(record?.part)?.field);
const signature = `${messageID}:${partID}:${field ?? 'text'}:${delta}`;
// Without a runtime event identity, only suppress an immediately repeated
// signature. With an identity, the reducer-level event cache is the source
// of truth and equal consecutive chunks remain valid output.
if (!eventKey(event, sessionID) && state.lastDeltaSignature === signature) return state;
const existing = state.partsById[partID];
const isReasoning = field === 'reasoning'
|| field === 'thinking'
|| asRecord(record?.part)?.type === 'reasoning'
|| existing?.type === 'reasoning';
const nextPart: OpencodeNormalizedSessionPart = {
...(existing ?? {
id: partID,
sessionID,
messageID,
type: isReasoning ? 'reasoning' : 'text',
rawType: isReasoning ? 'reasoning' : 'text',
}),
text: `${existing?.text ?? ''}${delta}`,
};
return {
...upsertPart(ensureMessage(state, record?.part ?? record, sessionID, messageID), nextPart),
lastDeltaSignature: signature,
};
}
export function normalizeOpenCodeMessage(
input: unknown,
fallbackSessionID?: string,
): OpencodeNormalizedSessionMessage | null {
const record = asRecord(input);
if (!record) return null;
const info = pickMessageInfo(record);
const sessionID = getSessionId(record) ?? fallbackSessionID;
const id = nonEmptyString(info?.id) ?? nonEmptyString(record.id);
if (!sessionID || !id) return null;
const parts = Array.isArray(record.parts) ? record.parts : [];
const normalizedParts = parts
.map((part) => normalizePart(part, sessionID, id))
.filter((part): part is OpencodeNormalizedSessionPart => Boolean(part));
return {
id,
sessionID,
role: normalizeMessageRole(info?.role),
...(toMilliseconds(asRecord(info?.time)?.created ?? info?.createdAt) !== undefined
? { createdAt: toMilliseconds(asRecord(info?.time)?.created ?? info?.createdAt) }
: {}),
...(toMilliseconds(asRecord(info?.time)?.completed ?? info?.completedAt) !== undefined
? { completedAt: toMilliseconds(asRecord(info?.time)?.completed ?? info?.completedAt) }
: {}),
partIDs: normalizedParts.map((part) => part.id),
};
}
export function hydrateOpenCodeSession(
sessionID: string,
messages: unknown,
status: OpencodeSessionExecutionState = 'idle',
): OpencodeSessionTranscriptState {
const state = createEmptyState(sessionID);
if (!Array.isArray(messages)) return { ...state, status };
let next = state;
for (const messageInput of messages) {
const message = normalizeOpenCodeMessage(messageInput, sessionID);
if (!message) continue;
next = ensureMessage(next, messageInput, sessionID, message.id);
const record = asRecord(messageInput);
const parts = Array.isArray(record?.parts) ? record.parts : [];
for (const partInput of parts) {
const part = normalizePart(partInput, sessionID, message.id);
if (part) next = upsertPart(next, part);
}
}
return { ...next, status };
}
/** Rebuild the canonical transcript from the server's message/Part snapshot. */
export function hydrateSessionSnapshot(
sessionID: string,
messages: unknown,
status: OpencodeSessionExecutionState = 'idle',
): OpencodeSessionTranscriptState {
return hydrateOpenCodeSession(sessionID, messages, status);
}
export function reduceOpenCodeEvent(
current: OpencodeSessionTranscriptState | undefined,
event: OpenCodeEventEnvelope,
): OpencodeSessionTranscriptState | undefined {
const sessionID = getSessionId(event.payload) ?? current?.sessionID;
if (!sessionID) return current;
if (current && current.sessionID !== sessionID) return current;
let state = current ?? createEmptyState(sessionID);
const key = eventKey(event, sessionID);
if (key && state.seenEventKeys?.includes(key)) return state;
state = rememberEventKey(state, key);
if (event.type === 'message.part.delta') {
return applyDelta(state, event);
}
if (event.type === 'message.part.removed') {
const partID = getPartId(event.payload);
return partID ? removePart(state, partID) : state;
}
if (event.type === 'message.updated') {
const messageInput = asRecord(event.payload)?.message ?? event.payload;
const message = normalizeOpenCodeMessage(messageInput, sessionID);
if (!message) return state;
let next = ensureMessage(state, messageInput, sessionID, message.id);
const parts = Array.isArray(asRecord(messageInput)?.parts)
? asRecord(messageInput)?.parts as unknown[]
: [];
for (const partInput of parts) {
const part = normalizePart(partInput, sessionID, message.id);
if (part) next = upsertPart(next, part);
}
return next;
}
if (event.type === 'message.part.updated') {
const record = asRecord(event.payload);
const part = normalizePart(record?.part ?? event.payload, sessionID, getMessageId(event.payload) ?? '');
const messageID = part?.messageID || getMessageId(event.payload);
if (!part || !messageID) return state;
const withMessage = ensureMessage(state, record?.message ?? record?.part ?? record, sessionID, messageID);
return upsertPart(withMessage, { ...part, messageID });
}
if (event.type === 'session.status') {
const type = nonEmptyString(asRecord(event.payload)?.status && asRecord(asRecord(event.payload)?.status)?.type)
?? nonEmptyString(asRecord(event.payload)?.type);
return {
...state,
status: type === 'busy' || type === 'retry' || type === 'idle' ? type : state.status,
};
}
if (event.type === 'session.idle' || event.type === 'session.compacted') {
return { ...state, status: 'idle' };
}
if (event.type === 'session.error') {
const payload = asRecord(event.payload);
const error = payload?.error;
return {
...state,
status: 'idle',
lastError: stringifyDetail(error ?? payload?.message ?? payload?.errorMessage, 2_000),
};
}
return state;
}
export function getOrderedSessionParts(
state: OpencodeSessionTranscriptState | undefined,
messageID: string,
): OpencodeNormalizedSessionPart[] {
if (!state) return [];
return (state.partOrderByMessageId[messageID] ?? [])
.map((partID) => state.partsById[partID])
.filter((part): part is OpencodeNormalizedSessionPart => Boolean(part));
}
export function getSessionPartCount(state: OpencodeSessionTranscriptState | undefined): number {
return state ? Object.keys(state.partsById).length : 0;
}