feat(FN-3471): apply tokenized sizing and focus styles to component CSS

Merged CSS changes that apply tokenized sizing and focus styles to InsightsView and DesktopModeChooser components, with the majority of changes in InsightsView.css.

Fusion-Task-Id: FN-3471
This commit is contained in:
Fusion
2026-05-05 00:15:52 -07:00
committed by gsxdsm
parent cdb64e008d
commit 43dd04860c
10 changed files with 128 additions and 50 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix Droid CLI auth/status probing to resolve the effective binary path from plugin settings (including custom `droidBinaryPath`) so Settings no longer reports false "not installed" states when Droid is configured at a non-default path.

View File

@@ -308,7 +308,7 @@ Fusion supports OAuth-based authentication for AI providers configured via **Set
- **Anthropic (Claude)** — Authenticates via the same Settings/onboarding OAuth flow as other OAuth providers
- **OpenAI Codex** — Authenticates via Settings OAuth flow with secure state validation
- **Factory AI — via Droid CLI** *(optional)* — requires local `droid` install + `droid auth login`, then enable the provider in **Settings → Authentication** and restart Fusion
- **Factory AI — via Droid CLI** *(optional)* — requires local Droid CLI install + `droid auth login`; detection follows the effective runtime binary path (default `droid`, or plugin `droidBinaryPath` when configured), then enable in **Settings → Authentication** and restart Fusion
- **llama.cpp — via HTTP server** *(optional)* — configure your llama.cpp server URL (default `http://127.0.0.1:8080`) and optional API key, then enable in **Settings → Authentication**
- **Other providers** — Authenticate via API key entry in Settings (including Google/Gemini API key, Google Generative AI, Vertex, and Cloud Code aliases)

View File

@@ -269,10 +269,12 @@ To revoke/reset access, choose the behavior you want:
When the published CLI bundle includes the vendored `@fusion/droid-cli` extension, users can enable **Factory AI — via Droid CLI** in **Settings → Authentication**.
Requirements:
- `droid` binary installed and available on `PATH`
- a working Droid CLI binary (`droid` on `PATH` by default, or a custom plugin `droidBinaryPath`)
- successful local login (`droid auth login`)
- Fusion restart after toggling the provider on (to reload extensions)
Authentication status checks in **Settings → Authentication** use the same effective Droid binary path as the Droid runtime plugin, so custom binary-path installs are detected correctly.
---
## `fn serve`

View File

