feat(session-limits): LS8-front — filet humain niveau 3 (saisie d'heure de reprise)
UI permettant à l'humain de renseigner l'heure de reprise quand le
niveau 2 détecte une limite sans heure exploitable.
- ports/index.ts : setResumeAt(agentId, resetsAtMs) sur InputGateway.
- adapters/input.ts : setResumeAt → invoke("set_resume_at").
- adapters/mock/index.ts : MockInputGateway.setResumeAt (resumeArmings[]).
- features/agents/useAgents.ts : action setResumeAt (sans mutation optimiste).
- features/agents/AgentLimitBadge.tsx : formulaire de saisie d'heure sur
l'état suspected sans heure + helper pur timeInputToEpochMs (TODO LS7 retiré).
- features/agents/AgentsPanel.tsx : câblage onSetResumeAt.
Tests AgentLimitBadge.test.tsx mis à jour au nouveau contrat + couverture LS8 ;
typecheck propre, suite agents verte.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -44,4 +44,10 @@ export class TauriInputGateway implements InputGateway {
|
|||||||
// `request` envelope, and returns whether a pending resume was disarmed.
|
// `request` envelope, and returns whether a pending resume was disarmed.
|
||||||
return invoke<boolean>("cancel_resume", { agentId });
|
return invoke<boolean>("cancel_resume", { agentId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setResumeAt(agentId: string, resetsAtMs: number): Promise<void> {
|
||||||
|
// `set_resume_at` takes bare `agent_id` + `resets_at_ms` (camelCase), like
|
||||||
|
// `cancel_resume`; arms the cancellable resume at the chosen instant.
|
||||||
|
await invoke("set_resume_at", { agentId, resetsAtMs });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1561,6 +1561,8 @@ export class MockInputGateway implements InputGateway {
|
|||||||
readonly frontAttached: { agentId: string; attached: boolean }[] = [];
|
readonly frontAttached: { agentId: string; attached: boolean }[] = [];
|
||||||
/** All recorded cancel-resume calls, in order. */
|
/** All recorded cancel-resume calls, in order. */
|
||||||
readonly cancelledResumes: string[] = [];
|
readonly cancelledResumes: string[] = [];
|
||||||
|
/** All recorded set-resume-at armings (human net, level 3), in order. */
|
||||||
|
readonly resumeArmings: { agentId: string; resetsAtMs: number }[] = [];
|
||||||
/**
|
/**
|
||||||
* What {@link cancelResume} resolves to (mirrors the backend `bool`: whether a
|
* What {@link cancelResume} resolves to (mirrors the backend `bool`: whether a
|
||||||
* pending resume was disarmed). Tests can flip it to exercise the "already
|
* pending resume was disarmed). Tests can flip it to exercise the "already
|
||||||
@ -1588,6 +1590,10 @@ export class MockInputGateway implements InputGateway {
|
|||||||
this.cancelledResumes.push(agentId);
|
this.cancelledResumes.push(agentId);
|
||||||
return this.cancelResumeResult;
|
return this.cancelResumeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setResumeAt(agentId: string, resetsAtMs: number): Promise<void> {
|
||||||
|
this.resumeArmings.push({ agentId, resetsAtMs });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** In-memory permissions gateway. */
|
/** In-memory permissions gateway. */
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* LS7-front — {@link AgentLimitBadge} presentation + its pure helpers
|
* LS7/LS8-front — {@link AgentLimitBadge} presentation + its pure helpers
|
||||||
* (ARCHITECTURE §21).
|
* (ARCHITECTURE §21).
|
||||||
*
|
*
|
||||||
* Two layers:
|
* Three layers:
|
||||||
* - the exported pure helpers `formatCountdown` / `formatResetTime` (edge cases:
|
* - the exported pure helpers `formatCountdown` / `formatResetTime` /
|
||||||
* 0, negative, sub-minute, multi-minute, midnight, seconds-stripped);
|
* `timeInputToEpochMs` (edge cases: 0, negative, sub-minute, multi-minute,
|
||||||
* - the rendered badge across its three states (limité jusqu'à HH:MM /
|
* midnight, seconds-stripped, malformed/empty time input);
|
||||||
* "limité" without a time / "heure inconnue" for suspected without a reset),
|
* - the rendered badge across its states (limité jusqu'à HH:MM / "limité"
|
||||||
* plus the "Annuler la reprise" button wiring when a resume is armed.
|
* without a time / the "heure inconnue" resume-time form for a suspected limit
|
||||||
|
* without a reset), plus the "Annuler la reprise" button wiring when a resume
|
||||||
|
* is armed;
|
||||||
|
* - the human-net form (LS8): submitting a time arms a resume via onSetResumeAt.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
@ -17,6 +20,7 @@ import {
|
|||||||
AgentLimitBadge,
|
AgentLimitBadge,
|
||||||
formatCountdown,
|
formatCountdown,
|
||||||
formatResetTime,
|
formatResetTime,
|
||||||
|
timeInputToEpochMs,
|
||||||
} from "./AgentLimitBadge";
|
} from "./AgentLimitBadge";
|
||||||
|
|
||||||
describe("formatCountdown", () => {
|
describe("formatCountdown", () => {
|
||||||
@ -74,13 +78,48 @@ describe("formatResetTime", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("timeInputToEpochMs", () => {
|
||||||
|
it("maps HH:MM to the same calendar day as `now`", () => {
|
||||||
|
const now = new Date(2026, 0, 15, 9, 0, 0).getTime();
|
||||||
|
const ms = timeInputToEpochMs("14:30", now);
|
||||||
|
expect(ms).not.toBeNull();
|
||||||
|
const d = new Date(ms!);
|
||||||
|
expect(d.getFullYear()).toBe(2026);
|
||||||
|
expect(d.getMonth()).toBe(0);
|
||||||
|
expect(d.getDate()).toBe(15);
|
||||||
|
expect(d.getHours()).toBe(14);
|
||||||
|
expect(d.getMinutes()).toBe(30);
|
||||||
|
expect(d.getSeconds()).toBe(0);
|
||||||
|
expect(d.getMilliseconds()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a past instant unchanged (backend clamps to now)", () => {
|
||||||
|
const now = new Date(2026, 0, 15, 18, 0, 0).getTime();
|
||||||
|
const ms = timeInputToEpochMs("08:00", now);
|
||||||
|
expect(ms).not.toBeNull();
|
||||||
|
expect(ms!).toBeLessThan(now);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for empty or malformed input", () => {
|
||||||
|
const now = Date.now();
|
||||||
|
expect(timeInputToEpochMs("", now)).toBeNull();
|
||||||
|
expect(timeInputToEpochMs("nope", now)).toBeNull();
|
||||||
|
expect(timeInputToEpochMs("25:00", now)).toBeNull();
|
||||||
|
expect(timeInputToEpochMs("12:60", now)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("AgentLimitBadge (render)", () => {
|
describe("AgentLimitBadge (render)", () => {
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
it("shows 'limité jusqu'à HH:MM' when the reset time is known", () => {
|
it("shows 'limité jusqu'à HH:MM' when the reset time is known", () => {
|
||||||
const at = new Date(2026, 0, 15, 14, 30, 0).getTime();
|
const at = new Date(2026, 0, 15, 14, 30, 0).getTime();
|
||||||
render(
|
render(
|
||||||
<AgentLimitBadge state={{ limitedUntil: at }} onCancelResume={noop} />,
|
<AgentLimitBadge
|
||||||
|
state={{ limitedUntil: at }}
|
||||||
|
onCancelResume={noop}
|
||||||
|
onSetResumeAt={noop}
|
||||||
|
/>,
|
||||||
);
|
);
|
||||||
const expected = `limité jusqu'à ${formatResetTime(at)}`;
|
const expected = `limité jusqu'à ${formatResetTime(at)}`;
|
||||||
expect(screen.getByText(expected)).toBeTruthy();
|
expect(screen.getByText(expected)).toBeTruthy();
|
||||||
@ -89,30 +128,74 @@ describe("AgentLimitBadge (render)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shows plain 'limité' when no reset time is known", () => {
|
it("shows plain 'limité' when no reset time is known", () => {
|
||||||
render(<AgentLimitBadge state={{}} onCancelResume={noop} />);
|
|
||||||
expect(screen.getByText("limité")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows the 'heure inconnue' note for a suspected limit without a time", () => {
|
|
||||||
render(
|
render(
|
||||||
<AgentLimitBadge state={{ suspected: true }} onCancelResume={noop} />,
|
<AgentLimitBadge state={{}} onCancelResume={noop} onSetResumeAt={noop} />,
|
||||||
);
|
);
|
||||||
expect(screen.getByText("limité")).toBeTruthy();
|
expect(screen.getByText("limité")).toBeTruthy();
|
||||||
expect(
|
|
||||||
screen.getByText(/heure inconnue — reprise à préciser/),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does NOT show the 'heure inconnue' note when a suspected limit has a time", () => {
|
it("shows the 'heure inconnue' resume-time form for a suspected limit without a time", () => {
|
||||||
|
render(
|
||||||
|
<AgentLimitBadge
|
||||||
|
state={{ suspected: true }}
|
||||||
|
onCancelResume={noop}
|
||||||
|
onSetResumeAt={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("limité")).toBeTruthy();
|
||||||
|
expect(screen.getByText(/heure inconnue/)).toBeTruthy();
|
||||||
|
// The human-net form: a resume-time input + a "schedule resume" button.
|
||||||
|
expect(screen.getByLabelText("resume time")).toBeTruthy();
|
||||||
|
expect(screen.getByLabelText("schedule resume")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT show the resume-time form when a suspected limit has a time", () => {
|
||||||
const at = new Date(2026, 0, 15, 14, 30, 0).getTime();
|
const at = new Date(2026, 0, 15, 14, 30, 0).getTime();
|
||||||
render(
|
render(
|
||||||
<AgentLimitBadge
|
<AgentLimitBadge
|
||||||
state={{ suspected: true, limitedUntil: at }}
|
state={{ suspected: true, limitedUntil: at }}
|
||||||
onCancelResume={noop}
|
onCancelResume={noop}
|
||||||
|
onSetResumeAt={noop}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.getByText(`limité jusqu'à ${formatResetTime(at)}`)).toBeTruthy();
|
expect(screen.getByText(`limité jusqu'à ${formatResetTime(at)}`)).toBeTruthy();
|
||||||
expect(screen.queryByText(/heure inconnue/)).toBeNull();
|
expect(screen.queryByText(/heure inconnue/)).toBeNull();
|
||||||
|
expect(screen.queryByLabelText("resume time")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("submitting the resume-time form arms a resume at the chosen epoch-ms", () => {
|
||||||
|
const onSetResumeAt = vi.fn();
|
||||||
|
render(
|
||||||
|
<AgentLimitBadge
|
||||||
|
state={{ suspected: true }}
|
||||||
|
onCancelResume={noop}
|
||||||
|
onSetResumeAt={onSetResumeAt}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
fireEvent.change(screen.getByLabelText("resume time"), {
|
||||||
|
target: { value: "23:45" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByLabelText("schedule resume"));
|
||||||
|
expect(onSetResumeAt).toHaveBeenCalledTimes(1);
|
||||||
|
const ms = onSetResumeAt.mock.calls[0][0] as number;
|
||||||
|
const d = new Date(ms);
|
||||||
|
expect(d.getHours()).toBe(23);
|
||||||
|
expect(d.getMinutes()).toBe(45);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not arm a resume when the time input is empty (button disabled)", () => {
|
||||||
|
const onSetResumeAt = vi.fn();
|
||||||
|
render(
|
||||||
|
<AgentLimitBadge
|
||||||
|
state={{ suspected: true }}
|
||||||
|
onCancelResume={noop}
|
||||||
|
onSetResumeAt={onSetResumeAt}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
(screen.getByLabelText("schedule resume") as HTMLButtonElement).disabled,
|
||||||
|
).toBe(true);
|
||||||
|
expect(onSetResumeAt).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the countdown + calls onCancelResume when a resume is armed", () => {
|
it("renders the countdown + calls onCancelResume when a resume is armed", () => {
|
||||||
@ -122,6 +205,7 @@ describe("AgentLimitBadge (render)", () => {
|
|||||||
<AgentLimitBadge
|
<AgentLimitBadge
|
||||||
state={{ limitedUntil: Date.now(), resumeFireAt: fireAt }}
|
state={{ limitedUntil: Date.now(), resumeFireAt: fireAt }}
|
||||||
onCancelResume={onCancelResume}
|
onCancelResume={onCancelResume}
|
||||||
|
onSetResumeAt={noop}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
// Countdown label present (≈ "reprise dans 1m 30s").
|
// Countdown label present (≈ "reprise dans 1m 30s").
|
||||||
@ -139,6 +223,7 @@ describe("AgentLimitBadge (render)", () => {
|
|||||||
<AgentLimitBadge
|
<AgentLimitBadge
|
||||||
state={{ resumeFireAt: Date.now() + 10_000 }}
|
state={{ resumeFireAt: Date.now() + 10_000 }}
|
||||||
onCancelResume={noop}
|
onCancelResume={noop}
|
||||||
|
onSetResumeAt={noop}
|
||||||
busy
|
busy
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,9 +13,29 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Button, cn } from "@/shared";
|
import { Button, Input, cn } from "@/shared";
|
||||||
import type { AgentLimitState } from "./useAgents";
|
import type { AgentLimitState } from "./useAgents";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a `<input type="time">` value (`"HH:MM"`) into an epoch-ms instant on
|
||||||
|
* the same calendar day as `now`. Returns `null` for an empty/malformed value so
|
||||||
|
* the caller can ignore an incomplete entry.
|
||||||
|
*
|
||||||
|
* No future/past guard on purpose: an instant earlier than `now` is left as-is —
|
||||||
|
* the backend clamps a past time to "now" (⇒ immediate resume), so the front
|
||||||
|
* needs no strict validation.
|
||||||
|
*/
|
||||||
|
export function timeInputToEpochMs(value: string, now: number): number | null {
|
||||||
|
const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
|
||||||
|
if (!match) return null;
|
||||||
|
const hours = Number(match[1]);
|
||||||
|
const minutes = Number(match[2]);
|
||||||
|
if (hours > 23 || minutes > 59) return null;
|
||||||
|
const d = new Date(now);
|
||||||
|
d.setHours(hours, minutes, 0, 0);
|
||||||
|
return d.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
/** Formats an epoch-ms instant as a local `HH:MM` wall-clock label. */
|
/** Formats an epoch-ms instant as a local `HH:MM` wall-clock label. */
|
||||||
export function formatResetTime(epochMs: number): string {
|
export function formatResetTime(epochMs: number): string {
|
||||||
return new Date(epochMs).toLocaleTimeString([], {
|
return new Date(epochMs).toLocaleTimeString([], {
|
||||||
@ -40,13 +60,19 @@ export interface AgentLimitBadgeProps {
|
|||||||
state: AgentLimitState;
|
state: AgentLimitState;
|
||||||
/** Invoked when the user clicks "Annuler la reprise". */
|
/** Invoked when the user clicks "Annuler la reprise". */
|
||||||
onCancelResume: () => void;
|
onCancelResume: () => void;
|
||||||
/** Disables the cancel action (e.g. while another request is in flight). */
|
/**
|
||||||
|
* Human net (level 3): invoked with the chosen wake-up instant (epoch-ms) when
|
||||||
|
* the user submits the resume-time form on a suspected-without-time limit.
|
||||||
|
*/
|
||||||
|
onSetResumeAt: (resetsAtMs: number) => void;
|
||||||
|
/** Disables the actions (e.g. while another request is in flight). */
|
||||||
busy?: boolean;
|
busy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentLimitBadge({
|
export function AgentLimitBadge({
|
||||||
state,
|
state,
|
||||||
onCancelResume,
|
onCancelResume,
|
||||||
|
onSetResumeAt,
|
||||||
busy = false,
|
busy = false,
|
||||||
}: AgentLimitBadgeProps) {
|
}: AgentLimitBadgeProps) {
|
||||||
const { limitedUntil, resumeFireAt, suspected } = state;
|
const { limitedUntil, resumeFireAt, suspected } = state;
|
||||||
@ -60,6 +86,24 @@ export function AgentLimitBadge({
|
|||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [resumeFireAt]);
|
}, [resumeFireAt]);
|
||||||
|
|
||||||
|
// Local resume-time entry (human net form). Bound to a `time` input.
|
||||||
|
const [resumeTime, setResumeTime] = useState("");
|
||||||
|
|
||||||
|
// The human-net form shows only when the limit is *suspected* with neither a
|
||||||
|
// reliable reset time nor an armed resume — IdeA must ask before resuming.
|
||||||
|
const needsResumeTime =
|
||||||
|
suspected === true &&
|
||||||
|
limitedUntil === undefined &&
|
||||||
|
resumeFireAt === undefined;
|
||||||
|
|
||||||
|
function handleSubmitResumeTime(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
const epochMs = timeInputToEpochMs(resumeTime, Date.now());
|
||||||
|
if (epochMs === null) return;
|
||||||
|
onSetResumeAt(epochMs);
|
||||||
|
setResumeTime("");
|
||||||
|
}
|
||||||
|
|
||||||
const limitLabel =
|
const limitLabel =
|
||||||
limitedUntil !== undefined
|
limitedUntil !== undefined
|
||||||
? `limité jusqu'à ${formatResetTime(limitedUntil)}`
|
? `limité jusqu'à ${formatResetTime(limitedUntil)}`
|
||||||
@ -82,15 +126,37 @@ export function AgentLimitBadge({
|
|||||||
{limitLabel}
|
{limitLabel}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{suspected && limitedUntil === undefined && (
|
{needsResumeTime && (
|
||||||
// Human net (level 3, §21.1): the limit is suspected but no reliable time
|
// Human net (level 3, §21.1): the limit is suspected with no reliable
|
||||||
// is known. IdeA never resumes blind. NOTE: there is no backend command
|
// time. IdeA never resumes blind — ask the user for a resume time, then
|
||||||
// yet to persist a user-entered resume time, so we only surface the state
|
// arm it via `set_resume_at`. A past time is clamped to "now" by the
|
||||||
// here. TODO(LS-front): wire an hour-entry form once the backend exposes a
|
// backend (⇒ immediate resume), so no strict validation is needed here.
|
||||||
// command to record the chosen resume instant — do NOT fabricate one.
|
<form
|
||||||
<span className="text-xs text-muted" role="note">
|
onSubmit={handleSubmitResumeTime}
|
||||||
heure inconnue — reprise à préciser
|
className="flex items-center gap-2"
|
||||||
</span>
|
aria-label="resume time form"
|
||||||
|
>
|
||||||
|
<span className="text-xs text-muted" role="note">
|
||||||
|
heure inconnue
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
aria-label="resume time"
|
||||||
|
value={resumeTime}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => setResumeTime(e.target.value)}
|
||||||
|
className="h-8 w-28 text-xs"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
variant="primary"
|
||||||
|
aria-label="schedule resume"
|
||||||
|
disabled={busy || resumeTime.trim() === ""}
|
||||||
|
>
|
||||||
|
Programmer la reprise
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{resumeFireAt !== undefined && (
|
{resumeFireAt !== undefined && (
|
||||||
|
|||||||
@ -381,6 +381,9 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
|||||||
state={limitState}
|
state={limitState}
|
||||||
busy={vm.busy}
|
busy={vm.busy}
|
||||||
onCancelResume={() => void vm.cancelResume(a.id)}
|
onCancelResume={() => void vm.cancelResume(a.id)}
|
||||||
|
onSetResumeAt={(resetsAtMs) =>
|
||||||
|
void vm.setResumeAt(a.id, resetsAtMs)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -121,6 +121,13 @@ export interface AgentsViewModel {
|
|||||||
* resume had already fired / none was armed).
|
* resume had already fired / none was armed).
|
||||||
*/
|
*/
|
||||||
cancelResume: (agentId: string) => Promise<boolean>;
|
cancelResume: (agentId: string) => Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Human net (level 3, ARCHITECTURE §21.1): arms a resume at a user-chosen
|
||||||
|
* `resetsAtMs` (epoch-ms) for an agent whose limit was suspected without a
|
||||||
|
* reliable time. No optimistic mutation — the backend re-emits
|
||||||
|
* `agentResumeScheduled`, which flips the badge to the nominal countdown.
|
||||||
|
*/
|
||||||
|
setResumeAt: (agentId: string, resetsAtMs: number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function describe(e: unknown): string {
|
function describe(e: unknown): string {
|
||||||
@ -447,6 +454,19 @@ export function useAgents(projectId: string): AgentsViewModel {
|
|||||||
[input],
|
[input],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setResumeAt = useCallback(
|
||||||
|
async (agentId: string, resetsAtMs: number): Promise<void> => {
|
||||||
|
// No optimistic mutation: the backend re-emits `agentResumeScheduled`,
|
||||||
|
// which the subscription folds into `limitByAgent` (badge → countdown).
|
||||||
|
try {
|
||||||
|
await input?.setResumeAt(agentId, resetsAtMs);
|
||||||
|
} catch (e) {
|
||||||
|
setError(describe(e));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[input],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
agents,
|
agents,
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
@ -468,5 +488,6 @@ export function useAgents(projectId: string): AgentsViewModel {
|
|||||||
launchAgent,
|
launchAgent,
|
||||||
stopAgent,
|
stopAgent,
|
||||||
cancelResume,
|
cancelResume,
|
||||||
|
setResumeAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -561,6 +561,15 @@ export interface InputGateway {
|
|||||||
* `agentResumeCancelled` on success, which clears the countdown in the UI.
|
* `agentResumeCancelled` on success, which clears the countdown in the UI.
|
||||||
*/
|
*/
|
||||||
cancelResume(agentId: string): Promise<boolean>;
|
cancelResume(agentId: string): Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Human net (level 3, ARCHITECTURE §21.1): arms a resume at a user-chosen
|
||||||
|
* instant for an agent whose limit was *suspected* without a reliable reset
|
||||||
|
* time. `resetsAtMs` is the chosen wake-up in epoch-milliseconds; the backend
|
||||||
|
* clamps a past instant to "now" (⇒ immediate resume). This arms the same
|
||||||
|
* cancellable resume as the automatic path and re-emits `agentResumeScheduled`,
|
||||||
|
* so the UI flips on its own from "heure inconnue" to the nominal countdown.
|
||||||
|
*/
|
||||||
|
setResumeAt(agentId: string, resetsAtMs: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user