summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/mesh/TaskOutput.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/mesh/TaskOutput.tsx')
-rw-r--r--makima/frontend/src/components/mesh/TaskOutput.tsx151
1 files changed, 148 insertions, 3 deletions
diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx
index cb0eba3..d53429d 100644
--- a/makima/frontend/src/components/mesh/TaskOutput.tsx
+++ b/makima/frontend/src/components/mesh/TaskOutput.tsx
@@ -16,9 +16,23 @@ interface TaskOutputProps {
taskId?: string | null;
/** Callback when user sends input (to show it immediately in output) */
onUserInput?: (message: string) => void;
+ /** Set of pending question IDs (for supervisor questions) */
+ pendingQuestionIds?: Set<string>;
+ /** Callback to answer a supervisor question */
+ onAnswerQuestion?: (questionId: string, response: string) => Promise<void>;
}
-export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSubtaskView, onClear, taskId, onUserInput }: TaskOutputProps) {
+export function TaskOutput({
+ entries,
+ isStreaming,
+ viewingSubtaskName,
+ onClearSubtaskView,
+ onClear,
+ taskId,
+ onUserInput,
+ pendingQuestionIds,
+ onAnswerQuestion,
+}: TaskOutputProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true);
const [inputValue, setInputValue] = useState("");
@@ -135,7 +149,12 @@ export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSu
) : (
<div className="space-y-3">
{entries.map((entry, idx) => (
- <OutputEntryRenderer key={idx} entry={entry} />
+ <OutputEntryRenderer
+ key={idx}
+ entry={entry}
+ pendingQuestionIds={pendingQuestionIds}
+ onAnswerQuestion={onAnswerQuestion}
+ />
))}
{isStreaming && (
<span className="inline-block w-2 h-4 bg-[#9bc3ff] animate-pulse" />
@@ -177,7 +196,13 @@ export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSu
);
}
-function OutputEntryRenderer({ entry }: { entry: TaskOutputEvent }) {
+interface OutputEntryRendererProps {
+ entry: TaskOutputEvent;
+ pendingQuestionIds?: Set<string>;
+ onAnswerQuestion?: (questionId: string, response: string) => Promise<void>;
+}
+
+function OutputEntryRenderer({ entry, pendingQuestionIds, onAnswerQuestion }: OutputEntryRendererProps) {
const [expanded, setExpanded] = useState(false);
switch (entry.messageType) {
@@ -278,11 +303,131 @@ function OutputEntryRenderer({ entry }: { entry: TaskOutputEvent }) {
case "auth_required":
return <AuthRequiredEntry entry={entry} />;
+ case "supervisor_question":
+ return (
+ <SupervisorQuestionEntry
+ entry={entry}
+ pendingQuestionIds={pendingQuestionIds}
+ onAnswerQuestion={onAnswerQuestion}
+ />
+ );
+
default:
return null;
}
}
+function SupervisorQuestionEntry({
+ entry,
+ pendingQuestionIds,
+ onAnswerQuestion,
+}: {
+ entry: TaskOutputEvent;
+ pendingQuestionIds?: Set<string>;
+ onAnswerQuestion?: (questionId: string, response: string) => Promise<void>;
+}) {
+ const questionId = entry.toolInput?.question_id as string;
+ const choices = (entry.toolInput?.choices as string[]) || [];
+ const context = entry.toolInput?.context as string | null;
+
+ const [customInput, setCustomInput] = useState("");
+ const [showOther, setShowOther] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+
+ const isPending = pendingQuestionIds?.has(questionId) ?? false;
+
+ const handleChoiceSelect = async (choice: string) => {
+ if (!onAnswerQuestion || submitting) return;
+ setSubmitting(true);
+ try {
+ await onAnswerQuestion(questionId, choice);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleOtherSubmit = async () => {
+ if (!onAnswerQuestion || !customInput.trim() || submitting) return;
+ setSubmitting(true);
+ try {
+ await onAnswerQuestion(questionId, customInput.trim());
+ setCustomInput("");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+ <div className="bg-amber-900/20 border border-amber-500/50 rounded p-3 my-2">
+ <div className="flex items-center gap-2 text-amber-400 font-semibold mb-2">
+ <span>?</span>
+ <span>Question</span>
+ {!isPending && (
+ <span className="text-green-400 text-xs font-normal">(Answered)</span>
+ )}
+ </div>
+
+ {context && (
+ <p className="text-amber-200/60 text-xs mb-2 uppercase">{context}</p>
+ )}
+
+ <p className="text-amber-100 mb-3">{entry.content}</p>
+
+ {isPending && (
+ <div className="space-y-2">
+ {choices.length > 0 && (
+ <div className="flex flex-wrap gap-2">
+ {choices.map((choice, idx) => (
+ <button
+ key={idx}
+ onClick={() => handleChoiceSelect(choice)}
+ disabled={submitting}
+ className="px-3 py-1.5 text-sm font-mono bg-amber-500/20 border border-amber-500/50 hover:bg-amber-500/30 disabled:opacity-50 text-amber-100 transition-colors"
+ >
+ {choice}
+ </button>
+ ))}
+ </div>
+ )}
+
+ {/* Other option */}
+ {!showOther ? (
+ <button
+ onClick={() => setShowOther(true)}
+ className="text-xs text-amber-400 hover:text-amber-300 transition-colors"
+ >
+ + Other (custom response)
+ </button>
+ ) : (
+ <div className="flex gap-2">
+ <input
+ type="text"
+ value={customInput}
+ onChange={(e) => setCustomInput(e.target.value)}
+ placeholder="Type custom response..."
+ disabled={submitting}
+ className="flex-1 px-2 py-1 bg-[#0a1525] border border-amber-500/30 text-amber-100 text-sm rounded focus:outline-none focus:border-amber-400"
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && customInput.trim()) {
+ handleOtherSubmit();
+ }
+ }}
+ />
+ <button
+ onClick={handleOtherSubmit}
+ disabled={submitting || !customInput.trim()}
+ className="px-3 py-1 bg-amber-500 text-black text-sm font-medium rounded disabled:opacity-50 disabled:cursor-not-allowed transition-colors hover:bg-amber-400"
+ >
+ {submitting ? "..." : "Submit"}
+ </button>
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+ );
+}
+
function AuthRequiredEntry({ entry }: { entry: TaskOutputEvent }) {
const [authCode, setAuthCode] = useState("");
const [submitting, setSubmitting] = useState(false);