>
diff --git a/src/renderer/components/company-onboarding/OnboardingWizard.tsx b/src/renderer/components/company-onboarding/OnboardingWizard.tsx
index cce471dc..b731c77f 100644
--- a/src/renderer/components/company-onboarding/OnboardingWizard.tsx
+++ b/src/renderer/components/company-onboarding/OnboardingWizard.tsx
@@ -8,9 +8,9 @@ import type {
import type { AppLocale } from "@shared/locale";
import { useT } from "../../i18n";
import { unwrap } from "../../lib/desktop";
-import type { OnboardingStep } from "./OnboardingHero";
+import type { OnboardingStep, OnboardingStepName } from "./OnboardingHero";
import { WelcomeStep } from "./WelcomeStep";
-import { OnboardingStepPanel } from "./OnboardingStepPanel";
+import { OnboardingStepPanel, type OnboardingTabCompletion } from "./OnboardingStepPanel";
import { useConnectorCallbacks } from "./useConnectorCallbacks";
import { useConnectorModelSelection } from "./useConnectorModelSelection";
import {
@@ -100,6 +100,7 @@ export function CompanyOnboarding({
selectedConnectorEnabledModels,
totalEnabledModels,
toggleConnectorModel,
+ setAllConnectorModels,
} = useConnectorModelSelection(connectors, currentLocale, t, setError);
const pickDirectory = useCallback(async (): Promise
=> {
@@ -221,6 +222,8 @@ export function CompanyOnboarding({
selectedConnectorEnabledModels,
onToggleConnectorModel: (connectorId, modelId) =>
void toggleConnectorModel(connectorId, modelId),
+ onSetAllConnectorModels: (connectorId, enabled) =>
+ void setAllConnectorModels(connectorId, enabled),
}),
[
connectors,
@@ -239,12 +242,43 @@ export function CompanyOnboarding({
totalEnabledModels,
selectedConnectorEnabledModels,
toggleConnectorModel,
+ setAllConnectorModels,
],
);
const isWelcome = step === "welcome";
const heroStep: OnboardingStep = step as OnboardingStep;
+ // Completion flags drive the tab ticks. Each rule mirrors the
+ // validation that the in-step "Continue" button enforces, so a user
+ // who navigates tabs freely cannot land on "mission" with a green
+ // "company" tick unless company data is actually valid. `launch`
+ // does not get a tab (it has no independent field; it just consumes
+ // the others).
+ const navigateToStep = useCallback((target: OnboardingStepName) => {
+ setError(null);
+ setStep(target);
+ }, []);
+
+ const tabCompletion: OnboardingTabCompletion = useMemo(
+ () => ({
+ nodecheck: Boolean(nodeStatus?.installed),
+ providers: hasAtLeastOneReady,
+ company: companyValues.companyName.trim().length > 0 && selectedConnectorId !== "",
+ modelpool: totalEnabledModels > 0,
+ executionmode: true, // has a default ("ai_ceo")
+ mission: missionValues.goalTitle.trim().length > 0,
+ }),
+ [
+ nodeStatus?.installed,
+ hasAtLeastOneReady,
+ companyValues.companyName,
+ selectedConnectorId,
+ totalEnabledModels,
+ missionValues.goalTitle,
+ ],
+ );
+
return (
{
setError(null);
setStep("providers");
diff --git a/src/renderer/components/company-onboarding/useConnectorModelSelection.ts b/src/renderer/components/company-onboarding/useConnectorModelSelection.ts
index 9a042cb8..04dbbee3 100644
--- a/src/renderer/components/company-onboarding/useConnectorModelSelection.ts
+++ b/src/renderer/components/company-onboarding/useConnectorModelSelection.ts
@@ -54,6 +54,7 @@ interface UseConnectorModelSelectionResult {
totalEnabledModels: number;
selectedConnectorEnabledModels: number;
toggleConnectorModel: (connectorId: ConnectorId, modelId: string) => Promise;
+ setAllConnectorModels: (connectorId: ConnectorId, enabled: boolean) => Promise;
}
export function useConnectorModelSelection(
@@ -265,6 +266,42 @@ export function useConnectorModelSelection(
}
}, [connectorModelsById, currentLocale, setError, t]);
+ /**
+ * Bulk-flip every model of one connector on or off. Uses the same
+ * single-call `setConnectorDisabledModels` IPC as the individual
+ * toggle so the UI and the DB agree after one round-trip.
+ *
+ * Disabling-all is allowed even if it zeroes the global enabled
+ * count — the continue gate at the wizard level catches that and
+ * blocks the next step, matching the single-toggle behavior.
+ */
+ const setAllConnectorModels = useCallback(
+ async (connectorId: ConnectorId, enabled: boolean) => {
+ const connectorModels = connectorModelsById[connectorId];
+ if (!connectorModels || connectorModels.length === 0) return;
+
+ const updatedModels = connectorModels.map((model) => ({ ...model, enabled }));
+ const previousModels = connectorModelsById;
+ setConnectorModelsById({ ...connectorModelsById, [connectorId]: updatedModels });
+ setError(null);
+
+ try {
+ unwrap(
+ await window.agentCompany.setConnectorDisabledModels({
+ connectorId,
+ disabledModels: enabled
+ ? []
+ : updatedModels.map((model) => ({ id: model.id, label: model.label })),
+ }),
+ );
+ } catch (error) {
+ setConnectorModelsById(previousModels);
+ setError(error instanceof Error ? error.message : t("onboarding.unableToBootstrap"));
+ }
+ },
+ [connectorModelsById, setError, t],
+ );
+
return {
executionReadyConnectors,
hasAtLeastOneReady,
@@ -280,5 +317,6 @@ export function useConnectorModelSelection(
totalEnabledModels,
selectedConnectorEnabledModels,
toggleConnectorModel,
+ setAllConnectorModels,
};
}
diff --git a/src/renderer/components/execution/ModelManagementShared.tsx b/src/renderer/components/execution/ModelManagementShared.tsx
index a06f2810..229c19fe 100644
--- a/src/renderer/components/execution/ModelManagementShared.tsx
+++ b/src/renderer/components/execution/ModelManagementShared.tsx
@@ -30,7 +30,7 @@ export function StatusBadge({ status }: { status: ConnectorStatus }) {
const t = useT();
if (status === "ready") {
return (
-
+
{t("models.ready")}
@@ -38,7 +38,7 @@ export function StatusBadge({ status }: { status: ConnectorStatus }) {
}
if (status === "not_installed") {
return (
-
+
{t("models.notInstalled")}
@@ -46,7 +46,7 @@ export function StatusBadge({ status }: { status: ConnectorStatus }) {
}
if (status === "auth_required") {
return (
-
+
{t("models.authRequired")}
@@ -54,14 +54,14 @@ export function StatusBadge({ status }: { status: ConnectorStatus }) {
}
if (status === "detected" || (status as string) === "checking") {
return (
-
+
{t("models.checking")}
);
}
return (
-
+
{status.replaceAll("_", " ")}
@@ -153,12 +153,8 @@ export function ModelToggleList({
);
}
- const enabledCount = models.filter((m) => m.enabled).length;
return (
-
- {t("models.label")} ({t("models.enabledCount").replace("{enabled}", String(enabledCount)).replace("{total}", String(models.length))})
-
{models.map((m) => (
{agent.role}}
{formatTime(comment.createdAt)}
-
{comment.body}
+
+
+
)
})}
diff --git a/src/renderer/components/ui/ActionButton.tsx b/src/renderer/components/ui/ActionButton.tsx
index f3525619..9075666d 100644
--- a/src/renderer/components/ui/ActionButton.tsx
+++ b/src/renderer/components/ui/ActionButton.tsx
@@ -39,7 +39,7 @@ export function ActionButton({
type={type}
aria-label={iconOnly ? (ariaLabel ?? label) : ariaLabel}
title={iconOnly ? label : undefined}
- className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 inline-flex items-center rounded-lg border text-[12px] font-bold transition-all duration-150 active:scale-[0.97] active:translate-y-px ${sizing} ${toneClass} ${isDisabled ? "pointer-events-none opacity-60" : ""} ${loading ? "scale-[0.98]" : ""}`}
+ className={`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 inline-flex shrink-0 items-center whitespace-nowrap rounded-lg border text-[12px] font-bold transition-all duration-150 active:scale-[0.97] active:translate-y-px ${sizing} ${toneClass} ${isDisabled ? "pointer-events-none opacity-60" : ""} ${loading ? "scale-[0.98]" : ""}`}
onClick={onClick}
disabled={isDisabled}
aria-busy={loading}
@@ -49,7 +49,7 @@ export function ActionButton({
) : Icon ? (
) : null}
- {iconOnly ? null :
{label}}
+ {iconOnly ? null :
{label}}
);
}
diff --git a/src/renderer/components/ui/MarkdownBody.tsx b/src/renderer/components/ui/MarkdownBody.tsx
new file mode 100644
index 00000000..c7e5ac8c
--- /dev/null
+++ b/src/renderer/components/ui/MarkdownBody.tsx
@@ -0,0 +1,82 @@
+import { memo } from "react";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+
+/**
+ * Read-only markdown renderer used by message bubbles, comment bodies, and
+ * other agent-authored text. AI agents consistently emit GFM flavour (bold,
+ * bullet lists, numbered lists, inline code, fenced blocks, tables), so we
+ * ship those plus strikethrough and task lists.
+ *
+ * Security: `react-markdown` disables HTML by default. We do not pass the
+ * `rehype-raw` plugin, so any `