@@ -25,7 +25,7 @@
.insights-view-title h2 {
margin: 0;
font-size: 18px;
font-size: 1.125rem;
font-weight: 600;
display: flex;
align-items: center;
@@ -33,7 +33,7 @@
}
.insights-view-count {
font-size: 13px;
font-size: 0.8125rem;
color: var(--text-muted);
font-weight: normal;
}
@@ -68,7 +68,7 @@
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-md);
font-size: 13px;
font-size: 0.8125rem;
}
.insights-status-message--success {
@@ -97,7 +97,7 @@
border: 1px solid color-mix(in srgb, var(--color-error) 30%, transparent);
border-radius: var(--radius-md);
color: var(--color-error);
font-size: 13px;
font-size: 0.8125rem;
flex-shrink: 0;
}
@@ -108,7 +108,7 @@
}
.insights-run-status {
font-size: 12px;
font-size: 0.75rem;
color: var(--text-muted);
}
@@ -153,14 +153,14 @@
.insights-empty h3 {
margin: 0;
font-size: 18px;
font-size: 1.125rem;
color: var(--text);
}
.insights-empty p {
color: var(--text-muted);
margin: 0;
max-width: 400px;
max-width: calc(var(--space-2xl) * 12 + var(--space-lg));
}
/* Two-pane body */
@@ -173,7 +173,7 @@
/* Sidebar (categories) */
.insights-sidebar {
width: 240px;
width: calc(var(--space-2xl) * 7 + var(--space-lg));
flex-shrink: 0;
border-right: 1px solid var(--border);
background: var(--surface);
@@ -187,7 +187,7 @@
padding: var(--space-sm);
display: flex;
flex-direction: column;
gap: 2px;
gap: var(--space-xs);
}
.insights-category-item {
@@ -201,7 +201,7 @@
border-radius: var(--radius-md);
color: var(--text);
font: inherit;
font-size: 13px;
font-size: 0.8125rem;
text-align: left;
cursor: pointer;
transition: background var(--transition-fast), border-color var(--transition-fast);
@@ -233,10 +233,10 @@
.insights-category-count {
flex-shrink: 0;
font-size: 12px;
font-size: 0.75rem;
color: var(--text-muted);
background: var(--surface-elevated);
padding: 2px 8px;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-pill);
border: 1px solid var(--border);
}
@@ -281,7 +281,7 @@
.insights-section-title h3 {
margin: 0;
font-size: 15px;
font-size: 0.9375rem;
font-weight: 600;
}
@@ -290,10 +290,10 @@
}
.insights-section-count {
font-size: 12px;
font-size: 0.75rem;
color: var(--text-muted);
background: var(--surface-elevated);
padding: 2px 8px;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-pill);
border: 1px solid var(--border);
}
@@ -317,7 +317,7 @@
padding: var(--space-md);
background: var(--surface);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-left: var(--space-xs) solid var(--accent);
border-radius: var(--radius-md);
transition: border-color var(--transition-fast);
}
@@ -335,7 +335,7 @@
.insight-item-title {
margin: 0;
font-size: 14px;
font-size: 0.875rem;
font-weight: 600;
color: var(--text);
line-height: 1.4;
@@ -353,8 +353,8 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
width: calc(var(--space-lg) * 2);
height: calc(var(--space-lg) * 2);
padding: 0;
background: transparent;
border: none;
@@ -369,6 +369,11 @@
color: var(--accent);
}
.insight-item-action-btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.insight-item-action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
@@ -376,7 +381,7 @@
.insight-item-content {
margin: var(--space-sm) 0 0;
font-size: 13px;
font-size: 0.8125rem;
color: var(--text-muted);
line-height: 1.5;
white-space: pre-wrap;
@@ -388,12 +393,12 @@
align-items: center;
gap: var(--space-md);
margin-top: var(--space-sm);
font-size: 12px;
font-size: 0.75rem;
color: var(--text-muted);
}
.insight-item-status {
padding: 2px 6px;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-sm);
text-transform: capitalize;
font-weight: 500;
@@ -441,7 +446,7 @@
.insight-item-date {
display: flex;
align-items: center;
gap: 4px;
gap: var(--space-xs);
}
/* Spin animation */
@@ -468,7 +473,7 @@
}
.insights-view-title h2 {
font-size: 16px;
font-size: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -550,27 +555,27 @@
}
.insight-item-action-btn {
width: 40px;
height: 40px;
width: calc(var(--space-2xl) + var(--space-sm));
height: calc(var(--space-2xl) + var(--space-sm));
}
.insights-view-actions .btn {
min-height: 36px;
min-height: calc(var(--space-2xl) + var(--space-xs));
}
.insights-show-archived-toggle {
max-width: 180px;
max-width: calc(var(--space-2xl) * 5 + var(--space-md));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.insights-view-close {
min-height: 36px;
min-width: 36px;
min-height: calc(var(--space-2xl) + var(--space-xs));
min-width: calc(var(--space-2xl) + var(--space-xs));
}
.insights-view-count {
font-size: 12px;
font-size: 0.75rem;
}
}

View File

