feat: support multiple event proposals in single AI response

- Change proposedChange to proposedChanges array in ChatMessage type
- Add unique id and individual respondedAction to each ProposedEventChange
- Implement arrow navigation UI for multiple proposals with "Event X von Y" counter
- Add updateProposalResponse() method for per-proposal confirm/reject tracking
- GPTAdapter now collects multiple tool call results into proposals array
- Add RRULE documentation to system prompt (separate events for different times)
- Fix RRULE parsing to strip RRULE: prefix if present
- Add log summarization for large args (conversationHistory, existingEvents)
- Keep proposedChanges logged in full for debugging AI issues
This commit is contained in:
2026-01-10 23:30:32 +01:00
parent 8efe6c304e
commit e6b9dd9d34
18 changed files with 533 additions and 158 deletions

View File

@@ -1,5 +1,57 @@
import { createLogger } from "./logger";
/**
* Summarize args for logging to avoid huge log entries.
* - Arrays: show length only
* - Long strings: truncate
* - Objects with conversationHistory/existingEvents: summarize
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function summarizeArgs(args: any[]): any[] {
return args.map((arg) => summarizeValue(arg));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function summarizeValue(value: any, depth = 0): any {
if (depth > 2) return "[...]";
if (value === null || value === undefined) return value;
if (Array.isArray(value)) {
return `[Array(${value.length})]`;
}
if (typeof value === "string" && value.length > 100) {
return value.substring(0, 100) + "...";
}
if (typeof value === "object") {
// Summarize known large fields
const summarized: Record<string, unknown> = {};
for (const [key, val] of Object.entries(value)) {
if (key === "conversationHistory" && Array.isArray(val)) {
summarized[key] = `[${val.length} messages]`;
} else if (key === "existingEvents" && Array.isArray(val)) {
summarized[key] = `[${val.length} events]`;
} else if (key === "proposedChanges" && Array.isArray(val)) {
// Log full proposedChanges for debugging AI issues
summarized[key] = val.map((p) => summarizeValue(p, depth + 1));
} else if (Array.isArray(val)) {
summarized[key] = `[Array(${val.length})]`;
} else if (typeof val === "object" && val !== null) {
summarized[key] = summarizeValue(val, depth + 1);
} else if (typeof val === "string" && val.length > 100) {
summarized[key] = val.substring(0, 100) + "...";
} else {
summarized[key] = val;
}
}
return summarized;
}
return value;
}
export function Logged(name: string) {
const log = createLogger(name);
@@ -27,8 +79,8 @@ export function Logged(name: string) {
const start = performance.now();
const method = String(propKey);
// Pino's redact handles sanitization - just pass args directly
log.debug({ method, args: methodArgs }, `${method} started`);
// Summarize args to avoid huge log entries
log.debug({ method, args: summarizeArgs(methodArgs) }, `${method} started`);
const logCompletion = (err?: unknown) => {
const duration = Math.round(performance.now() - start);