fix(ui): stream phase progress without gates
This commit is contained in:
@@ -52,6 +52,11 @@ import {
|
||||
} from "./gate/disambiguation/index.js";
|
||||
import { isReserved } from "./gate/core/reserved-labels.mjs";
|
||||
|
||||
// Machine-readable RPC notification consumed by SessionBridge. Pi's extension API
|
||||
// exposes notify/input but no custom client-event emitter, so this reserved prefix is
|
||||
// the narrow transport contract for phase progress that does not require a human gate.
|
||||
export const PHASE_STARTED_NOTIFICATION_PREFIX = "__tht_phase_started__:";
|
||||
|
||||
// Load the workflow contract once when Pi loads the extension. Asking the model to
|
||||
// discover/read the skill as its first action proved unreliable with remote models:
|
||||
// they can spend the whole RPC turn exploring the repository before opening the exact
|
||||
@@ -596,6 +601,18 @@ export default function (pi) {
|
||||
let lastSteered = false;
|
||||
let pendingKickoff = null;
|
||||
let activeSessionId = null;
|
||||
const announcedPhases = new Map();
|
||||
const announceCurrentPhase = async (ctx, session) => {
|
||||
if (ctx.mode !== "rpc" || typeof session !== "string" || !session.trim()) return;
|
||||
try {
|
||||
const phase = phaseId(ctx, currentPhase(ctx, session));
|
||||
if (!/^F[1-8]$/.test(phase) || announcedPhases.get(session) === phase) return;
|
||||
await ctx.ui.notify(`${PHASE_STARTED_NOTIFICATION_PREFIX}${phase}`, "info");
|
||||
announcedPhases.set(session, phase);
|
||||
} catch {
|
||||
// Progress reporting is observational: it must never block the workflow.
|
||||
}
|
||||
};
|
||||
const memoryGate = createMemoryGate({
|
||||
workflow: {
|
||||
activate: () => {
|
||||
@@ -748,7 +765,8 @@ export default function (pi) {
|
||||
|
||||
// 3) BEFORE_AGENT_START: one-shot kickoff injection (appends the operational
|
||||
// instructions to the system prompt on the turn that starts/resumes a session).
|
||||
pi.on("before_agent_start", (event, ctx) => {
|
||||
pi.on("before_agent_start", async (event, ctx) => {
|
||||
await announceCurrentPhase(ctx, process.env.THT_SESSION);
|
||||
if (!pendingKickoff) return undefined;
|
||||
const sessionId = process.env.THT_SESSION;
|
||||
const isProvidedNewSession =
|
||||
@@ -789,9 +807,18 @@ export default function (pi) {
|
||||
lastSteered = false;
|
||||
}
|
||||
activeSessionId = null;
|
||||
announcedPhases.clear();
|
||||
_phaseMetaCache = null;
|
||||
});
|
||||
|
||||
// A reviewer tool is the ownership boundary that persists a decision and may
|
||||
// advance the folded workflow phase. Observe the persisted phase after the tool
|
||||
// completes, including auto-approved phases that never open a human widget.
|
||||
pi.on("tool_result", async (event, ctx) => {
|
||||
const session = typeof event.input?.session === "string" ? event.input.session : null;
|
||||
if (session) await announceCurrentPhase(ctx, session);
|
||||
});
|
||||
|
||||
// 5) AGENT_END prose safety net: if the model emits prose instead of a reviewer_*
|
||||
// tool call (and the lock is active), nudge it back to the gate tools.
|
||||
pi.on("agent_end", async (event) => {
|
||||
|
||||
Reference in New Issue
Block a user