mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
feat: add workflow-wave-plan skill and A2UI debug logging
- Add CSV Wave planning and execution skill (explore via wave → conflict resolution → execution CSV with linked exploration context) - Add debug NDJSON logging for A2UI submit-all and multi-answer polling
This commit is contained in:
@@ -341,6 +341,77 @@ export class A2UIWebSocketHandler {
|
||||
): boolean {
|
||||
const params = action.parameters ?? {};
|
||||
const questionId = typeof params.questionId === 'string' ? params.questionId : undefined;
|
||||
|
||||
// Handle submit-all first - it uses compositeId instead of questionId
|
||||
if (action.actionId === 'submit-all') {
|
||||
const compositeId = typeof params.compositeId === 'string' ? params.compositeId : undefined;
|
||||
const questionIds = Array.isArray(params.questionIds) ? params.questionIds as string[] : undefined;
|
||||
if (!compositeId || !questionIds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// DEBUG: NDJSON log for submit-all received
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H2',
|
||||
event: 'submit_all_received_in_handleQuestionAction',
|
||||
compositeId,
|
||||
questionCount: questionIds.length
|
||||
}));
|
||||
|
||||
// Collect answers for all sub-questions
|
||||
const answers: QuestionAnswer[] = [];
|
||||
for (const qId of questionIds) {
|
||||
const singleSel = this.singleSelectSelections.get(qId);
|
||||
const multiSel = this.multiSelectSelections.get(qId);
|
||||
const inputVal = this.inputValues.get(qId);
|
||||
const otherText = this.inputValues.get(`__other__:${qId}`);
|
||||
|
||||
if (singleSel !== undefined) {
|
||||
const value = singleSel === '__other__' && otherText ? otherText : singleSel;
|
||||
answers.push({ questionId: qId, value, cancelled: false });
|
||||
} else if (multiSel !== undefined) {
|
||||
const values = Array.from(multiSel).map(v =>
|
||||
v === '__other__' && otherText ? otherText : v
|
||||
);
|
||||
answers.push({ questionId: qId, value: values, cancelled: false });
|
||||
} else if (inputVal !== undefined) {
|
||||
answers.push({ questionId: qId, value: inputVal, cancelled: false });
|
||||
} else {
|
||||
answers.push({ questionId: qId, value: '', cancelled: false });
|
||||
}
|
||||
|
||||
// Cleanup per-question tracking
|
||||
this.singleSelectSelections.delete(qId);
|
||||
this.multiSelectSelections.delete(qId);
|
||||
this.inputValues.delete(qId);
|
||||
this.inputValues.delete(`__other__:${qId}`);
|
||||
}
|
||||
|
||||
// Call multi-answer callback
|
||||
let handled = false;
|
||||
if (this.multiAnswerCallback) {
|
||||
handled = this.multiAnswerCallback(compositeId, answers);
|
||||
}
|
||||
if (!handled) {
|
||||
// Store for HTTP polling retrieval
|
||||
this.resolvedMultiAnswers.set(compositeId, { compositeId, answers, timestamp: Date.now() });
|
||||
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H2',
|
||||
event: 'answer_stored_for_polling',
|
||||
compositeId,
|
||||
answerCount: answers.length
|
||||
}));
|
||||
}
|
||||
this.activeSurfaces.delete(compositeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// For other actions, questionId is required
|
||||
if (!questionId) {
|
||||
return false;
|
||||
}
|
||||
@@ -446,60 +517,6 @@ export class A2UIWebSocketHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'submit-all': {
|
||||
// Multi-question composite submit
|
||||
const compositeId = typeof params.compositeId === 'string' ? params.compositeId : undefined;
|
||||
const questionIds = Array.isArray(params.questionIds) ? params.questionIds as string[] : undefined;
|
||||
if (!compositeId || !questionIds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect answers for all sub-questions
|
||||
const answers: QuestionAnswer[] = [];
|
||||
for (const qId of questionIds) {
|
||||
const singleSel = this.singleSelectSelections.get(qId);
|
||||
const multiSel = this.multiSelectSelections.get(qId);
|
||||
const inputVal = this.inputValues.get(qId);
|
||||
const otherText = this.inputValues.get(`__other__:${qId}`);
|
||||
|
||||
if (singleSel !== undefined) {
|
||||
// Resolve __other__ to actual text input
|
||||
const value = singleSel === '__other__' && otherText ? otherText : singleSel;
|
||||
answers.push({ questionId: qId, value, cancelled: false });
|
||||
} else if (multiSel !== undefined) {
|
||||
// Resolve __other__ in multi-select: replace with actual text
|
||||
const values = Array.from(multiSel).map(v =>
|
||||
v === '__other__' && otherText ? otherText : v
|
||||
);
|
||||
answers.push({ questionId: qId, value: values, cancelled: false });
|
||||
} else if (inputVal !== undefined) {
|
||||
answers.push({ questionId: qId, value: inputVal, cancelled: false });
|
||||
} else {
|
||||
// No value recorded — include empty
|
||||
answers.push({ questionId: qId, value: '', cancelled: false });
|
||||
}
|
||||
|
||||
// Cleanup per-question tracking
|
||||
this.singleSelectSelections.delete(qId);
|
||||
this.multiSelectSelections.delete(qId);
|
||||
this.inputValues.delete(qId);
|
||||
this.inputValues.delete(`__other__:${qId}`);
|
||||
}
|
||||
|
||||
// Call multi-answer callback
|
||||
let handled = false;
|
||||
if (this.multiAnswerCallback) {
|
||||
handled = this.multiAnswerCallback(compositeId, answers);
|
||||
}
|
||||
if (!handled) {
|
||||
// Store for HTTP polling retrieval
|
||||
this.resolvedMultiAnswers.set(compositeId, { compositeId, answers, timestamp: Date.now() });
|
||||
}
|
||||
// Always clean up UI state
|
||||
this.activeSurfaces.delete(compositeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -611,8 +611,29 @@ export function handleAnswer(answer: QuestionAnswer): boolean {
|
||||
* @returns True if answer was processed
|
||||
*/
|
||||
export function handleMultiAnswer(compositeId: string, answers: QuestionAnswer[]): boolean {
|
||||
const handleStartTime = Date.now();
|
||||
|
||||
// DEBUG: NDJSON log for handleMultiAnswer start
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H3',
|
||||
event: 'handle_multi_answer_start',
|
||||
compositeId,
|
||||
answerCount: answers.length
|
||||
}));
|
||||
|
||||
const pending = getPendingQuestion(compositeId);
|
||||
if (!pending) {
|
||||
// DEBUG: NDJSON log for missing pending question
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H3',
|
||||
event: 'handle_multi_answer_no_pending',
|
||||
compositeId,
|
||||
elapsedMs: Date.now() - handleStartTime
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -625,6 +646,17 @@ export function handleMultiAnswer(compositeId: string, answers: QuestionAnswer[]
|
||||
});
|
||||
|
||||
removePendingQuestion(compositeId);
|
||||
|
||||
// DEBUG: NDJSON log for handleMultiAnswer complete
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H3',
|
||||
event: 'handle_multi_answer_complete',
|
||||
compositeId,
|
||||
elapsedMs: Date.now() - handleStartTime
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -637,9 +669,24 @@ export function handleMultiAnswer(compositeId: string, answers: QuestionAnswer[]
|
||||
*/
|
||||
function startAnswerPolling(questionId: string, isComposite: boolean = false): void {
|
||||
const pollPath = `/api/a2ui/answer?questionId=${encodeURIComponent(questionId)}&composite=${isComposite}`;
|
||||
const startTime = Date.now();
|
||||
|
||||
// DEBUG: NDJSON log for polling start
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H1',
|
||||
event: 'polling_start',
|
||||
questionId,
|
||||
isComposite,
|
||||
port: DASHBOARD_PORT,
|
||||
firstPollDelayMs: POLL_INTERVAL_MS
|
||||
}));
|
||||
|
||||
console.error(`[A2UI-Poll] Starting polling for questionId=${questionId}, composite=${isComposite}, port=${DASHBOARD_PORT}`);
|
||||
|
||||
let pollCount = 0;
|
||||
|
||||
const poll = () => {
|
||||
// Stop if the question was already resolved or timed out
|
||||
if (!hasPendingQuestion(questionId)) {
|
||||
@@ -647,6 +694,20 @@ function startAnswerPolling(questionId: string, isComposite: boolean = false): v
|
||||
return;
|
||||
}
|
||||
|
||||
pollCount++;
|
||||
const pollStartTime = Date.now();
|
||||
|
||||
// DEBUG: NDJSON log for each poll attempt
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H1',
|
||||
event: 'poll_attempt',
|
||||
questionId,
|
||||
pollCount,
|
||||
elapsedMs: pollStartTime - startTime
|
||||
}));
|
||||
|
||||
const req = http.get({ hostname: '127.0.0.1', port: DASHBOARD_PORT, path: pollPath, timeout: 2000 }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
|
||||
@@ -667,6 +728,20 @@ function startAnswerPolling(questionId: string, isComposite: boolean = false): v
|
||||
|
||||
console.error(`[A2UI-Poll] Answer received for questionId=${questionId}:`, JSON.stringify(parsed).slice(0, 200));
|
||||
|
||||
// DEBUG: NDJSON log for answer received
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H1',
|
||||
event: 'answer_received',
|
||||
questionId,
|
||||
pollCount,
|
||||
elapsedMs: Date.now() - startTime,
|
||||
pollLatencyMs: Date.now() - pollStartTime,
|
||||
isComposite,
|
||||
answerPreview: JSON.stringify(parsed).slice(0, 100)
|
||||
}));
|
||||
|
||||
if (isComposite && Array.isArray(parsed.answers)) {
|
||||
const ok = handleMultiAnswer(questionId, parsed.answers as QuestionAnswer[]);
|
||||
console.error(`[A2UI-Poll] handleMultiAnswer result: ${ok}`);
|
||||
|
||||
Reference in New Issue
Block a user