fix(ui): stream phase progress without gates

This commit is contained in:
2026-08-24 12:10:56 +02:00
parent 36a7a0ab33
commit fa2298653b
10 changed files with 186 additions and 5 deletions
+28 -1
View File
@@ -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) => {