sync: update from private repo (0cddeaa)
This commit is contained in:
@@ -37,6 +37,13 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
|
||||
const [sendError, setSendError] = useState<string | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
// Snapshot of comments.length at submit start. We hold "submitting" until
|
||||
// (a) the new user comment is reflected in the list AND (b) the job is
|
||||
// visibly busy (= picked up by a worker). Without this, the gap between the
|
||||
// POST resolving and the worker dispatching the job lets the user fire
|
||||
// multiple sends in a row.
|
||||
const submitBaselineRef = useRef<number | null>(null);
|
||||
const submitTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const [newMessageCount, setNewMessageCount] = useState(0);
|
||||
const prevCommentCountRef = useRef(comments.length);
|
||||
@@ -90,18 +97,32 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
|
||||
setAttachments(prev => prev.filter(a => a.name !== name));
|
||||
};
|
||||
|
||||
const releaseSubmitting = () => {
|
||||
if (submitTimeoutRef.current) {
|
||||
clearTimeout(submitTimeoutRef.current);
|
||||
submitTimeoutRef.current = null;
|
||||
}
|
||||
submitBaselineRef.current = null;
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if ((!draft.trim() && attachments.length === 0) || submitting) return;
|
||||
setSendError(null);
|
||||
setSubmitting(true);
|
||||
submitBaselineRef.current = comments.length;
|
||||
try {
|
||||
await onSubmit(draft, attachments.length > 0 ? attachments : undefined);
|
||||
setDraft('');
|
||||
setAttachments([]);
|
||||
// Hold the lock until the agent is visibly responding (see effect below).
|
||||
// Safety net: if the worker never picks the job up (queue stuck, server
|
||||
// crash, etc.), release the lock after 10s so the user isn't trapped.
|
||||
if (submitTimeoutRef.current) clearTimeout(submitTimeoutRef.current);
|
||||
submitTimeoutRef.current = setTimeout(releaseSubmitting, 10000);
|
||||
} catch (e) {
|
||||
setSendError(e instanceof Error && e.message ? e.message : '送信に失敗しました');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
releaseSubmitting();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,6 +186,26 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
|
||||
const canInterject = jobStatus === 'running' || jobStatus === 'waiting_subtasks';
|
||||
const inputLocked = jobStatus === 'dispatching';
|
||||
|
||||
// Release the submit lock once the agent is visibly responding: the new user
|
||||
// comment is reflected in the list AND the job has been picked up by a worker
|
||||
// (isBusy=true). This bridges the queued->dispatching gap where a stale
|
||||
// re-enabled send button would otherwise allow a double submit.
|
||||
useEffect(() => {
|
||||
if (!submitting) return;
|
||||
const baseline = submitBaselineRef.current;
|
||||
if (baseline === null) return;
|
||||
if (comments.length > baseline && isBusy) {
|
||||
releaseSubmitting();
|
||||
}
|
||||
}, [submitting, comments.length, isBusy]);
|
||||
|
||||
// Clear the safety-net timeout on unmount.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (submitTimeoutRef.current) clearTimeout(submitTimeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// During an active run, suppress the trailing thinking comment so the
|
||||
// live SSE preview is the single source of truth for in-flight text.
|
||||
// We keep the comment in history (MovementGroup will render it once the
|
||||
@@ -387,7 +428,7 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
|
||||
/>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={inputLocked}
|
||||
disabled={inputLocked || submitting}
|
||||
className="flex-shrink-0 w-9 h-9 flex items-center justify-center text-slate-500 hover:text-slate-900 hover:bg-surface rounded-md transition-colors disabled:opacity-50 disabled:hover:bg-transparent disabled:cursor-not-allowed"
|
||||
title="ファイルを添付"
|
||||
aria-label="ファイルを添付"
|
||||
|
||||
@@ -24,6 +24,13 @@ export function SafetyForm({ config, onChange }: SectionFormProps) {
|
||||
<HelpText>同一 movement への再訪問上限(ループ検出)。デフォルト: 3</HelpText>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FieldLabel>Max Tool Loop Repeats</FieldLabel>
|
||||
<FieldInput type="number" value={safety.maxToolLoopRepeats ?? 5}
|
||||
onChange={v => onChange('safety.maxToolLoopRepeats', Number(v))} />
|
||||
<HelpText>同一 movement 内で全く同じツール呼び出し(ツール名+引数)を連続で繰り返した回数がこの値に達したら、ループとみなして強制中断する(2以上、デフォルト: 5)。手前で1回エージェントに警告を注入する</HelpText>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FieldLabel>Prompt Guard Ratio</FieldLabel>
|
||||
<FieldInput type="number" value={safety.promptGuardRatio ?? 0.8}
|
||||
|
||||
Reference in New Issue
Block a user