Skip to content

Commit 67a11ee

Browse files
Separate response submission from refresh failures
Co-authored-by: DanielWalnut <hetaoBackend@users.noreply.github.com>
1 parent 47ff7c1 commit 67a11ee

3 files changed

Lines changed: 99 additions & 41 deletions

File tree

taskboard-electron/src/renderer/App.tsx

Lines changed: 71 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ import {
3636
mergeChannelsStatus,
3737
} from "./channelsSettings.ts";
3838
import {
39+
getTaskResponseUiState,
3940
prepareTaskResponse,
4041
selectTickAfterRefresh,
4142
startHeartbeatTickPolling,
42-
taskNeedsResponse,
43+
type TaskResponseRefreshResult,
4344
} from "./operatorUi.ts";
4445
import { buildExecutionSteps } from "./traceSteps.ts";
4546

@@ -3071,6 +3072,7 @@ function DetailPanel({ task, onClose, onRespond, onResume }: any) {
30713072
const [answerText, setAnswerText] = useState("");
30723073
const [responseLoading, setResponseLoading] = useState(false);
30733074
const [responseError, setResponseError] = useState("");
3075+
const [responseResult, setResponseResult] = useState<TaskResponseRefreshResult | null>(null);
30743076
const [messages, setMessages] = useState<any[]>([]);
30753077
const [events, setEvents] = useState<any[]>([]);
30763078
const [showMessages, setShowMessages] = useState(false);
@@ -3152,6 +3154,7 @@ function DetailPanel({ task, onClose, onRespond, onResume }: any) {
31523154
setAnswerText("");
31533155
setResponseError("");
31543156
setResponseLoading(false);
3157+
setResponseResult(null);
31553158
}, [task.id, task.question, task.answer]);
31563159

31573160
const handleResponse = async () => {
@@ -3164,14 +3167,16 @@ function DetailPanel({ task, onClose, onRespond, onResume }: any) {
31643167
setResponseError("");
31653168
setResponseLoading(true);
31663169
try {
3167-
await onRespond(task.id, prepared.answer);
3170+
const result = await onRespond(task.id, prepared.answer);
31683171
setAnswerText("");
3172+
setResponseResult({ refreshed: result?.refreshed !== false });
31693173
} catch (error) {
31703174
setResponseError(error instanceof Error ? error.message : "Failed to submit response.");
31713175
} finally {
31723176
setResponseLoading(false);
31733177
}
31743178
};
3179+
const responseUiState = getTaskResponseUiState(task.question, task.answer, responseResult);
31753180

31763181
const handleResume = async () => {
31773182
if (!resumeText.trim()) return;
@@ -3296,7 +3301,7 @@ function DetailPanel({ task, onClose, onRespond, onResume }: any) {
32963301
)}
32973302
</Section>
32983303

3299-
{taskNeedsResponse(task.question, task.answer) && (
3304+
{responseUiState !== "hidden" && (
33003305
<Section title="Agent Question">
33013306
<div
33023307
style={{
@@ -3314,46 +3319,68 @@ function DetailPanel({ task, onClose, onRespond, onResume }: any) {
33143319
>
33153320
{task.question}
33163321
</div>
3317-
<textarea
3318-
value={answerText}
3319-
onChange={(event) => {
3320-
setAnswerText(event.target.value);
3321-
if (responseError) setResponseError("");
3322-
}}
3323-
placeholder="Enter your answer..."
3324-
disabled={responseLoading}
3325-
rows={4}
3326-
style={{
3327-
width: "100%",
3328-
boxSizing: "border-box",
3329-
background: theme.field,
3330-
color: theme.text,
3331-
border: `1px solid ${responseError ? theme.red : theme.border}`,
3332-
borderRadius: 8,
3333-
padding: 12,
3334-
fontSize: 12,
3335-
fontFamily: "'JetBrains Mono', monospace",
3336-
lineHeight: 1.6,
3337-
resize: "vertical",
3338-
outline: "none",
3339-
}}
3340-
/>
3341-
{responseError && (
3342-
<div style={{ color: theme.red, fontSize: 11, marginTop: 8 }}>{responseError}</div>
3343-
)}
3344-
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 10 }}>
3345-
<button
3346-
onClick={handleResponse}
3347-
disabled={responseLoading}
3322+
{responseUiState === "form" ? (
3323+
<>
3324+
<textarea
3325+
value={answerText}
3326+
onChange={(event) => {
3327+
setAnswerText(event.target.value);
3328+
if (responseError) setResponseError("");
3329+
}}
3330+
placeholder="Enter your answer..."
3331+
disabled={responseLoading}
3332+
rows={4}
3333+
style={{
3334+
width: "100%",
3335+
boxSizing: "border-box",
3336+
background: theme.field,
3337+
color: theme.text,
3338+
border: `1px solid ${responseError ? theme.red : theme.border}`,
3339+
borderRadius: 8,
3340+
padding: 12,
3341+
fontSize: 12,
3342+
fontFamily: "'JetBrains Mono', monospace",
3343+
lineHeight: 1.6,
3344+
resize: "vertical",
3345+
outline: "none",
3346+
}}
3347+
/>
3348+
{responseError && (
3349+
<div style={{ color: theme.red, fontSize: 11, marginTop: 8 }}>{responseError}</div>
3350+
)}
3351+
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 10 }}>
3352+
<button
3353+
onClick={handleResponse}
3354+
disabled={responseLoading}
3355+
style={{
3356+
...primaryButton(),
3357+
cursor: responseLoading ? "wait" : "pointer",
3358+
opacity: responseLoading ? 0.65 : 1,
3359+
}}
3360+
>
3361+
{responseLoading ? "Submitting..." : "Submit Answer"}
3362+
</button>
3363+
</div>
3364+
</>
3365+
) : (
3366+
<div
33483367
style={{
3349-
...primaryButton(),
3350-
cursor: responseLoading ? "wait" : "pointer",
3351-
opacity: responseLoading ? 0.65 : 1,
3368+
background: responseUiState === "submitted-stale" ? theme.orangeBg : theme.greenBg,
3369+
border: `1px solid ${
3370+
responseUiState === "submitted-stale" ? theme.orange : theme.green
3371+
}`,
3372+
borderRadius: 8,
3373+
padding: 12,
3374+
color: theme.text,
3375+
fontSize: 12,
3376+
lineHeight: 1.6,
33523377
}}
33533378
>
3354-
{responseLoading ? "Submitting..." : "Submit Answer"}
3355-
</button>
3356-
</div>
3379+
{responseUiState === "submitted-stale"
3380+
? "Answer submitted, but the task refresh failed. This view may be stale; automatic refresh will retry."
3381+
: "Answer submitted. Refreshing task status..."}
3382+
</div>
3383+
)}
33573384
</Section>
33583385
)}
33593386

@@ -5987,9 +6014,11 @@ export default function App() {
59876014
setSkills(skillsRes.skills || []);
59886015
setConnected(true);
59896016
setApiError(null);
6017+
return true;
59906018
} catch (err) {
59916019
setConnected(false);
59926020
setApiError(`Failed to fetch tasks: ${err.message}`);
6021+
return false;
59936022
}
59946023
}, []);
59956024

