fix: keep hidden sf commands callable in print mode
This commit is contained in:
parent
ccdf530488
commit
2e4bdd292c
5 changed files with 66 additions and 13 deletions
|
|
@ -229,6 +229,8 @@ export interface ExtensionBindings {
|
|||
commandContextActions?: ExtensionCommandContextActions;
|
||||
shutdownHandler?: ShutdownHandler;
|
||||
onError?: ExtensionErrorListener;
|
||||
/** When false, bind runtime contexts without firing session_start/resources_discover. */
|
||||
runLifecycle?: boolean;
|
||||
}
|
||||
|
||||
/** Options for AgentSession.prompt() */
|
||||
|
|
@ -2227,8 +2229,10 @@ export class AgentSession {
|
|||
|
||||
if (this._extensionRunner) {
|
||||
this._applyExtensionBindings(this._extensionRunner);
|
||||
await this._extensionRunner.emit({ type: "session_start" });
|
||||
await this.extendResourcesFromExtensions("startup");
|
||||
if (bindings.runLifecycle !== false) {
|
||||
await this._extensionRunner.emit({ type: "session_start" });
|
||||
await this.extendResourcesFromExtensions("startup");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ test("promptWithPrintTimeout_when_prompt_settles_clears_watchdog", async () => {
|
|||
|
||||
test("runPrintMode_when_extension_startup_hangs_still_runs_prompt", async () => {
|
||||
const logs: string[] = [];
|
||||
let lifecycleDisabled = false;
|
||||
const originalLog = console.log;
|
||||
console.log = (message?: unknown) => {
|
||||
logs.push(String(message));
|
||||
|
|
@ -54,7 +55,9 @@ test("runPrintMode_when_extension_startup_hangs_still_runs_prompt", async () =>
|
|||
try {
|
||||
const session = {
|
||||
sessionManager: { getHeader: () => null },
|
||||
bindExtensions: () => new Promise<void>(() => {}),
|
||||
bindExtensions: async (bindings: { runLifecycle?: boolean }) => {
|
||||
lifecycleDisabled = bindings.runLifecycle === false;
|
||||
},
|
||||
subscribe: () => () => undefined,
|
||||
prompt: async () => {},
|
||||
abort: async () => {},
|
||||
|
|
@ -76,6 +79,7 @@ test("runPrintMode_when_extension_startup_hangs_still_runs_prompt", async () =>
|
|||
});
|
||||
|
||||
assert.deepEqual(logs, ["hi"]);
|
||||
assert.equal(lifecycleDisabled, true);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import type { AssistantMessage, ImageContent } from "@singularity-forge/ai";
|
||||
import type { AgentSession } from "../core/agent-session.js";
|
||||
import type { ExtensionUIContext } from "../core/extensions/types.js";
|
||||
import { theme } from "./interactive/theme/theme.js";
|
||||
import { createDefaultCommandContextActions } from "./shared/command-context-actions.js";
|
||||
|
||||
/**
|
||||
* Options for print mode.
|
||||
|
|
@ -123,9 +126,18 @@ export async function runPrintMode(
|
|||
console.log(JSON.stringify(header));
|
||||
}
|
||||
}
|
||||
// Print mode intentionally skips extension session_start binding. One-shot
|
||||
// automation needs bounded prompt output; startup hooks are interactive/RPC
|
||||
// lifecycle work and have previously blocked `sf -p` before the prompt ran.
|
||||
// Bind command context so `/todo ...` and other extension slash commands work
|
||||
// in print mode, but skip session_start/resources_discover lifecycle work.
|
||||
// Those startup hooks are interactive/RPC concerns and have previously blocked
|
||||
// `sf -p` before the prompt ran.
|
||||
await session.bindExtensions({
|
||||
commandContextActions: createDefaultCommandContextActions(session),
|
||||
uiContext: createPrintModeUIContext(),
|
||||
onError: (err) => {
|
||||
console.error(`Extension error (${err.extensionPath}): ${err.error}`);
|
||||
},
|
||||
runLifecycle: false,
|
||||
});
|
||||
const liveness = createPrintModeLivenessReporter(mode);
|
||||
|
||||
// Always subscribe to enable session persistence via _handleAgentEvent
|
||||
|
|
@ -235,6 +247,39 @@ export async function runPrintMode(
|
|||
}
|
||||
}
|
||||
|
||||
function createPrintModeUIContext(): ExtensionUIContext {
|
||||
return {
|
||||
select: async () => undefined,
|
||||
confirm: async () => false,
|
||||
input: async () => undefined,
|
||||
notify: (message: string) => {
|
||||
process.stdout.write(message.endsWith("\n") ? message : `${message}\n`);
|
||||
},
|
||||
onTerminalInput: () => () => {},
|
||||
setStatus: () => {},
|
||||
setWorkingMessage: () => {},
|
||||
setWorkingVisible: () => {},
|
||||
setWidget: () => {},
|
||||
setFooter: () => {},
|
||||
setHeader: () => {},
|
||||
setTitle: () => {},
|
||||
custom: async () => undefined as never,
|
||||
pasteToEditor: () => {},
|
||||
setEditorText: () => {},
|
||||
getEditorText: () => "",
|
||||
editor: async () => undefined,
|
||||
setEditorComponent: () => {},
|
||||
get theme() {
|
||||
return theme;
|
||||
},
|
||||
getAllThemes: () => [],
|
||||
getTheme: () => undefined,
|
||||
setTheme: () => ({ success: false, error: "UI not available" }),
|
||||
getToolsExpanded: () => false,
|
||||
setToolsExpanded: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
export function createPrintModeLivenessReporter(
|
||||
mode: "text" | "json",
|
||||
): (event: { type: string; assistantMessageEvent?: { type: string } }) => void {
|
||||
|
|
|
|||
|
|
@ -321,7 +321,8 @@ export const TOP_LEVEL_SUBCOMMANDS = [
|
|||
// pick workflows, not personas, and SF runs implementation machinery (agent
|
||||
// picks, mode/work-mode shifts, orchestration internals) on its own. Commands
|
||||
// not in this set stay callable for scripting/debug but don't show up in the
|
||||
// slash catalog or help.
|
||||
// slash catalog or help. Registration uses DIRECT_SF_COMMANDS below; visibility
|
||||
// uses PUBLIC_DIRECT_COMMANDS.
|
||||
//
|
||||
// Hidden by category:
|
||||
// - /agent, /parallel, /cmux, /sidekicks — internal orchestration machinery
|
||||
|
|
@ -396,9 +397,7 @@ export const PUBLIC_TOP_LEVEL_SUBCOMMANDS = TOP_LEVEL_SUBCOMMANDS.filter(
|
|||
);
|
||||
|
||||
export const DIRECT_SF_COMMANDS = TOP_LEVEL_SUBCOMMANDS.filter(
|
||||
(command) =>
|
||||
PUBLIC_DIRECT_COMMANDS.has(command.cmd) &&
|
||||
!BASE_RUNTIME_COMMANDS.has(command.cmd),
|
||||
(command) => !BASE_RUNTIME_COMMANDS.has(command.cmd),
|
||||
);
|
||||
|
||||
export const DIRECT_SF_COMMAND_NAMES = DIRECT_SF_COMMANDS.map(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { showHelp } from "../commands/handlers/core.js";
|
|||
import { registerSFCommands } from "../commands/index.js";
|
||||
import { queryJournal } from "../journal.js";
|
||||
|
||||
test("direct SF command surface registers workflow verbs without legacy sf namespace", () => {
|
||||
test("direct SF command surface registers callable verbs without legacy sf namespace", () => {
|
||||
const registered = [];
|
||||
const pi = {
|
||||
registerCommand(name, options) {
|
||||
|
|
@ -38,9 +38,10 @@ test("direct SF command surface registers workflow verbs without legacy sf names
|
|||
assert.ok(names.includes("doctor"));
|
||||
assert.ok(names.includes("status"));
|
||||
assert.ok(names.includes("ship"));
|
||||
assert.equal(names.includes("plan"), false);
|
||||
assert.ok(names.includes("plan"));
|
||||
assert.ok(names.includes("todo"));
|
||||
assert.equal(names.includes("model"), false);
|
||||
assert.equal(names.includes("permission-profile"), false);
|
||||
assert.ok(names.includes("permission-profile"));
|
||||
assert.ok(!names.includes("sf"));
|
||||
assert.ok(!names.includes("stop"));
|
||||
assert.deepEqual(names, [...DIRECT_SF_COMMAND_NAMES].sort());
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue