Add office agent management wizard (#56)

* Add agents

* Agent

* Added agents management

* Polish agent wizard and release blockers.

Finalize the office agent management flow by aligning the gateway fallback behavior, cleaning up UI semantics, and updating tests so the branch is ready to ship.

Made-with: Cursor

---------

Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
Co-authored-by: iamlukethedev <lucas.guilherme@smartwayslfl.com>
This commit is contained in:
Luke The Dev
2026-03-23 18:04:37 -05:00
committed by GitHub
parent 5e7812c352
commit c9789c2148
32 changed files with 1504 additions and 181 deletions
+2 -2
View File
@@ -30,7 +30,7 @@ export type PersonalityBuilderDraft = {
type AgentFilesInput = Record<AgentFileName, { content: string; exists: boolean }>;
const createEmptyDraft = (): PersonalityBuilderDraft => ({
export const createEmptyPersonalityDraft = (): PersonalityBuilderDraft => ({
identity: {
name: "",
creature: "",
@@ -253,7 +253,7 @@ const serializeSoulMarkdown = (draft: PersonalityBuilderDraft["soul"]) => {
};
export const parsePersonalityFiles = (files: AgentFilesInput): PersonalityBuilderDraft => {
const draft = createEmptyDraft();
const draft = createEmptyPersonalityDraft();
const identity = parseLabelMap(files["IDENTITY.md"].content);
const identityName = readFirst(identity, ["name"]);
+1
View File
@@ -479,6 +479,7 @@ const NON_RETRYABLE_CONNECT_ERROR_CODES = new Set([
"studio.gateway_token_missing",
"studio.gateway_url_invalid",
"studio.settings_load_failed",
"studio.upstream_error",
]);
const isNonRetryableConnectErrorCode = (code: string | null): boolean => {
+22
View File
@@ -8,6 +8,18 @@ const parseHostname = (gatewayUrl: string): string | null => {
}
};
const LOCAL_HOST_SUFFIXES = [".local", ".home.arpa", ".lan", ".internal"] as const;
const isPrivateIpv4Address = (hostname: string) => {
const match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(hostname);
if (!match) return false;
const [first, second] = [Number.parseInt(match[1], 10), Number.parseInt(match[2], 10)];
if (first === 10) return true;
if (first === 172 && second >= 16 && second <= 31) return true;
if (first === 192 && second === 168) return true;
return false;
};
export const isLocalGatewayUrl = (gatewayUrl: string): boolean => {
const hostname = parseHostname(gatewayUrl);
if (!hostname) return false;
@@ -20,3 +32,13 @@ export const isLocalGatewayUrl = (gatewayUrl: string): boolean => {
);
};
export const isLikelyLocalGatewayUrl = (gatewayUrl: string): boolean => {
if (isLocalGatewayUrl(gatewayUrl)) return true;
const hostname = parseHostname(gatewayUrl);
if (!hostname) return false;
const normalized = hostname.trim().toLowerCase();
if (isPrivateIpv4Address(normalized)) return true;
if (normalized.endsWith(".ts.net")) return true;
return LOCAL_HOST_SUFFIXES.some((suffix) => normalized.endsWith(suffix));
};
+27
View File
@@ -0,0 +1,27 @@
const TEMP_SKILL_AGENT_RE = /^Skill (Installer|Remover) (\d{13})$/;
const STALE_TEMP_SKILL_AGENT_MS = 15 * 60 * 1000;
export const isTemporarySkillAgentName = (value: string | null | undefined): boolean => {
const trimmed = value?.trim() ?? "";
return TEMP_SKILL_AGENT_RE.test(trimmed);
};
export const resolveTemporarySkillAgentCreatedAt = (
value: string | null | undefined
): number | null => {
const trimmed = value?.trim() ?? "";
const match = TEMP_SKILL_AGENT_RE.exec(trimmed);
if (!match) return null;
const timestamp = Number.parseInt(match[2], 10);
return Number.isFinite(timestamp) ? timestamp : null;
};
export const isStaleTemporarySkillAgentName = (
value: string | null | undefined,
nowMs: number = Date.now()
): boolean => {
const createdAt = resolveTemporarySkillAgentCreatedAt(value);
if (createdAt === null) return false;
return nowMs - createdAt >= STALE_TEMP_SKILL_AGENT_MS;
};