singularity-forge/src/welcome-screen.ts
Mikael Hugo 22cbd83675 fix: update test snapshots for queryInstruction and complete /sf prefix Phase 2 deprecation
- Fix memory-embeddings-llm-gateway tests: add queryInstruction field to
  expected config objects after loadGatewayConfigFromEnv was updated to
  return it
- Add STYLEGUIDE.md: SF code standards adapted from ace-coder patterns
  (purpose doctrine, principles, anti-patterns STY001-012, thresholds,
  naming, patterns, documentation sections)
- Phase 2 /sf prefix removal: update all web components, browser dispatch,
  and tests to use direct commands (/autonomous, /stop, /next, /discuss,
  /init, /new-milestone) instead of /sf-prefixed forms
  - workflow-actions.ts: all command strings updated
  - chat-mode.tsx: SF_ACTIONS array updated
  - project-welcome.tsx: primaryCommand values updated
  - command-surface.tsx: fallback display updated
  - remaining-command-panels.tsx: usage examples updated
  - browser-slash-command-dispatch.ts: add stop/new-milestone/init to
    SF_PASSTHROUGH_COMMANDS so they route correctly to the extension
  - recovery-diagnostics-service.ts: suggestion commands updated
  - welcome-screen.ts: hint text updated
  - All affected tests updated to match new command strings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-09 00:17:47 +02:00

155 lines
5.3 KiB
TypeScript

/**
* SF Welcome Screen
*
* Two-panel bar layout: full-width accent bars at top/bottom (matching the
* autonomous mode progress widget style), logo left (fixed width), info right.
* Falls back to simple text on narrow terminals (<70 cols) or non-TTY.
*/
import { execFileSync } from "node:child_process";
import os from "node:os";
import chalk from "chalk";
import stripAnsi from "strip-ansi";
import { SF_LOGO } from "./logo.js";
export interface WelcomeScreenOptions {
version: string;
modelName?: string;
provider?: string;
remoteChannel?: string;
}
function getShortCwd(): string {
const cwd = process.cwd();
const home = os.homedir();
return cwd.startsWith(home) ? "~" + cwd.slice(home.length) : cwd;
}
/** Visible length — strips ANSI escape codes before measuring. */
function visLen(s: string): number {
return stripAnsi(s).length;
}
/** Right-pad a string to the given visible width. */
function rpad(s: string, w: number): string {
return s + " ".repeat(Math.max(0, w - visLen(s)));
}
/** Read the current git branch name. Returns undefined on failure. */
function getGitBranch(): string | undefined {
try {
return (
execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
encoding: "utf-8",
timeout: 2000,
stdio: ["ignore", "pipe", "ignore"],
}).trim() || undefined
);
} catch {
return undefined;
}
}
export function printWelcomeScreen(opts: WelcomeScreenOptions): void {
if (!process.stderr.isTTY) return;
const { version, modelName, provider, remoteChannel } = opts;
const shortCwd = getShortCwd();
const branch = getGitBranch();
const termWidth = (process.stderr.columns || 80) - 1;
// Narrow terminal fallback
if (termWidth < 70) {
process.stderr.write(
`\n Singularity Forge v${version}\n ${shortCwd}\n\n`,
);
return;
}
// ── Panel widths ────────────────────────────────────────────────────────────
// Layout: 1 leading space + LEFT_INNER logo content + 1 inner divider + RIGHT_INNER info
// Total: 1 + LEFT_INNER + 1 + RIGHT_INNER = termWidth
const LEFT_INNER = 34;
const RIGHT_INNER = termWidth - LEFT_INNER - 2; // 2 = leading space + inner divider
// ── Bar/divider chars (matching GLYPH.separator + widget ui.bar() style) ────
const H = "─",
DV = "│",
DS = "├";
// ── Left rows: blank + 6 logo lines + blank (8 total) ───────────────────────
const leftRows = ["", ...SF_LOGO, ""];
// ── Right rows (8 total, null = divider) ────────────────────────────────────
const titleLeft = ` ${chalk.bold("Singularity Forge")}`;
const titleRight = chalk.dim(`v${version}`);
const titleFill = RIGHT_INNER - visLen(titleLeft) - visLen(titleRight);
const titleRow = titleLeft + " ".repeat(Math.max(1, titleFill)) + titleRight;
const toolParts: string[] = [];
if (process.env.BRAVE_API_KEY) toolParts.push("Brave ✓");
if (process.env.BRAVE_ANSWERS_KEY) toolParts.push("Answers ✓");
if (process.env.JINA_API_KEY) toolParts.push("Jina ✓");
if (process.env.TAVILY_API_KEY) toolParts.push("Tavily ✓");
if (process.env.CONTEXT7_API_KEY) toolParts.push("Context7 ✓");
if (remoteChannel)
toolParts.push(
`${remoteChannel.charAt(0).toUpperCase() + remoteChannel.slice(1)}`,
);
// Tools left, hint right-aligned on the same row
const toolsLeft =
toolParts.length > 0 ? chalk.dim(" " + toolParts.join(" · ")) : "";
const hintRight = chalk.dim("/next to step · /help");
const footerFill = RIGHT_INNER - visLen(toolsLeft) - visLen(hintRight);
const footerRow = toolsLeft + " ".repeat(Math.max(1, footerFill)) + hintRight;
// Combined session line: "provider / model" or just model or just provider
const sessionParts = [provider, modelName].filter(Boolean);
const sessionLine =
sessionParts.length > 0
? ` Session ${chalk.dim(sessionParts.join(" / "))}`
: "";
// Combined project line: "~/path [branch]"
const branchSuffix = branch ? ` [${branch}]` : "";
const projectLine = ` Project ${chalk.dim(shortCwd + branchSuffix)}`;
const DIVIDER = null;
const rightRows: (string | null)[] = [
titleRow,
DIVIDER,
"",
sessionLine,
projectLine,
"",
DIVIDER,
footerRow,
];
// ── Render ──────────────────────────────────────────────────────────────────
const out: string[] = [""];
// Top bar — full-width accent separator, matches autonomous mode widget ui.bar()
out.push(chalk.cyan(H.repeat(termWidth)));
for (let i = 0; i < 8; i++) {
const row = leftRows[i] ?? "";
const lContent = rpad(row ? chalk.cyan(row) : "", LEFT_INNER);
const rRow = rightRows[i];
if (rRow === null) {
// Section divider: left logo area + dim ├────... extending right
out.push(" " + lContent + chalk.dim(DS + H.repeat(RIGHT_INNER)));
} else {
// Content row: 1 space + logo │ info (no outer vertical borders)
out.push(" " + lContent + chalk.dim(DV) + rpad(rRow, RIGHT_INNER));
}
}
// Bottom bar — full-width accent separator
out.push(chalk.cyan(H.repeat(termWidth)));
out.push("");
process.stderr.write(out.join("\n") + "\n");
}