sf snapshot: uncommitted changes after 43m inactivity

This commit is contained in:
Mikael Hugo 2026-05-05 21:39:56 +02:00
parent 54bfd68b01
commit d75ebfe7c3
44 changed files with 121 additions and 2977 deletions

View file

@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.13/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

View file

@ -51,7 +51,6 @@
| **Skills** | Skill tool registration, health, telemetry |
| **Slash Commands** | Command boilerplate generators extension |
| **State Machine** | State, history, persistence, reactive graph |
| **Studio App** | Electron desktop app (renderer, main, preload) |
| **Subagent** | Parallel/serial subagent delegation |
| **Syntax Highlighting** | Syntect-backed ANSI code coloring |
| **Text Processing** | Diff, truncation, HTML→MD, ANSI, JSON parse |
@ -833,20 +832,6 @@
---
## studio/ — Electron Desktop App
| File | System Label(s) | Description |
|------|-----------------|-------------|
| studio/electron.vite.config.ts | Studio App, Build System | Electron Vite build configuration |
| studio/src/main/index.ts | Studio App | Electron main process window creation |
| studio/src/preload/index.ts | Studio App | Context isolation preload for IPC bridge |
| studio/src/preload/index.d.ts | Studio App | Preload bridge type definitions |
| studio/src/renderer/src/main.tsx | Studio App | React renderer entry point |
| studio/src/renderer/src/App.tsx | Studio App | Main app component |
| studio/src/renderer/src/lib/theme/tokens.ts | Studio App | Design tokens (colors, fonts, sizes) |
---
## rust-engine/ — Rust Engine
| File | System Label(s) | Description |
@ -1005,7 +990,6 @@ Quick lookup: which files are part of each system?
| **Skills** | src/resources/skills/*, sf/skill-telemetry.ts, sf/preferences-skills.ts, core/skills.ts |
| **Slash Commands** | src/resources/extensions/slash-commands/* |
| **State Machine** | sf/state.ts, sf/history.ts, sf/json-persistence.ts, sf/memory-store.ts, sf/reactive-graph.ts, core/agent-session.ts, web/lib/sf-workspace-store.tsx |
| **Studio App** | studio/* |
| **Subagent** | src/resources/extensions/subagent/*, src/resources/agents/* |
| **Syntax Highlighting** | rust-engine/crates/engine/src/highlight.rs, packages/rust-engine/src/highlight/* |
| **Text Processing** | rust-engine/crates/engine/src/diff.rs, html.rs, text.rs, truncate.rs, json_parse.rs, stream_process.rs |

2345
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,8 +13,7 @@
},
"type": "module",
"workspaces": [
"packages/*",
"studio"
"packages/*"
],
"bin": {
"sf": "dist/loader.js",

View file

@ -26,8 +26,7 @@ console.log(`[bump-version] package.json: ${oldVersion} → ${newVersion}`);
// 2. Update all non-private workspace packages under packages/
// These share the root version to keep the repo's source of truth coherent
// with what ships. Private packages (studio, web) are skipped — they're not
// published and have their own lifecycle.
// with what ships. The web package is private and has its own lifecycle.
const workspacePackages = [
"daemon",
"native",

View file

@ -10,7 +10,7 @@
*/
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
const CONTRACT_EXACT_PATHS = new Set([
"src/resources/extensions/sf/workflow-templates/registry.json",
@ -27,7 +27,7 @@ function trackedJsonFiles() {
return out
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
.filter((line) => line && existsSync(line));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`failed to list tracked JSON files: ${message}`);
@ -84,6 +84,15 @@ export function checkJsonPolicy(paths, readText) {
try {
parsed = JSON.parse(readText(path));
} catch (error) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === "ENOENT"
) {
filesParsed--;
continue;
}
const message = error instanceof Error ? error.message : String(error);
failures.push(`${path}: invalid JSON (${message})`);
continue;

View file

@ -23,6 +23,20 @@ test("check-versioned-json: parses every JSON file", () => {
assert.equal(result.filesParsed, 2);
});
test("check-versioned-json: skips deleted tracked files", () => {
const result = checkJsonPolicy(["deleted.json", "package.json"], (path) => {
if (path === "deleted.json") {
const error = new Error("ENOENT");
error.code = "ENOENT";
throw error;
}
return '{"version":"1.0.0"}';
});
assert.deepEqual(result.failures, []);
assert.equal(result.filesParsed, 1);
});
test("check-versioned-json: requires numeric schemaVersion for SF contracts", () => {
const files = {
"src/resources/extensions/sf/learning/data/unit-weights.json":

View file

@ -2,7 +2,7 @@
/**
* Migrate ALL test files from node:test to vitest.
*
* Scans src/, packages/, web/, studio/, and scripts/.
* Scans src/, packages/, web/, and scripts/.
* Changes:
* 1. Replace `from "node:test"` `from 'vitest'` in all imports
* 2. Files using mock.fn(): replace `mock.fn` `vi.fn` and add `vi` to imports
@ -16,7 +16,6 @@ const ROOTS = [
join(process.cwd(), "src"),
join(process.cwd(), "packages"),
join(process.cwd(), "web"),
join(process.cwd(), "studio"),
join(process.cwd(), "scripts"),
];

View file

@ -72,7 +72,6 @@ const RISK_TIERS = {
"Migration",
"Onboarding",
"Memory Extension",
"Studio App",
"VS Code Extension",
"Voice",
"CMux",

View file

@ -180,10 +180,9 @@ function readManagedResourceManifest(
* bundled resourcesDir).
*
* Walks all files under `rootDir` and builds an aggregate fingerprint from
* `${relativePath}:${mtime}:${size}` for each one. This is orders of magnitude
* faster than full content hashing for large resource trees (1,700+ files)
* while still reliably detecting changes during development (npm link) and
* after SF version upgrades.
* `${relativePath}:${sha256(contents)}` for each one. Content hashing is required
* because same-byte-length prompt/resource edits must still invalidate the
* installed resource cache.
*
* Cost is ~1-5ms even for large trees negligible at startup.
*
@ -210,13 +209,11 @@ function collectFileEntries(dir: string, root: string, out: string[]): void {
collectFileEntries(fullPath, root, out);
} else {
const rel = relative(root, fullPath);
// Use mtime and size for the fingerprint instead of full content hashing (#3471).
// This is orders of magnitude faster for large resource trees (1700+ files)
// while still reliably detecting dev-workflow changes and upgrades.
let fingerprint: string;
try {
const stats = lstatSync(fullPath);
fingerprint = `${stats.mtimeMs}:${stats.size}`;
fingerprint = createHash("sha256")
.update(readFileSync(fullPath))
.digest("hex");
} catch {
// Unreadable file — fall back to a stable marker so the entry still
// contributes to the aggregate hash and future reads will re-hash.

View file

@ -43,20 +43,19 @@ import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const jiti = require("jiti")(__dirname, { interopDefault: true, debug: false });
const { EVALUATE_HELPERS_SOURCE } = jiti("../evaluate-helpers.ts");
const { EVALUATE_HELPERS_SOURCE } = jiti("../evaluate-helpers.js");
// 2. Intent scoring — module-private buildIntentScoringScript.
// Extract the function from source, wrap it, and eval to get the builder.
const intentSource = readFileSync(resolve(ROOT, "tools/intent.ts"), "utf-8");
const intentSource = readFileSync(resolve(ROOT, "tools/intent.js"), "utf-8");
function extractBuildIntentScoringScript() {
// Match the function body: starts with "function buildIntentScoringScript"
// and returns a template literal string. We extract up to the matching closing brace.
const startMarker =
"function buildIntentScoringScript(intent: string, scope?: string): string {";
const startMarker = "function buildIntentScoringScript(intent, scope) {";
const startIdx = intentSource.indexOf(startMarker);
if (startIdx === -1)
throw new Error("Could not find buildIntentScoringScript in intent.ts");
throw new Error("Could not find buildIntentScoringScript in intent.js");
// Walk from start, counting braces to find the end
let depth = 0;
@ -86,14 +85,13 @@ function extractBuildIntentScoringScript() {
const buildIntentScoringScript = extractBuildIntentScoringScript();
// 3. Form analysis — module-private buildFormAnalysisScript.
const formsSource = readFileSync(resolve(ROOT, "tools/forms.ts"), "utf-8");
const formsSource = readFileSync(resolve(ROOT, "tools/forms.js"), "utf-8");
function extractBuildFormAnalysisScript() {
const startMarker =
"function buildFormAnalysisScript(selector?: string): string {";
const startMarker = "function buildFormAnalysisScript(selector) {";
const startIdx = formsSource.indexOf(startMarker);
if (startIdx === -1)
throw new Error("Could not find buildFormAnalysisScript in forms.ts");
throw new Error("Could not find buildFormAnalysisScript in forms.js");
let depth = 0;
let foundFirst = false;

View file

@ -84,13 +84,17 @@ export function appendNotification(
) {
if (!_basePath) return;
if (_suppressCount > 0) return;
const normalizedSeverity = severity === "warn" ? "warning" : severity;
const persistedMessage =
message.length > 500 ? message.slice(0, 500) + "…" : message;
if (!shouldPersistNotification(severity, metadata, persistedMessage)) return;
if (
!shouldPersistNotification(normalizedSeverity, metadata, persistedMessage)
)
return;
// Use explicit dedupe_key when provided; fall back to message-hash based key.
const dedupKey = metadata?.dedupe_key
? `${_basePath}:${metadata.dedupe_key}`
: `${_basePath}:${severity}:${source}:${persistedMessage}`;
: `${_basePath}:${normalizedSeverity}:${source}:${persistedMessage}`;
const now = Date.now();
const lastSeen = _recentMessageTimestamps.get(dedupKey);
if (lastSeen !== undefined && now - lastSeen <= DEDUP_WINDOW_MS) return;
@ -103,7 +107,8 @@ export function appendNotification(
if (
hasRecentPersistedDuplicate(
_basePath,
metadata?.dedupe_key ?? `${severity}:${source}:${persistedMessage}`,
metadata?.dedupe_key ??
`${normalizedSeverity}:${source}:${persistedMessage}`,
now,
)
) {
@ -112,7 +117,7 @@ export function appendNotification(
const entry = {
id: randomUUID(),
ts: new Date().toISOString(),
severity,
severity: normalizedSeverity,
message: persistedMessage,
source,
read: false,

View file

@ -23,25 +23,25 @@ test("assistant-message caps thinking block height when text content is present"
assert.match(
src,
/const hasTextContent = message\.content\.some\(\(c\) => c\.type === "text" && c\.text\.trim\(\)\.length > 0\);/,
/const hasTextContent = message\.content\.some\(\s*\(c\) => c\.type === "text" && c\.text\.trim\(\)\.length > 0,\s*\);/,
"assistant-message should detect text presence in mixed thinking+text messages",
);
assert.match(
src,
/const hasToolContent = message\.content\.some\(\(c\) => c\.type === "toolCall" \|\| c\.type === "serverToolUse"\);/,
/const hasToolContent = message\.content\.some\(\s*\(c\) => c\.type === "toolCall" \|\| c\.type === "serverToolUse",\s*\);/,
"assistant-message should detect tool blocks in mixed turns",
);
assert.match(
src,
/const shouldCapThinking = hasTextContent \|\| hasToolContent \|\| message\.provider === "claude-code";/,
/const shouldCapThinking =\s*hasTextContent \|\| hasToolContent \|\| message\.provider === "claude-code";/,
"assistant-message should derive a cap policy that also covers claude-code long reasoning traces",
);
assert.match(
src,
/if \(shouldCapThinking\)\s*\{\s*thinkingMarkdown\.maxLines = 8;\s*\}/s,
/if \(shouldCapThinking\) \{\s*thinkingMarkdown\.maxLines = 8;\s*\}/s,
"assistant-message should cap visible thinking lines when the cap policy is active",
);
});

View file

@ -7,21 +7,15 @@ import {
parseBundledExtensions,
parseKnownProviders,
parseSearchProviders,
parseWorkflowToolNames,
START,
updateFeaturesContent,
} from "../../scripts/generate-features-inventory.mjs";
test("features inventory generator surfaces expected workflow tool, extension, search, and provider inventories", () => {
const workflowTools = parseWorkflowToolNames();
const extensions = parseBundledExtensions();
const searchProviders = parseSearchProviders();
const knownProviders = parseKnownProviders();
assert.ok(workflowTools.includes("sf_plan_milestone"));
assert.ok(workflowTools.includes("sf_replan_slice"));
assert.ok(workflowTools.includes("sf_task_complete"));
assert.ok(extensions.includes("sf"));
assert.ok(extensions.includes("search-the-web"));
assert.ok(extensions.includes("subagent"));
@ -54,13 +48,12 @@ test("features inventory generator injects a rendered appendix between markers",
assert.match(
updated,
new RegExp(
`${START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n\\n### Workflow Tools`,
`${START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n\\n### Bundled Extensions`,
),
);
assert.match(updated, /### Bundled Extensions/);
assert.match(updated, /### Search Providers/);
assert.match(updated, /### Known Model Providers/);
assert.match(updated, /- `sf_plan_milestone`/);
assert.match(updated, /- `search-the-web` — \[extension-manifest\.json]/);
assert.match(updated, /- `brave`/);
assert.match(updated, /- `xiaomi`/);

View file

@ -188,7 +188,7 @@ test("npm pack produces tarball with required files", async () => {
"tarball contains pkg/package.json",
);
assert.ok(
files.some((f) => f.includes("src/resources/extensions/sf/index.ts")),
files.some((f) => f.includes("src/resources/extensions/sf/index.js")),
"tarball contains bundled sf extension",
);
assert.ok(
@ -271,7 +271,7 @@ test("tarball installs and sf binary resolves", async () => {
"resources",
"extensions",
"sf",
"index.ts",
"index.js",
);
assert.ok(
existsSync(installedSfExt),

View file

@ -921,7 +921,7 @@ test("command-surface session affordances use the shared store action path", ()
);
assert.match(
commandSurfaceSource,
/void renameSessionFromSurface\(selectedNameTarget\.sessionPath, selectedNameTarget\.name\)/,
/void renameSessionFromSurface\(\s*selectedNameTarget\.sessionPath,\s*selectedNameTarget\.name,\s*\)/,
"command-surface rename apply button should reuse the shared session-rename store action",
);
});

View file

@ -608,7 +608,12 @@ test("fresh sf --web browser onboarding stays locked on failed validation and un
headers: { Accept: "application/json", ...auth },
signal: AbortSignal.timeout(10_000),
});
assert.equal(bootAfter.ok, true);
const bootAfterText = await bootAfter.clone().text();
assert.equal(
bootAfter.ok,
true,
`expected boot endpoint to respond after successful retry: ${bootAfter.status} ${bootAfterText}`,
);
const bootAfterPayload = (await bootAfter.json()) as any;
assert.equal(bootAfterPayload.onboarding.locked, false);
assert.equal(bootAfterPayload.onboarding.lockReason, null);

View file

@ -613,7 +613,7 @@ test("chat tool blocks normalize Claude Code tool names before choosing built-in
assert.match(
source,
/const normalizedToolName = typeof tool\.name === "string" \? tool\.name\.toLowerCase\(\) : ""/,
/const normalizedToolName =\s*typeof tool\.name === "string" \? tool\.name\.toLowerCase\(\) : ""/,
"chat-mode.tsx must normalize Claude Code tool names before matching built-in tool render branches",
);
assert.match(
@ -951,7 +951,7 @@ test("recovery diagnostics surface stays on a dedicated route with explicit stal
assert.match(
storeSource,
/loadRecoveryDiagnostics = async/,
/loadRecoveryDiagnostics =\s*async/,
"sf-workspace-store.tsx must expose a recovery diagnostics loader",
);
assert.match(

View file

@ -139,7 +139,7 @@ test("InteractiveMode kills descendant processes on shutdown", () => {
test("bg-shell removes signal handlers on session_shutdown", () => {
const src = readSource(
"src/resources/extensions/bg-shell/bg-shell-lifecycle.ts",
"src/resources/extensions/bg-shell/bg-shell-lifecycle.js",
);
assert.ok(
src.includes('process.off("SIGTERM"') ||
@ -157,7 +157,7 @@ test("bg-shell removes signal handlers on session_shutdown", () => {
test("pendingAlerts has a maximum size cap", () => {
const src = readSource(
"src/resources/extensions/bg-shell/process-manager.ts",
"src/resources/extensions/bg-shell/process-manager.js",
);
assert.ok(
src.includes("MAX_PENDING_ALERTS"),

View file

@ -6,7 +6,7 @@ import { test } from "vitest";
const __dirname = dirname(fileURLToPath(import.meta.url));
const subagentSrc = readFileSync(
join(__dirname, "../resources/extensions/subagent/index.ts"),
join(__dirname, "../resources/extensions/subagent/index.js"),
"utf-8",
);
@ -31,7 +31,7 @@ test("subagent debate mode injects prior-round transcript", () => {
);
assert.match(
subagentSrc,
/const\s+transcriptEntries:\s*string\[\]\s*=\s*\[\]/,
/const\s+transcriptEntries\s*=\s*\[\]/,
"debate should maintain a transcript across rounds",
);
assert.match(
@ -49,7 +49,7 @@ test("subagent debate mode injects prior-round transcript", () => {
test("subagent details includes debate as a first-class mode", () => {
assert.match(
subagentSrc,
/type\s+SubagentMode\s*=\s*"single"\s*\|\s*"parallel"\s*\|\s*"debate"\s*\|\s*"chain"/,
/TaskBatchModeSchema\s*=\s*StringEnum\(\["parallel",\s*"debate"\]/,
);
assert.match(
subagentSrc,

View file

@ -32,7 +32,7 @@ test("update commands use the registry fetch helper instead of npm view (#3806)"
"resources",
"extensions",
"sf",
"commands-handlers.ts",
"commands-handlers.js",
),
"utf-8",
);
@ -113,7 +113,7 @@ test("commands-handlers uses resolveInstallCommand instead of hardcoded npm (#41
"resources",
"extensions",
"sf",
"commands-handlers.ts",
"commands-handlers.js",
),
"utf-8",
);

View file

@ -69,7 +69,7 @@ test("Windows launch points use shell-safe shims", () => {
"resources",
"extensions",
"sf",
"pre-execution-checks.ts",
"pre-execution-checks.js",
),
"utf8",
);
@ -81,5 +81,5 @@ test("Windows launch points use shell-safe shims", () => {
assert.match(sfClient, /shell:\s*process\.platform === "win32"/);
assert.match(updateService, /npm\.cmd/);
assert.match(preExecution, /npm\.cmd/);
assert.match(validatePack, /shell:\s*process\.platform === 'win32'/);
assert.match(validatePack, /shell:\s*process\.platform === ["']win32["']/);
});

View file

@ -58,12 +58,15 @@ export function resolveSubprocessModule(
relPath: string,
checkExists: (path: string) => boolean = defaultExistsSync,
): SubprocessModuleResolution {
if (isUnderNodeModules(packageRoot)) {
const jsRelPath = relPath.replace(/\.ts$/, ".js");
const distPath = join(packageRoot, "dist", jsRelPath);
if (checkExists(distPath)) {
return { modulePath: distPath, useCompiledJs: true };
}
const jsRelPath = relPath.replace(/\.ts$/, ".js");
const distPath = join(packageRoot, "dist", jsRelPath);
if (checkExists(distPath)) {
return { modulePath: distPath, useCompiledJs: true };
}
const sourceJsPath = join(packageRoot, "src", jsRelPath);
if (checkExists(sourceJsPath)) {
return { modulePath: sourceJsPath, useCompiledJs: true };
}
return {

View file

@ -13,7 +13,7 @@
* so nothing is lost. The SF extension reads SF_CLI_WORKTREE to know
* when a session was launched via -w.
*
* Note: Extension modules are .ts files loaded via jiti (not compiled to .js).
* Note: Extension modules are JavaScript resource files loaded via jiti.
* We use createJiti() here because this module is compiled by tsc but imports
* from resources/extensions/sf/ which are shipped as raw .ts (#1283).
*/
@ -89,11 +89,11 @@ interface ExtensionModules {
async function loadExtensionModules(): Promise<ExtensionModules> {
if (_ext) return _ext;
const [wtMgr, autoWt, gitBridge, gitSvc, wt] = await Promise.all([
jiti.import(sfExtensionPath("worktree-manager.ts"), {}) as Promise<any>,
jiti.import(sfExtensionPath("auto-worktree.ts"), {}) as Promise<any>,
jiti.import(sfExtensionPath("native-git-bridge.ts"), {}) as Promise<any>,
jiti.import(sfExtensionPath("git-service.ts"), {}) as Promise<any>,
jiti.import(sfExtensionPath("worktree.ts"), {}) as Promise<any>,
jiti.import(sfExtensionPath("worktree-manager.js"), {}) as Promise<any>,
jiti.import(sfExtensionPath("auto-worktree.js"), {}) as Promise<any>,
jiti.import(sfExtensionPath("native-git-bridge.js"), {}) as Promise<any>,
jiti.import(sfExtensionPath("git-service.js"), {}) as Promise<any>,
jiti.import(sfExtensionPath("worktree.js"), {}) as Promise<any>,
]);
_ext = {
createWorktree: wtMgr.createWorktree,

View file

@ -1,39 +0,0 @@
import { resolve } from "node:path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "electron-vite";
export default defineConfig({
main: {
build: {
outDir: "dist/main",
rollupOptions: {
input: {
index: resolve(__dirname, "src/main/index.ts"),
},
},
},
},
preload: {
build: {
outDir: "dist/preload",
rollupOptions: {
input: {
index: resolve(__dirname, "src/preload/index.ts"),
},
},
},
},
renderer: {
root: resolve(__dirname, "src/renderer"),
resolve: {
alias: {
"@": resolve(__dirname, "src/renderer/src"),
},
},
plugins: [tailwindcss(), react()],
build: {
outDir: resolve(__dirname, "dist/renderer"),
},
},
});

View file

@ -1,34 +0,0 @@
{
"name": "@singularity-forge/studio",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "dist/main/index.js",
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build",
"preview": "electron-vite preview",
"test": "node --test test/*.test.mjs"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-resizable-panels": "^4.7.3",
"zustand": "^5.0.8"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
"@types/node": "^24.0.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"electron": "^41.0.3",
"electron-vite": "^5.0.0",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=24.15.0"
}
}

View file

@ -1,55 +0,0 @@
import { join } from "node:path";
import { app, BrowserWindow } from "electron";
const __dirname = import.meta.dirname;
let _mainWindow: BrowserWindow | null = null;
function createWindow(): BrowserWindow {
const preload = join(__dirname, "../preload/index.mjs");
const window = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1100,
minHeight: 720,
backgroundColor: "#0a0a0a",
titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default",
trafficLightPosition:
process.platform === "darwin" ? { x: 16, y: 16 } : undefined,
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false,
},
});
const rendererUrl = process.env.ELECTRON_RENDERER_URL;
if (rendererUrl) {
void window.loadURL(rendererUrl);
} else {
void window.loadFile(join(__dirname, "../renderer/index.html"));
}
console.log("[studio] window created");
console.log("SF Studio ready");
return window;
}
app.whenReady().then(() => {
_mainWindow = createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
_mainWindow = createWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});

View file

@ -1,7 +0,0 @@
import type { StudioBridge } from "./index";
declare global {
interface Window {
studio: StudioBridge;
}
}

View file

@ -1,22 +0,0 @@
import { contextBridge } from "electron";
export type StudioStatus = {
connected: boolean;
};
export type StudioBridge = {
onEvent: (callback: (event: unknown) => void) => () => void;
sendCommand: (command: string, args?: Record<string, unknown>) => void;
spawn: () => void;
getStatus: () => Promise<StudioStatus>;
};
const studio: StudioBridge = {
onEvent: (_callback) => () => undefined,
sendCommand: (_command, _args) => undefined,
spawn: () => undefined,
getStatus: () => Promise.resolve({ connected: false }),
};
console.log("[studio] preload loaded");
contextBridge.exposeInMainWorld("studio", studio);

View file

@ -1,17 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SF Studio</title>
<link rel="preload" href="./src/assets/fonts/Inter-Regular.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="./src/assets/fonts/Inter-Medium.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="./src/assets/fonts/Inter-SemiBold.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="./src/assets/fonts/JetBrainsMono-Regular.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="./src/assets/fonts/JetBrainsMono-Medium.woff2" as="font" type="font/woff2" crossorigin />
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.tsx"></script>
</body>
</html>

View file

@ -1,99 +0,0 @@
import { BracketsCurly, Lightning, Palette } from "@phosphor-icons/react";
import { colors, fontSizes, fonts } from "./lib/theme/tokens";
const statusRows = [
{ label: "Shell", value: "electron-vite + React 19", icon: Lightning },
{ label: "Theme", value: colors.accent, icon: Palette },
{ label: "Code", value: fonts.mono, icon: BracketsCurly },
];
export default function App() {
return (
<main className="min-h-screen bg-bg-primary text-text-primary">
<div className="mx-auto flex min-h-screen max-w-6xl flex-col justify-center px-10 py-16">
<div className="grid gap-10 lg:grid-cols-[1.2fr_0.8fr]">
<section className="rounded-[28px] border border-border bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.01))] p-10 shadow-[0_24px_80px_rgba(0,0,0,0.35)] backdrop-blur-sm">
<div className="mb-8 inline-flex items-center gap-3 rounded-full border border-[color:var(--color-accent-muted)] bg-[color:var(--color-accent-muted)] px-4 py-2 text-xs font-medium uppercase tracking-[0.28em] text-accent">
<span className="h-2 w-2 rounded-full bg-accent shadow-[0_0_18px_rgba(212,160,78,0.7)]" />
Studio bootstrap
</div>
<h1 className="max-w-3xl text-[clamp(3.4rem,9vw,6.8rem)] font-semibold leading-[0.92] tracking-[-0.06em] text-balance text-text-primary">
SF Studio ships with a dark shell that actually feels deliberate.
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-text-secondary">
Inter drives the interface, JetBrains Mono handles code surfaces,
and the warm amber system accent keeps the palette restrained
instead of drifting into generic app chrome.
</p>
<div className="mt-10 grid gap-4 sm:grid-cols-3">
{statusRows.map(({ label, value, icon: Icon }) => (
<div
key={label}
className="rounded-2xl border border-border bg-bg-secondary/70 p-4"
>
<div className="mb-4 flex items-center justify-between">
<span className="text-xs uppercase tracking-[0.24em] text-text-tertiary">
{label}
</span>
<Icon size={18} weight="duotone" className="text-accent" />
</div>
<p className="text-sm font-medium text-text-primary">
{value}
</p>
</div>
))}
</div>
</section>
<aside className="space-y-4 rounded-[28px] border border-border bg-bg-secondary/80 p-8 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
<div>
<p className="text-xs uppercase tracking-[0.24em] text-text-tertiary">
Typography proof
</p>
<p className="mt-3 text-2xl font-semibold text-text-primary">
Inter 600 for hierarchy
</p>
<p className="mt-2 text-sm leading-7 text-text-secondary">
The first task only validates the shell and token system.
Three-column layout and primitives land in T02.
</p>
</div>
<div className="rounded-2xl border border-[color:var(--color-accent-muted)] bg-[#120f09] p-5">
<p className="text-xs uppercase tracking-[0.24em] text-accent/80">
Code surface
</p>
<pre className="mt-4 overflow-x-auto rounded-xl border border-border bg-black/30 p-4 text-sm leading-7 text-[#f5deb3]">
<code>{`const studio = await window.studio.getStatus();\nif (!studio.connected) {\n console.log('Renderer scaffold ready');\n}`}</code>
</pre>
</div>
<dl className="grid grid-cols-3 gap-3 text-sm">
<div className="rounded-2xl border border-border bg-bg-primary p-4">
<dt className="text-text-tertiary">Accent</dt>
<dd className="mt-2 font-medium text-accent">
{colors.accent}
</dd>
</div>
<div className="rounded-2xl border border-border bg-bg-primary p-4">
<dt className="text-text-tertiary">UI font</dt>
<dd className="mt-2 font-medium text-text-primary">
{fontSizes.body}
</dd>
</div>
<div className="rounded-2xl border border-border bg-bg-primary p-4">
<dt className="text-text-tertiary">Mono</dt>
<dd className="mt-2 font-mono text-[13px] text-text-primary">
{fonts.mono}
</dd>
</div>
</dl>
</aside>
</div>
</div>
</main>
);
}

View file

@ -1,28 +0,0 @@
export const colors = {
bgPrimary: "#0a0a0a",
bgSecondary: "#111111",
bgTertiary: "#1a1a1a",
bgHover: "#222222",
border: "#262626",
borderActive: "#333333",
textPrimary: "#e5e5e5",
textSecondary: "#a3a3a3",
textTertiary: "#737373",
accent: "#d4a04e",
accentHover: "#e0b366",
accentMuted: "rgba(212, 160, 78, 0.15)",
} as const;
export const fonts = {
sans: "'Inter', system-ui, sans-serif",
mono: "'JetBrains Mono', ui-monospace, monospace",
} as const;
export const fontSizes = {
hero: "4.75rem",
display: "3.5rem",
title: "2rem",
bodyLg: "1.125rem",
body: "0.9375rem",
caption: "0.75rem",
} as const;

View file

@ -1,16 +0,0 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./styles/index.css";
const rootElement = document.getElementById("root");
if (!rootElement) {
throw new Error("Root element #root was not found");
}
createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>,
);

View file

@ -1,129 +0,0 @@
@import "tailwindcss";
@font-face {
font-family: "Inter";
src: url("../assets/fonts/Inter-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Inter";
src: url("../assets/fonts/Inter-Medium.woff2") format("woff2");
font-weight: 500;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Inter";
src: url("../assets/fonts/Inter-SemiBold.woff2") format("woff2");
font-weight: 600;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "JetBrains Mono";
src: url("../assets/fonts/JetBrainsMono-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "JetBrains Mono";
src: url("../assets/fonts/JetBrainsMono-Medium.woff2") format("woff2");
font-weight: 500;
font-style: normal;
font-display: block;
}
@theme {
--color-bg-primary: #0a0a0a;
--color-bg-secondary: #111111;
--color-bg-tertiary: #1a1a1a;
--color-bg-hover: #222222;
--color-border: #262626;
--color-border-active: #333333;
--color-text-primary: #e5e5e5;
--color-text-secondary: #a3a3a3;
--color-text-tertiary: #737373;
--color-accent: #d4a04e;
--color-accent-hover: #e0b366;
--color-accent-muted: rgba(212, 160, 78, 0.15);
--font-sans: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--text-hero: 4.75rem;
--text-display: 3.5rem;
--text-title: 2rem;
--text-body-lg: 1.125rem;
--text-body: 0.9375rem;
--text-caption: 0.75rem;
}
:root {
color-scheme: dark;
background-color: var(--color-bg-primary);
}
* {
box-sizing: border-box;
}
html {
background: var(--color-bg-primary);
}
body {
margin: 0;
min-height: 100vh;
background: var(--color-bg-primary);
color: var(--color-text-primary);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
#root {
min-height: 100vh;
}
code,
pre,
.font-mono {
font-family: var(--font-mono);
}
::selection {
background: rgba(212, 160, 78, 0.28);
color: var(--color-text-primary);
}
* {
scrollbar-width: thin;
scrollbar-color: #3a3125 #111111;
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: #111111;
}
*::-webkit-scrollbar-thumb {
border-radius: 999px;
background: linear-gradient(180deg, #403223 0%, #2d241a 100%);
border: 2px solid #111111;
}
*::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #5b4731 0%, #3c2f21 100%);
}

View file

@ -1,48 +0,0 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "vitest";
const cssPath = new URL(
"../src/renderer/src/styles/index.css",
import.meta.url,
);
const tokensPath = new URL(
"../src/renderer/src/lib/theme/tokens.ts",
import.meta.url,
);
test("theme CSS defines required color tokens and font-display block", async () => {
const css = await readFile(cssPath, "utf8");
for (const token of [
"--color-bg-primary",
"--color-bg-secondary",
"--color-bg-tertiary",
"--color-bg-hover",
"--color-border",
"--color-border-active",
"--color-text-primary",
"--color-text-secondary",
"--color-text-tertiary",
"--color-accent",
"--color-accent-hover",
"--color-accent-muted",
"--font-sans",
"--font-mono",
]) {
assert.match(
css,
new RegExp(token.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")),
);
}
const blockMatches = css.match(/font-display:\s*block;/g) ?? [];
assert.equal(blockMatches.length, 5);
});
test("token module exports key theme primitives", async () => {
const tokensFile = await readFile(tokensPath, "utf8");
assert.match(tokensFile, /accent: '#d4a04e'/);
assert.match(tokensFile, /mono: "'JetBrains Mono'/);
assert.match(tokensFile, /body: '0\.9375rem'/);
});

View file

@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.web.json" }
]
}

View file

@ -1,22 +0,0 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"types": ["node", "electron-vite/node"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": [
"electron.vite.config.ts",
"src/main/**/*.ts",
"src/preload/**/*.ts",
"src/preload/**/*.d.ts"
]
}

View file

@ -1,22 +0,0 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"types": ["node"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/renderer/src/*"]
}
},
"include": ["src/renderer/src/**/*", "src/preload/index.d.ts"]
}

View file

@ -12,12 +12,39 @@
* npx vitest run --changed # only tests affected by recent changes
*/
import { resolve } from "node:path";
import { existsSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
const __dirname = import.meta.dirname;
export default defineConfig({
plugins: [
{
name: "sf-resource-js-extension-resolver",
enforce: "pre",
resolveId(source, importer) {
if (
!source.endsWith(".ts") ||
!source.includes("resources/extensions")
) {
return null;
}
const importerPath = importer?.startsWith("file://")
? fileURLToPath(importer)
: importer;
const tsPath = source.startsWith("/")
? resolve(__dirname, source.slice(1))
: importerPath
? resolve(dirname(importerPath), source)
: resolve(__dirname, source);
const jsPath = tsPath.replace(/\.ts$/, ".js");
if (!existsSync(tsPath) && existsSync(jsPath)) return jsPath;
return null;
},
},
],
// ── TypeScript / module resolution ─────────────────────────────────────────
// Vitest uses esbuild for TS transform (fast, bundled). We still set up
// NodeNext module resolution and path aliases to match the project's tsconfig.
@ -162,7 +189,6 @@ export default defineConfig({
"packages/rpc-client/src/**/*.test.ts",
"packages/native/src/**/*.test.mjs",
"web/lib/**/*.test.ts",
"studio/test/**/*.test.mjs",
"scripts/*.test.mjs",
],