@@ -6162,11 +6191,12 @@ export default function App() {
61626191
const handleRespond = async (id, answer) => {
61636192
try {
61646193
await respondToTask(id, answer);
6165-
await poll();
61666194
} catch (e) {
61676195
setApiError(`Respond failed: ${e.message}`);
61686196
throw e;
61696197
}
6198+
const refreshed = await poll();
6199+
return { refreshed };
61706200
};
61716201

61726202
const handleResume = () => {

taskboard-electron/src/renderer/operatorUi.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect, test } from "bun:test";
22

33
import {
4+
getTaskResponseUiState,
45
prepareTaskResponse,
56
selectTickAfterRefresh,
67
startHeartbeatTickPolling,
@@ -25,6 +26,17 @@ test("prepareTaskResponse trims answers and rejects blank input", () => {
2526
});
2627
});
2728

29+
test("task response UI treats submit success separately from refresh failure", () => {
30+
expect(getTaskResponseUiState("Proceed?", null, null)).toBe("form");
31+
expect(getTaskResponseUiState("Proceed?", null, { refreshed: true })).toBe("submitted");
32+
expect(getTaskResponseUiState("Proceed?", null, { refreshed: false })).toBe("submitted-stale");
33+
});
34+
35+
test("task response UI hides once the refreshed task is answered", () => {
36+
expect(getTaskResponseUiState("Proceed?", "Yes", { refreshed: true })).toBe("hidden");
37+
expect(getTaskResponseUiState(null, null, { refreshed: false })).toBe("hidden");
38+
});
39+
2840
test("selectTickAfterRefresh preserves a selected tick still in the refreshed list", () => {
2941
const ticks = [{ id: 12 }, { id: 11 }, { id: 10 }];
3042

taskboard-electron/src/renderer/operatorUi.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,26 @@ type HeartbeatTickPollingOptions<T> = {
1414
cancelScheduled?: (timer: unknown) => void;
1515
};
1616

17+
export type TaskResponseRefreshResult = {
18+
refreshed: boolean;
19+
};
20+
21+
export type TaskResponseUiState = "hidden" | "form" | "submitted" | "submitted-stale";
22+
1723
export function taskNeedsResponse(question: unknown, answer: unknown): boolean {
1824
return typeof question === "string" && question.trim().length > 0 && !String(answer ?? "").trim();
1925
}
2026

27+
export function getTaskResponseUiState(
28+
question: unknown,
29+
answer: unknown,
30+
result: TaskResponseRefreshResult | null,
31+
): TaskResponseUiState {
32+
if (!taskNeedsResponse(question, answer)) return "hidden";
33+
if (!result) return "form";
34+
return result.refreshed ? "submitted" : "submitted-stale";
35+
}
36+
2137
export function prepareTaskResponse(
2238
value: string,
2339
): { answer: string; error: null } | { answer: null; error: string } {

0 commit comments

Comments
 (0)