@@ -808,6 +808,19 @@ describe("GET /providers/claude-cli/status", () => {
describe("Droid CLI auth routes", () => {
let store: TaskStore;
function setDroidPluginSettings(settings: Record<string, unknown>) {
(store as TaskStore & {
getPluginStore: () => { getPlugin: (id: string) => Promise<{ settings: Record<string, unknown> }> };
}).getPluginStore = vi.fn().mockReturnValue({
getPlugin: vi.fn().mockImplementation(async (id: string) => {
if (id !== "fusion-plugin-droid-runtime") {
throw new Error("not found");
}
return { settings };
}),
});
}
beforeEach(() => {
store = createMockStore({
updateGlobalSettings: vi.fn().mockResolvedValue({ useDroidCli: true }),
@@ -826,6 +839,7 @@ describe("Droid CLI auth routes", () => {
}
it("enables Droid CLI when binary is available", async () => {
setDroidPluginSettings({ droidBinaryPath: "/opt/custom-droid" });
const probeSpy = vi.spyOn(droidCliProbeModule, "probeDroidCli").mockResolvedValue({
available: true,
version: "droid 1.0.0",
@@ -839,6 +853,7 @@ describe("Droid CLI auth routes", () => {
expect(res.status).toBe(200);
expect(res.body).toEqual({ enabled: true, restartRequired: false });
expect(store.updateGlobalSettings).toHaveBeenCalledWith({ useDroidCli: true });
expect(probeSpy).toHaveBeenCalledWith({ settings: { droidBinaryPath: "/opt/custom-droid" } });
probeSpy.mockRestore();
});
@@ -899,7 +914,8 @@ describe("Droid CLI auth routes", () => {
});
it("returns binary + toggle + extension diagnostics and computed readiness", async () => {
vi.spyOn(droidCliProbeModule, "probeDroidCli").mockResolvedValue({
setDroidPluginSettings({ droidBinaryPath: "/opt/custom-droid" });
const probeSpy = vi.spyOn(droidCliProbeModule, "probeDroidCli").mockResolvedValue({
available: true,
version: "droid 1.0.0",
probeDurationMs: 10,
@@ -921,6 +937,7 @@ describe("Droid CLI auth routes", () => {
expect(res.body.ready).toBe(true);
expect(res.body.binary).toMatchObject({ available: true, version: "droid 1.0.0" });
expect(res.body.extension).toMatchObject({ status: "ok" });
expect(probeSpy).toHaveBeenCalledWith({ settings: { droidBinaryPath: "/opt/custom-droid" } });
});
it("returns ready false when binary unavailable", async () => {
@@ -952,7 +969,8 @@ describe("Droid CLI auth routes", () => {
});
it("GET /auth/status includes droid-cli provider with cli type", async () => {
vi.spyOn(droidCliProbeModule, "probeDroidCli").mockResolvedValue({
setDroidPluginSettings({ droidBinaryPath: "/opt/custom-droid" });
const probeSpy = vi.spyOn(droidCliProbeModule, "probeDroidCli").mockResolvedValue({
available: true,
version: "droid 1.0.0",
probeDurationMs: 10,
@@ -974,6 +992,7 @@ describe("Droid CLI auth routes", () => {
}),
]),
);
expect(probeSpy).toHaveBeenCalledWith({ settings: { droidBinaryPath: "/opt/custom-droid" } });
});
it("GET /auth/status marks droid-cli unauthenticated when extension status is not ok", async () => {

View File

@@ -2,6 +2,6 @@ import { probeDroidBinary } from "@fusion-plugin-examples/droid-runtime/probe";
export type DroidCliBinaryStatus = Awaited<ReturnType<typeof probeDroidBinary>>;
export async function probeDroidCli(options: { timeoutMs?: number } = {}): Promise<DroidCliBinaryStatus> {
return probeDroidBinary({ timeoutMs: options.timeoutMs });
export async function probeDroidCli(options: { timeoutMs?: number; settings?: Record<string, unknown> } = {}): Promise<DroidCliBinaryStatus> {
return probeDroidBinary({ timeoutMs: options.timeoutMs, settings: options.settings });
}

View File

@@ -1,3 +1,4 @@
import type { Request } from "express";
import { isGhAvailable, isGhAuthenticated } from "@fusion/core";
import { probeClaudeCli } from "../claude-cli-probe.js";
import { probeDroidCli } from "../droid-cli-probe.js";
@@ -9,7 +10,7 @@ import type { AuthStorageLike } from "../routes.js";
import type { ApiRouteRegistrar } from "./types.js";
export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
const { router, options, store, rethrowAsApiError } = ctx;
const { router, options, store, getScopedStore, rethrowAsApiError } = ctx;
const authStorage = options?.authStorage;
// Use injected AuthStorage or fail gracefully if not provided.
@@ -164,6 +165,23 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
};
}
async function probeDroidCliWithEffectiveBinary(req?: Request) {
let pluginSettings: Record<string, unknown> | undefined;
if (req) {
try {
const scopedStore = await getScopedStore(req);
const plugin = await scopedStore.getPluginStore().getPlugin("fusion-plugin-droid-runtime");
if (plugin && typeof plugin.settings === "object" && plugin.settings !== null) {
pluginSettings = plugin.settings as Record<string, unknown>;
}
} catch {
// Missing/unreadable plugin settings: fall back to default droid binary resolution.
}
}
return probeDroidCli({ settings: pluginSettings });
}
function appendManualCodeHint(
instructions: string | undefined,
providerId: string,
@@ -195,7 +213,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
* ghCli: { available: boolean, authenticated: boolean }
* }
*/
router.get("/auth/status", async (_req, res) => {
router.get("/auth/status", async (req, res) => {
try {
const storage = getAuthStorage();
storage.reload();
@@ -274,7 +292,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
// Unreadable settings — fall through with enabled=false
}
const droidExtension = options?.getDroidCliExtensionStatus?.() ?? null;
const droidBinary = await probeDroidCli();
const droidBinary = await probeDroidCliWithEffectiveBinary(req);
const droidExtensionOk = droidExtension === null || droidExtension.status === "ok";
providers.push({
id: "droid-cli",
@@ -422,7 +440,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
}
if (enabled) {
const binary = await probeDroidCli();
const binary = await probeDroidCliWithEffectiveBinary(req);
if (!binary.available) {
throw new ApiError(
400,
@@ -511,9 +529,9 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
}
});
router.get("/providers/droid-cli/status", async (_req, res) => {
router.get("/providers/droid-cli/status", async (req, res) => {
try {
const binary = await probeDroidCli();
const binary = await probeDroidCliWithEffectiveBinary(req);
let enabled = false;
if (store) {
try {

View File

@@ -1,5 +1,5 @@
.desktop-mode-chooser {
max-width: 640px;
max-width: calc(var(--space-2xl) * 20);
margin: 0 auto;
padding: var(--space-2xl);
display: flex;
@@ -13,7 +13,7 @@
.desktop-mode-chooser__title {
margin: 0;
font-size: 1.5rem;
font-size: calc(var(--space-lg) + var(--space-sm));
line-height: 1.2;
color: var(--text);
}

View File

@@ -8,7 +8,7 @@ vi.mock("node:child_process", () => ({
spawn: spawnMock,
}));
import { probeDroidBinary } from "../probe.js";
import { probeDroidBinary, resolveDroidBinaryPath } from "../probe.js";
describe("probeDroidBinary", () => {
beforeEach(() => {
@@ -26,7 +26,7 @@ describe("probeDroidBinary", () => {
const result = await probeDroidBinary({ timeoutMs: 10 });
expect(result.available).toBe(false);
expect(result.reason).toContain("not found");
expect(result.reason).toContain("Binary not found or not executable");
});
it("returns available and version on success", async () => {
@@ -46,6 +46,19 @@ describe("probeDroidBinary", () => {
expect(result.version).toBe("droid 1.2.3");
});
it("uses binary path from plugin settings", async () => {
spawnMock.mockImplementationOnce(() => {
const proc = new EventEmitter() as any;
proc.stdout = new PassThrough();
proc.stderr = new PassThrough();
queueMicrotask(() => proc.emit("close", 1));
return proc;
});
await probeDroidBinary({ settings: { droidBinaryPath: " /custom/from-settings " } });
expect(spawnMock).toHaveBeenCalledWith("/custom/from-settings", ["--version"], expect.anything());
});
it("uses custom binary path", async () => {
spawnMock.mockImplementationOnce(() => {
const proc = new EventEmitter() as any;
@@ -58,4 +71,10 @@ describe("probeDroidBinary", () => {
await probeDroidBinary({ binaryPath: "/custom/droid" });
expect(spawnMock).toHaveBeenCalledWith("/custom/droid", ["--version"], expect.anything());
});
it("falls back to droid when plugin setting is missing or blank", () => {
expect(resolveDroidBinaryPath()).toBe("droid");
expect(resolveDroidBinaryPath({})).toBe("droid");
expect(resolveDroidBinaryPath({ droidBinaryPath: " " })).toBe("droid");
});
});

View File

@@ -9,6 +9,13 @@ export interface DroidBinaryStatus {
probeDurationMs: number;
}
export function resolveDroidBinaryPath(settings?: Record<string, unknown>): string {
if (typeof settings?.droidBinaryPath === "string" && settings.droidBinaryPath.trim().length > 0) {
return settings.droidBinaryPath.trim();
}
return "droid";
}
async function run(binary: string, args: string[], timeoutMs = 2000): Promise<{ code: number | null; stdout: string; stderr: string }> {
return new Promise((resolve) => {
const child = spawn(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
@@ -33,9 +40,9 @@ async function run(binary: string, args: string[], timeoutMs = 2000): Promise<{
});
}
export async function probeDroidBinary(options?: { binaryPath?: string; timeoutMs?: number }): Promise<DroidBinaryStatus> {
export async function probeDroidBinary(options?: { binaryPath?: string; settings?: Record<string, unknown>; timeoutMs?: number }): Promise<DroidBinaryStatus> {
const startedAt = Date.now();
const binaryPath = options?.binaryPath?.trim() || "droid";
const binaryPath = options?.binaryPath?.trim() || resolveDroidBinaryPath(options?.settings);
const timeoutMs = options?.timeoutMs ?? 2000;
const versionRun = await run(binaryPath, ["--version"], timeoutMs);
@@ -43,7 +50,10 @@ export async function probeDroidBinary(options?: { binaryPath?: string; timeoutM
return {
available: false,
binaryPath,
reason: versionRun.code === 124 ? `Probe timed out after ${timeoutMs}ms` : "`droid` not found on PATH",
reason:
versionRun.code === 124
? `Probe timed out after ${timeoutMs}ms`
: `Binary not found or not executable: ${binaryPath}`,
probeDurationMs: Date.now() - startedAt,
};
}