Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/resources/extensions/gsd/auto-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ export interface WidgetStateAccessors {
getAutoStartTime(): number;
isStepMode(): boolean;
getCmdCtx(): ExtensionCommandContext | null;
getCurrentUnitModel?(): { provider: string; id: string } | null;
getBasePath(): string;
isVerbose(): boolean;
/** True while newSession() is in-flight — render must not access session state. */
Expand Down Expand Up @@ -616,9 +617,12 @@ export function updateProgressWidget(
const cxPctVal = cxUsage?.percent ?? 0;
const cxPct = cxUsage?.percent !== null ? cxPctVal.toFixed(1) : "?";

// Model display — shown in context section, not stats
const modelId = cmdCtx?.model?.id ?? "";
const modelProvider = cmdCtx?.model?.provider ?? "";
// Model display — shown in context section, not stats.
// Prefer the model actually dispatched for this unit; fall back
// to cmdCtx.model for compatibility.
const currentUnitModel = accessors.getCurrentUnitModel?.();
const modelId = currentUnitModel?.id ?? cmdCtx?.model?.id ?? "";
const modelProvider = currentUnitModel?.provider ?? cmdCtx?.model?.provider ?? "";
const tierIcon = resolveServiceTierIcon(effectiveServiceTier, modelId);
const modelDisplay = (modelProvider && modelId
? `${modelProvider}/${modelId}`
Expand Down
3 changes: 3 additions & 0 deletions src/resources/extensions/gsd/auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,9 @@ const widgetStateAccessors: WidgetStateAccessors = {
getAutoStartTime: () => s.autoStartTime,
isStepMode: () => s.stepMode,
getCmdCtx: () => s.cmdCtx,
getCurrentUnitModel: () => s.currentUnitModel
? { provider: s.currentUnitModel.provider, id: s.currentUnitModel.id }
: null,
getBasePath: () => s.basePath,
isVerbose: () => s.verbose,
isSessionSwitching: isSessionSwitchInFlight,
Expand Down
10 changes: 8 additions & 2 deletions src/resources/extensions/gsd/auto/phases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,9 @@ export async function runUnitPhase(
const previousTier = s.currentUnitRouting?.tier;

s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
// Reset per-unit model snapshot before routing so stale model labels do not
// leak from the previous unit if model selection fails early.
s.currentUnitModel = null;
const unitStartSeq = ic.nextSeq();
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
deps.captureAvailableSkills();
Expand Down Expand Up @@ -951,11 +954,10 @@ export async function runUnitPhase(
s.currentUnitModel =
modelResult.appliedModel as AutoSession["currentUnitModel"];

// Status bar + progress widget
// Status bar + preconditions
ctx.ui.setStatus("gsd-auto", "auto");
if (mid)
deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
deps.updateProgressWidget(ctx, unitType, unitId, state);

deps.ensurePreconditions(unitType, unitId, s.basePath, state);

Expand Down Expand Up @@ -1048,6 +1050,10 @@ export async function runUnitPhase(
}
}

// Render progress widget after model selection so the model label reflects
// the actual dispatched unit model from the first frame.
deps.updateProgressWidget(ctx, unitType, unitId, state);

// Start unit supervision
deps.clearUnitTimeout();
deps.startUnitSupervision({
Expand Down
3 changes: 3 additions & 0 deletions src/resources/extensions/gsd/auto/run-unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export async function runUnit(
`Failed to restore ${s.currentUnitModel.provider}/${s.currentUnitModel.id} after session creation. Using session default.`,
"warning",
);
// Clear so the widget shows the actual model the unit runs on,
// not the override that failed to apply (#3418).
s.currentUnitModel = null;
}
}

Expand Down
45 changes: 45 additions & 0 deletions src/resources/extensions/gsd/tests/auto-loop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,31 @@ test("runUnit re-applies the selected unit model after newSession before dispatc
assert.equal(pi.calls.length, 1);
});

test("runUnit clears currentUnitModel when setModel restore fails after newSession (#3418)", async () => {
_resetPendingResolve();

const ctx = makeMockCtx();
const pi = makeMockPi();
// setModel returns false — restore failed, unit will run on session default
pi.setModel = async () => false;

const s = makeMockSession();
s.currentUnitModel = { provider: "anthropic", id: "claude-opus-4-6" };

const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");

await new Promise((r) => setTimeout(r, 10));
resolveAgentEnd(makeEvent());
await resultPromise;

// Widget must not show the override model when execution fell back to session default
assert.equal(
s.currentUnitModel,
null,
"currentUnitModel must be cleared when setModel restore fails so widget does not show wrong model (#3418)",
);
});

// ─── Structural assertions ───────────────────────────────────────────────────

test("auto-loop.ts exports autoLoop, runUnit, resolveAgentEnd", async () => {
Expand Down Expand Up @@ -346,6 +371,26 @@ test("auto/phases.ts: selectAndApplyModel called exactly once and before updateP
);
});

test("auto/phases.ts: updateProgressWidget runs after hook model override block (#3412)", () => {
const src = readFileSync(
resolve(import.meta.dirname, "..", "auto", "phases.ts"),
"utf-8",
);
const fnStart = src.indexOf("export async function runUnitPhase");
assert.ok(fnStart > 0, "runUnitPhase should exist in phases.ts");
const fnBody = src.slice(fnStart, fnStart + 10000);

const hookOverrideIdx = fnBody.indexOf("const hookModelOverride = sidecarItem?.model ?? iterData.hookModelOverride;");
const widgetIdx = fnBody.indexOf("updateProgressWidget(");

assert.ok(hookOverrideIdx > 0, "hook model override block should exist in runUnitPhase");
assert.ok(widgetIdx > 0, "updateProgressWidget should exist in runUnitPhase");
assert.ok(
hookOverrideIdx < widgetIdx,
"updateProgressWidget must run after hook model override so widget shows final unit model",
);
});

// ─── autoLoop tests (T02) ─────────────────────────────────────────────────

/**
Expand Down
Loading