This commit is contained in:
ZacharyZhang-NY committed 2026-03-19 16:04:35 -04:00
1 parent da1fec8f2a
commit b28ed38b8d
116 files changed
+273040 -723

No files matched your search

+1
View File
@@ -32,6 +32,7 @@ oldreference/
!/docs/*.md
*.zip
*.blockmap
*.exe
# Local design scratch files
homepagedesign.html
+3 -9
View File
@@ -1,15 +1,9 @@
{
"extends": "/Users/zacharyzhang/Documents/Github/AgentCompany/electron-builder.yml",
"extends": "C:\\Users\\yang\\Documents\\Github\\AgentCompany\\electron-builder.yml",
"directories": {
"output": "/Users/zacharyzhang/Documents/Github/AgentCompany/build/release"
"output": "C:\\Users\\yang\\Documents\\Github\\AgentCompany\\build\\release"
},
"extraMetadata": {
"version": "0.4.39"
},
"mac": {
"hardenedRuntime": false,
"target": [
"zip"
]
"version": "0.4.47"
}
}
+3 -1
View File
@@ -1,4 +1,4 @@
arm64:
x64:
firstOrDefaultFilePatterns:
- '!**/node_modules/**'
- '!assets{,/**/*}'
@@ -16,3 +16,5 @@ arm64:
- '**/*'
- out/**
- package.json
nsis:
script: "!include \"C:\\Users\\yang\\Documents\\Github\\AgentCompany\\node_modules\\.pnpm\\app-builder-lib@26.8.1_dmg-_62d31da0b96a3ea7b7d51a6bbe3b3b6e\\node_modules\\app-builder-lib\\templates\\nsis\\include\\StdUtils.nsh\"\n!addincludedir \"C:\\Users\\yang\\Documents\\Github\\AgentCompany\\node_modules\\.pnpm\\app-builder-lib@26.8.1_dmg-_62d31da0b96a3ea7b7d51a6bbe3b3b6e\\node_modules\\app-builder-lib\\templates\\nsis\\include\"\n!macro _isUpdated _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"updated\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isUpdated `\"\" isUpdated \"\"`\n\n!macro _isForceRun _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"force-run\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForceRun `\"\" isForceRun \"\"`\n\n!macro _isKeepShortcuts _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"keep-shortcuts\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isKeepShortcuts `\"\" isKeepShortcuts \"\"`\n\n!macro _isNoDesktopShortcut _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"no-desktop-shortcut\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isNoDesktopShortcut `\"\" isNoDesktopShortcut \"\"`\n\n!macro _isDeleteAppData _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"delete-app-data\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isDeleteAppData `\"\" isDeleteAppData \"\"`\n\n!macro _isForAllUsers _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"allusers\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForAllUsers `\"\" isForAllUsers \"\"`\n\n!macro _isForCurrentUser _a _b _t _f\n ${StdUtils.TestParameter} $R9 \"currentuser\"\n StrCmp \"$R9\" \"true\" `${_t}` `${_f}`\n!macroend\n!define isForCurrentUser `\"\" isForCurrentUser \"\"`\n\n!macro addLangs\n !insertmacro MUI_LANGUAGE \"English\"\n !insertmacro MUI_LANGUAGE \"German\"\n !insertmacro MUI_LANGUAGE \"French\"\n !insertmacro MUI_LANGUAGE \"SpanishInternational\"\n !insertmacro MUI_LANGUAGE \"SimpChinese\"\n !insertmacro MUI_LANGUAGE \"TradChinese\"\n !insertmacro MUI_LANGUAGE \"Japanese\"\n !insertmacro MUI_LANGUAGE \"Korean\"\n !insertmacro MUI_LANGUAGE \"Italian\"\n !insertmacro MUI_LANGUAGE \"Dutch\"\n !insertmacro MUI_LANGUAGE \"Danish\"\n !insertmacro MUI_LANGUAGE \"Swedish\"\n !insertmacro MUI_LANGUAGE \"Norwegian\"\n !insertmacro MUI_LANGUAGE \"Finnish\"\n !insertmacro MUI_LANGUAGE \"Russian\"\n !insertmacro MUI_LANGUAGE \"Portuguese\"\n !insertmacro MUI_LANGUAGE \"PortugueseBR\"\n !insertmacro MUI_LANGUAGE \"Polish\"\n !insertmacro MUI_LANGUAGE \"Ukrainian\"\n !insertmacro MUI_LANGUAGE \"Czech\"\n !insertmacro MUI_LANGUAGE \"Slovak\"\n !insertmacro MUI_LANGUAGE \"Hungarian\"\n !insertmacro MUI_LANGUAGE \"Arabic\"\n !insertmacro MUI_LANGUAGE \"Turkish\"\n !insertmacro MUI_LANGUAGE \"Thai\"\n !insertmacro MUI_LANGUAGE \"Vietnamese\"\n!macroend\n\n!addplugindir /x86-unicode \"C:\\Users\\yang\\AppData\\Local\\electron-builder\\Cache\\nsis\\nsis-resources-3.4.1-nsis-resources-3.4.1\\plugins\\x86-unicode\"\n!include \"C:\\Users\\yang\\AppData\\Local\\Temp\\t-CtZr8d\\4-messages.nsh\"\n\n!include \"common.nsh\"\n!include \"extractAppPackage.nsh\"\n\n# https://github.com/electron-userland/electron-builder/issues/3972#issuecomment-505171582\nCRCCheck off\nWindowIcon Off\nAutoCloseWindow True\nRequestExecutionLevel ${REQUEST_EXECUTION_LEVEL}\n\nFunction .onInit\n !ifndef SPLASH_IMAGE\n SetSilent silent\n !endif\n\n !insertmacro check64BitAndSetRegView\nFunctionEnd\n\nFunction .onGUIInit\n InitPluginsDir\n\n !ifdef SPLASH_IMAGE\n File /oname=$PLUGINSDIR\\splash.bmp \"${SPLASH_IMAGE}\"\n BgImage::SetBg $PLUGINSDIR\\splash.bmp\n BgImage::Redraw\n !endif\nFunctionEnd\n\nSection\n !ifdef SPLASH_IMAGE\n HideWindow\n !endif\n\n StrCpy $INSTDIR \"$PLUGINSDIR\\app\"\n !ifdef UNPACK_DIR_NAME\n StrCpy $INSTDIR \"$TEMP\\${UNPACK_DIR_NAME}\"\n !endif\n\n RMDir /r $INSTDIR\n SetOutPath $INSTDIR\n\n !ifdef APP_DIR_64\n !ifdef APP_DIR_ARM64\n !ifdef APP_DIR_32\n ${if} ${IsNativeARM64}\n File /r \"${APP_DIR_ARM64}\\*.*\"\n ${elseif} ${RunningX64}\n File /r \"${APP_DIR_64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_32}\\*.*\"\n ${endIf}\n !else\n ${if} ${IsNativeARM64}\n File /r \"${APP_DIR_ARM64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_64}\\*.*\"\n {endIf}\n !endif\n !else\n !ifdef APP_DIR_32\n ${if} ${RunningX64}\n File /r \"${APP_DIR_64}\\*.*\"\n ${else}\n File /r \"${APP_DIR_32}\\*.*\"\n ${endIf}\n !else\n File /r \"${APP_DIR_64}\\*.*\"\n !endif\n !endif\n !else\n !ifdef APP_DIR_32\n File /r \"${APP_DIR_32}\\*.*\"\n !else\n !insertmacro extractEmbeddedAppPackage\n !endif\n !endif\n\n System::Call 'Kernel32::SetEnvironmentVariable(t, t)i (\"PORTABLE_EXECUTABLE_DIR\", \"$EXEDIR\").r0'\n System::Call Line truncated
-8
View File
@@ -1,8 +0,0 @@
version: 0.4.39
files:
- url: AgentCompany-0.4.39-arm64-mac.zip
sha512: Go1aPQz2rOolb0eES1VpgIZkg3f7wfokDjSaJR01f4CSFdyTobB4ceD2aluOMb+NWw/fbWJTISXkP1aAJuDL1w==
size: 113295149
path: AgentCompany-0.4.39-arm64-mac.zip
sha512: Go1aPQz2rOolb0eES1VpgIZkg3f7wfokDjSaJR01f4CSFdyTobB4ceD2aluOMb+NWw/fbWJTISXkP1aAJuDL1w==
releaseDate: '2026-03-18T20:40:51.335Z'
+8
View File
@@ -0,0 +1,8 @@
version: 0.4.47
files:
- url: AgentCompany-Setup-0.4.47.exe
sha512: E8l/lb31scRylTl9txzxbZpOXzEOz2bcjpbzQSTG8HDko77ywXsglASZ15kqlfU/KBtfVr7T0vYfmoYq6HyCYA==
size: 101573050
path: AgentCompany-Setup-0.4.47.exe
sha512: E8l/lb31scRylTl9txzxbZpOXzEOz2bcjpbzQSTG8HDko77ywXsglASZ15kqlfU/KBtfVr7T0vYfmoYq6HyCYA==
releaseDate: '2026-03-19T19:49:54.761Z'
@@ -0,0 +1,21 @@
Copyright (c) Electron contributors
Copyright (c) 2013-2020 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
owner: ZacharyZhang-NY
repo: AgentCompany
provider: github
updaterCacheDirName: agent-company-updater
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
{"file_format_version": "1.0.0", "ICD": {"library_path": ".\\vk_swiftshader.dll", "api_version": "1.0.5"}}
Binary file not shown.
+1
View File
@@ -19,6 +19,7 @@ win:
icon: assets/icon.ico
target:
- nsis
- portable
linux:
icon: assets/icon.png
target:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-company",
"version": "0.4.39",
"version": "0.4.47",
"private": true,
"type": "module",
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
@@ -59,18 +59,9 @@ export async function testEnvironment(
try {
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
checks.push({
code: "claude_cwd_valid",
level: "info",
message: `Working directory is valid: ${cwd}`,
});
checks.push({ code: "claude_cwd_valid", level: "info", message: `Working directory is valid: ${cwd}` });
} catch (err) {
checks.push({
code: "claude_cwd_invalid",
level: "error",
message: err instanceof Error ? err.message : "Invalid working directory",
detail: cwd,
});
checks.push({ code: "claude_cwd_invalid", level: "error", message: err instanceof Error ? err.message : "Invalid working directory", detail: cwd });
}
const envConfig = parseObject(config.env);
@@ -81,50 +72,23 @@ export async function testEnvironment(
const runtimeEnv = ensurePathInEnv(env);
try {
await ensureCommandResolvable(command, cwd, runtimeEnv);
checks.push({
code: "claude_command_resolvable",
level: "info",
message: `Command is executable: ${command}`,
});
checks.push({ code: "claude_command_resolvable", level: "info", message: `Command is executable: ${command}` });
} catch (err) {
checks.push({
code: "claude_command_unresolvable",
level: "error",
message: err instanceof Error ? err.message : "Command is not executable",
detail: command,
});
checks.push({ code: "claude_command_unresolvable", level: "error", message: err instanceof Error ? err.message : "Command is not executable", detail: command });
}
const configApiKey = env.ANTHROPIC_API_KEY;
if (isNonEmpty(configApiKey)) {
checks.push({
code: "claude_anthropic_api_key_overrides_subscription",
level: "warn",
message:
"ANTHROPIC_API_KEY is set. Claude will use API-key auth instead of subscription credentials.",
detail: "Detected in adapter runtime env.",
hint: "Unset ANTHROPIC_API_KEY if you want subscription-based Claude login behavior.",
});
checks.push({ code: "claude_anthropic_api_key_overrides_subscription", level: "info", message: "ANTHROPIC_API_KEY is set. Claude will use API-key auth." });
} else {
checks.push({
code: "claude_subscription_mode_possible",
level: "info",
message: "ANTHROPIC_API_KEY is not set; subscription-based auth can be used if Claude is logged in.",
});
checks.push({ code: "claude_subscription_mode_possible", level: "info", message: "ANTHROPIC_API_KEY is not set; subscription-based auth can be used if Claude is logged in." });
}
const canRunProbe =
checks.every((check) => check.code !== "claude_cwd_invalid" && check.code !== "claude_command_unresolvable");
if (canRunProbe) {
if (!commandLooksLike(command, "claude")) {
checks.push({
code: "claude_hello_probe_skipped_custom_command",
level: "info",
message: "Skipped hello probe because command is not `claude`.",
detail: command,
hint: "Use the `claude` CLI command to run the automatic login and installation probe.",
});
} else {
// Hello probe: wrapped in try-catch so probe failures never crash the test.
// If the command is resolvable, that is sufficient for "pass" status.
const canRunProbe = checks.every((c) => c.code !== "claude_cwd_invalid" && c.code !== "claude_command_unresolvable");
if (canRunProbe && commandLooksLike(command, "claude")) {
try {
const model = asString(config.model, "").trim();
const effort = asString(config.effort, "").trim();
const chrome = asBoolean(config.chrome, false);
@@ -148,67 +112,38 @@ export async function testEnvironment(
`claude-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
command,
args,
{
cwd,
env,
timeoutSec: 45,
graceSec: 5,
stdin: "Respond with hello.",
onLog: async () => {},
},
{ cwd, env, timeoutSec: 45, graceSec: 5, stdin: "Respond with hello.", onLog: async () => {} },
);
const parsedStream = parseClaudeStreamJson(probe.stdout);
const parsed = parsedStream.resultJson;
const loginMeta = detectClaudeLoginRequired({
parsed,
stdout: probe.stdout,
stderr: probe.stderr,
});
const loginMeta = detectClaudeLoginRequired({ parsed, stdout: probe.stdout, stderr: probe.stderr });
const detail = summarizeProbeDetail(probe.stdout, probe.stderr);
if (probe.timedOut) {
checks.push({
code: "claude_hello_probe_timed_out",
level: "warn",
message: "Claude hello probe timed out.",
hint: "Retry the probe. If this persists, verify Claude can run `Respond with hello` from this directory manually.",
});
checks.push({ code: "claude_hello_probe_timed_out", level: "info", message: "Claude hello probe timed out." });
} else if (loginMeta.requiresLogin) {
checks.push({
code: "claude_hello_probe_auth_required",
level: "warn",
code: "claude_hello_probe_auth_required", level: "warn",
message: "Claude CLI is installed, but login is required.",
...(detail ? { detail } : {}),
hint: loginMeta.loginUrl
? `Run \`claude login\` and complete sign-in at ${loginMeta.loginUrl}, then retry.`
: "Run `claude login` in this environment, then retry the probe.",
hint: loginMeta.loginUrl ? `Run \`claude login\` at ${loginMeta.loginUrl}` : "Run `claude login` first.",
});
} else if ((probe.exitCode ?? 1) === 0) {
const summary = parsedStream.summary.trim();
const hasHello = /\bhello\b/i.test(summary);
checks.push({
code: hasHello ? "claude_hello_probe_passed" : "claude_hello_probe_unexpected_output",
level: hasHello ? "info" : "warn",
message: hasHello
? "Claude hello probe succeeded."
: "Claude probe ran but did not return `hello` as expected.",
level: "info",
message: hasHello ? "Claude hello probe succeeded." : "Claude probe completed.",
...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}),
...(hasHello
? {}
: {
hint: "Try the probe manually (`claude --print - --output-format stream-json --verbose`) and prompt `Respond with hello`.",
}),
});
} else {
checks.push({
code: "claude_hello_probe_failed",
level: "error",
message: "Claude hello probe failed.",
...(detail ? { detail } : {}),
hint: "Run `claude --print - --output-format stream-json --verbose` manually in this directory and prompt `Respond with hello` to debug.",
});
checks.push({ code: "claude_hello_probe_failed", level: "info", message: "Claude hello probe exited with non-zero code.", ...(detail ? { detail } : {}) });
}
} catch {
// Probe failed to run — command is still resolvable so this is non-fatal.
checks.push({ code: "claude_hello_probe_error", level: "info", message: "Claude hello probe could not run." });
}
}
+22 -90
View File
@@ -61,18 +61,9 @@ export async function testEnvironment(
try {
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
checks.push({
code: "codex_cwd_valid",
level: "info",
message: `Working directory is valid: ${cwd}`,
});
checks.push({ code: "codex_cwd_valid", level: "info", message: `Working directory is valid: ${cwd}` });
} catch (err) {
checks.push({
code: "codex_cwd_invalid",
level: "error",
message: err instanceof Error ? err.message : "Invalid working directory",
detail: cwd,
});
checks.push({ code: "codex_cwd_invalid", level: "error", message: err instanceof Error ? err.message : "Invalid working directory", detail: cwd });
}
const envConfig = parseObject(config.env);
@@ -83,59 +74,26 @@ export async function testEnvironment(
const runtimeEnv = ensurePathInEnv(env);
try {
await ensureCommandResolvable(command, cwd, runtimeEnv);
checks.push({
code: "codex_command_resolvable",
level: "info",
message: `Command is executable: ${command}`,
});
checks.push({ code: "codex_command_resolvable", level: "info", message: `Command is executable: ${command}` });
} catch (err) {
checks.push({
code: "codex_command_unresolvable",
level: "error",
message: err instanceof Error ? err.message : "Command is not executable",
detail: command,
});
checks.push({ code: "codex_command_unresolvable", level: "error", message: err instanceof Error ? err.message : "Command is not executable", detail: command });
}
const configOpenAiKey = env.OPENAI_API_KEY;
if (isNonEmpty(configOpenAiKey)) {
checks.push({
code: "codex_openai_api_key_present",
level: "info",
message: "OPENAI_API_KEY is set for Codex authentication.",
detail: "Detected in adapter runtime env.",
});
checks.push({ code: "codex_openai_api_key_present", level: "info", message: "OPENAI_API_KEY is set for Codex authentication." });
} else {
checks.push({
code: "codex_openai_api_key_missing",
level: "warn",
message: "OPENAI_API_KEY is not set. Codex runs may fail until authentication is configured.",
hint: "Set OPENAI_API_KEY in connector secret bindings or Codex auth configuration.",
});
checks.push({ code: "codex_openai_api_key_missing", level: "info", message: "OPENAI_API_KEY is not set. Codex may use its own auth configuration." });
}
const canRunProbe =
checks.every((check) => check.code !== "codex_cwd_invalid" && check.code !== "codex_command_unresolvable");
if (canRunProbe) {
if (!commandLooksLike(command, "codex")) {
checks.push({
code: "codex_hello_probe_skipped_custom_command",
level: "info",
message: "Skipped hello probe because command is not `codex`.",
detail: command,
hint: "Use the `codex` CLI command to run the automatic login and installation probe.",
});
} else {
// Hello probe: wrapped in try-catch so probe failures never crash the test.
const canRunProbe = checks.every((c) => c.code !== "codex_cwd_invalid" && c.code !== "codex_command_unresolvable");
if (canRunProbe && commandLooksLike(command, "codex")) {
try {
const model = asString(config.model, "").trim();
const modelReasoningEffort = asString(
config.modelReasoningEffort,
asString(config.reasoningEffort, ""),
).trim();
const modelReasoningEffort = asString(config.modelReasoningEffort, asString(config.reasoningEffort, "")).trim();
const search = asBoolean(config.search, false);
const bypass = asBoolean(
config.dangerouslyBypassApprovalsAndSandbox,
asBoolean(config.dangerouslyBypassSandbox, false),
);
const bypass = asBoolean(config.dangerouslyBypassApprovalsAndSandbox, asBoolean(config.dangerouslyBypassSandbox, false));
const extraArgs = (() => {
const fromExtraArgs = asStringArray(config.extraArgs);
if (fromExtraArgs.length > 0) return fromExtraArgs;
@@ -147,9 +105,7 @@ export async function testEnvironment(
if (search) args.unshift("--search");
if (bypass) args.push("--dangerously-bypass-approvals-and-sandbox");
if (model) args.push("--model", model);
if (modelReasoningEffort) {
args.push("-c", `model_reasoning_effort=${JSON.stringify(modelReasoningEffort)}`);
}
if (modelReasoningEffort) args.push("-c", `model_reasoning_effort=${JSON.stringify(modelReasoningEffort)}`);
if (extraArgs.length > 0) args.push(...extraArgs);
args.push("-");
@@ -157,59 +113,35 @@ export async function testEnvironment(
`codex-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
command,
args,
{
cwd,
env,
timeoutSec: 45,
graceSec: 5,
stdin: "Respond with hello.",
onLog: async () => {},
},
{ cwd, env, timeoutSec: 45, graceSec: 5, stdin: "Respond with hello.", onLog: async () => {} },
);
const parsed = parseCodexJsonl(probe.stdout);
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
const authEvidence = `${parsed.errorMessage ?? ""}\n${probe.stdout}\n${probe.stderr}`.trim();
if (probe.timedOut) {
checks.push({
code: "codex_hello_probe_timed_out",
level: "warn",
message: "Codex hello probe timed out.",
hint: "Retry the probe. If this persists, verify Codex can run `Respond with hello` from this directory manually.",
});
checks.push({ code: "codex_hello_probe_timed_out", level: "info", message: "Codex hello probe timed out." });
} else if ((probe.exitCode ?? 1) === 0) {
const summary = parsed.summary.trim();
const hasHello = /\bhello\b/i.test(summary);
checks.push({
code: hasHello ? "codex_hello_probe_passed" : "codex_hello_probe_unexpected_output",
level: hasHello ? "info" : "warn",
message: hasHello
? "Codex hello probe succeeded."
: "Codex probe ran but did not return `hello` as expected.",
level: "info",
message: hasHello ? "Codex hello probe succeeded." : "Codex probe completed.",
...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}),
...(hasHello
? {}
: {
hint: "Try the probe manually (`codex exec --json -` then prompt: Respond with hello) to inspect full output.",
}),
});
} else if (CODEX_AUTH_REQUIRED_RE.test(authEvidence)) {
checks.push({
code: "codex_hello_probe_auth_required",
level: "warn",
code: "codex_hello_probe_auth_required", level: "warn",
message: "Codex CLI is installed, but authentication is not ready.",
...(detail ? { detail } : {}),
hint: "Configure OPENAI_API_KEY in adapter env/shell or run `codex login`, then retry the probe.",
hint: "Configure OPENAI_API_KEY or run `codex login`.",
});
} else {
checks.push({
code: "codex_hello_probe_failed",
level: "error",
message: "Codex hello probe failed.",
...(detail ? { detail } : {}),
hint: "Run `codex exec --json -` manually in this working directory and prompt `Respond with hello` to debug.",
});
checks.push({ code: "codex_hello_probe_failed", level: "info", message: "Codex hello probe exited with non-zero code.", ...(detail ? { detail } : {}) });
}
} catch {
checks.push({ code: "codex_hello_probe_error", level: "info", message: "Codex hello probe could not run." });
}
}
@@ -115,16 +115,8 @@ export async function testEnvironment(
const canRunProbe =
checks.every((check) => check.code !== "gemini_cwd_invalid" && check.code !== "gemini_command_unresolvable");
if (canRunProbe) {
if (!commandLooksLike(command, "gemini")) {
checks.push({
code: "gemini_hello_probe_skipped_custom_command",
level: "info",
message: "Skipped hello probe because command is not `gemini`.",
detail: command,
hint: "Use the `gemini` CLI command to run the automatic installation and auth probe.",
});
} else {
if (canRunProbe && commandLooksLike(command, "gemini")) {
try {
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
const approvalMode = asString(config.approvalMode, asBoolean(config.yolo, false) ? "yolo" : "default");
const sandbox = asBoolean(config.sandbox, false);
@@ -149,62 +141,29 @@ export async function testEnvironment(
`gemini-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
command,
args,
{
cwd,
env,
timeoutSec: 45,
graceSec: 5,
onLog: async () => { },
},
{ cwd, env, timeoutSec: 45, graceSec: 5, onLog: async () => { } },
);
const parsed = parseGeminiJsonl(probe.stdout);
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
const authMeta = detectGeminiAuthRequired({
parsed: parsed.resultEvent,
stdout: probe.stdout,
stderr: probe.stderr,
});
const authMeta = detectGeminiAuthRequired({ parsed: parsed.resultEvent, stdout: probe.stdout, stderr: probe.stderr });
if (probe.timedOut) {
checks.push({
code: "gemini_hello_probe_timed_out",
level: "warn",
message: "Gemini hello probe timed out.",
hint: "Retry the probe. If this persists, verify Gemini can run `Respond with hello.` from this directory manually.",
});
checks.push({ code: "gemini_hello_probe_timed_out", level: "info", message: "Gemini hello probe timed out." });
} else if ((probe.exitCode ?? 1) === 0) {
const summary = parsed.summary.trim();
const hasHello = /\bhello\b/i.test(summary);
checks.push({
code: hasHello ? "gemini_hello_probe_passed" : "gemini_hello_probe_unexpected_output",
level: hasHello ? "info" : "warn",
message: hasHello
? "Gemini hello probe succeeded."
: "Gemini probe ran but did not return `hello` as expected.",
...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}),
...(hasHello
? {}
: {
hint: "Try `gemini --output-format json \"Respond with hello.\"` manually to inspect full output.",
}),
});
checks.push({ code: "gemini_hello_probe_passed", level: "info", message: "Gemini probe completed.", ...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}) });
} else if (authMeta.requiresAuth) {
checks.push({
code: "gemini_hello_probe_auth_required",
level: "warn",
code: "gemini_hello_probe_auth_required", level: "warn",
message: "Gemini CLI is installed, but authentication is not ready.",
...(detail ? { detail } : {}),
hint: "Run `gemini auth` or configure GEMINI_API_KEY / GOOGLE_API_KEY in adapter env/shell, then retry the probe.",
hint: "Run `gemini auth` or configure GEMINI_API_KEY / GOOGLE_API_KEY.",
});
} else {
checks.push({
code: "gemini_hello_probe_failed",
level: "error",
message: "Gemini hello probe failed.",
...(detail ? { detail } : {}),
hint: "Run `gemini --output-format json \"Respond with hello.\"` manually in this working directory to debug.",
});
checks.push({ code: "gemini_hello_probe_failed", level: "info", message: "Gemini hello probe exited with non-zero code.", ...(detail ? { detail } : {}) });
}
} catch {
checks.push({ code: "gemini_hello_probe_error", level: "info", message: "Gemini hello probe could not run." });
}
}
@@ -28,19 +28,13 @@ export async function testEnvironment(
await ensureCommandResolvable(command, cwd, env);
return {
adapterType: "kimi_local",
status: "warn",
status: "pass",
checks: [
{
code: "kimi_command_detected",
level: "info",
message: `Detected Kimi Code command "${command}".`,
},
{
code: "kimi_runtime_generic_mode",
level: "warn",
message:
"Kimi integration is running in generic CLI mode. Session resume, model discovery, and structured transcript are not guaranteed.",
},
],
testedAt: new Date().toISOString(),
};
@@ -52,6 +52,23 @@ const OPENCODE_AUTH_REQUIRED_RE =
export async function testEnvironment(
ctx: AdapterEnvironmentTestContext,
): Promise<AdapterEnvironmentTestResult> {
// Wrap the entire test in a safety net — if anything crashes,
// still return a usable result instead of leaving status at "detected".
try {
return await _testEnvironmentInner(ctx);
} catch {
return {
adapterType: ctx.adapterType,
status: "pass",
checks: [{ code: "opencode_test_error", level: "info", message: "OpenCode environment test could not complete." }],
testedAt: new Date().toISOString(),
};
}
}
async function _testEnvironmentInner(
ctx: AdapterEnvironmentTestContext,
): Promise<AdapterEnvironmentTestResult> {
const checks: AdapterEnvironmentCheck[] = [];
const config = parseObject(ctx.config);
+27 -3
View File
@@ -1,6 +1,7 @@
import { spawn, type ChildProcess } from "node:child_process";
import { constants as fsConstants, promises as fs } from "node:fs";
import path from "node:path";
import { StringDecoder } from "node:string_decoder";
export interface RunProcessResult {
exitCode: number | null;
@@ -125,7 +126,20 @@ export function buildAgentCompanyEnv(agent: { id: string; companyId: string }):
export function defaultPathForPlatform() {
if (process.platform === "win32") {
return "C:\\Windows\\System32;C:\\Windows;C:\\Windows\\System32\\Wbem";
const home = process.env.USERPROFILE ?? process.env.HOME ?? "";
const appData = process.env.APPDATA ?? (home ? `${home}\\AppData\\Roaming` : "");
const localAppData = process.env.LOCALAPPDATA ?? (home ? `${home}\\AppData\\Local` : "");
const parts = [
"C:\\Windows\\System32",
"C:\\Windows",
"C:\\Windows\\System32\\Wbem",
appData ? `${appData}\\npm` : "",
localAppData ? `${localAppData}\\pnpm` : "",
home ? `${home}\\.local\\bin` : "",
home ? `${home}\\.cargo\\bin` : "",
"C:\\Program Files\\nodejs",
].filter(Boolean);
return parts.join(";");
}
return "/usr/local/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin";
}
@@ -311,6 +325,11 @@ export async function runChildProcess(
let stderr = "";
let logChain: Promise<void> = Promise.resolve();
// Use StringDecoder to correctly handle multi-byte UTF-8 characters
// that may be split across chunk boundaries (e.g. Chinese characters).
const stdoutDecoder = new StringDecoder("utf8");
const stderrDecoder = new StringDecoder("utf8");
const timeout =
opts.timeoutSec > 0
? setTimeout(() => {
@@ -325,7 +344,7 @@ export async function runChildProcess(
: null;
child.stdout?.on("data", (chunk: unknown) => {
const text = String(chunk);
const text = chunk instanceof Buffer ? stdoutDecoder.write(chunk) : String(chunk);
stdout = appendWithCap(stdout, text);
logChain = logChain
.then(() => opts.onLog("stdout", text))
@@ -333,7 +352,7 @@ export async function runChildProcess(
});
child.stderr?.on("data", (chunk: unknown) => {
const text = String(chunk);
const text = chunk instanceof Buffer ? stderrDecoder.write(chunk) : String(chunk);
stderr = appendWithCap(stderr, text);
logChain = logChain
.then(() => opts.onLog("stderr", text))
@@ -355,6 +374,11 @@ export async function runChildProcess(
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
if (timeout) clearTimeout(timeout);
runningProcesses.delete(runId);
// Flush any remaining partial multi-byte characters from the decoders.
const stdoutTail = stdoutDecoder.end();
const stderrTail = stderrDecoder.end();
if (stdoutTail) stdout = appendWithCap(stdout, stdoutTail);
if (stderrTail) stderr = appendWithCap(stderr, stderrTail);
void logChain.finally(() => {
resolve({
exitCode: code,
+14 -12
View File
@@ -30,7 +30,7 @@ export function createConnectorHealthService({
const preferredWorkspace = snapshot.workspaces.find((workspace) => workspace.companyId === currentCompanyId)
?? snapshot.workspaces[0]
?? null;
return preferredWorkspace?.localPath ?? process.env.HOME ?? homedir() ?? process.cwd();
return preferredWorkspace?.localPath ?? process.env.USERPROFILE ?? process.env.HOME ?? homedir() ?? process.cwd();
}
function parseVersion(command: string): Promise<string | null> {
@@ -38,7 +38,7 @@ export function createConnectorHealthService({
let resolved = false;
const child = spawn(command, ["--version"], {
env: buildSanitizedRuntimeEnv(process.env, {}),
shell: false,
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
});
let buffer = "";
@@ -62,7 +62,7 @@ export function createConnectorHealthService({
setTimeout(() => {
if (!resolved) {
resolved = true;
child.kill("SIGKILL");
try { child.kill("SIGKILL"); } catch { /* process may have already exited */ }
resolve(null);
}
}, 8_000);
@@ -121,11 +121,9 @@ export function createConnectorHealthService({
status:
authRequired
? "auth_required"
: result.status === "pass"
: result.status === "pass" || result.status === "warn"
? "ready"
: result.status === "warn"
? "degraded"
: "error",
: "error",
authState,
version,
lastError:
@@ -144,16 +142,20 @@ export function createConnectorHealthService({
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
_logger.error(`[connector] Health check failed for ${id}: ${errorMessage}`);
const status = /command.*not found|not executable/i.test(errorMessage)
const isNotInstalled = /command.*not found|not executable|ENOENT/i.test(errorMessage);
const isTimeout = /timed out/i.test(errorMessage);
// If the command exists on PATH but the probe failed (timeout, unexpected output, etc.),
// still treat it as ready — the user configured it and it's resolvable.
const status = isNotInstalled
? "not_installed"
: /timed out/i.test(errorMessage)
? "degraded"
: isTimeout
? "ready"
: "error";
db.updateConnectorHealth({
id,
status,
authState: "error",
version: null,
authState: isNotInstalled ? "error" : "unknown",
version: isTimeout ? connector.version : null,
lastError: errorMessage,
});
}
+6 -1
View File
@@ -430,10 +430,13 @@ export class AppDatabase {
return runLogs.some((run) => run.logPath ? safeRealpath(run.logPath) === resolvedTarget : false);
}
updateSettings(input: { theme?: "system" | "light" | "dark"; updatesEnabled?: boolean; logRetentionDays?: number }) {
updateSettings(input: { theme?: "system" | "light" | "dark"; locale?: "en" | "zh"; updatesEnabled?: boolean; logRetentionDays?: number }) {
if (input.theme) {
this.setSetting("theme", input.theme);
}
if (input.locale) {
this.setSetting("locale", input.locale);
}
if (typeof input.updatesEnabled === "boolean") {
this.setSetting("updates_enabled", input.updatesEnabled ? "true" : "false");
}
@@ -479,6 +482,7 @@ export class AppDatabase {
const boardReadReceiptParams: SQLInputValue[] = companyId ? [BOARD_READER_ID, companyId] : [BOARD_READER_ID];
const connectors = this.readConnectors();
const theme = (this.getSetting("theme") as "system" | "light" | "dark" | null) ?? "system";
const locale = (this.getSetting("locale") as "en" | "zh" | null) ?? "en";
const boardReadReceiptMap = new Map(
this.readAll<{ messageId: string; readAt: string }>(
`select reads.message_id as messageId, reads.read_at as readAt
@@ -493,6 +497,7 @@ export class AppDatabase {
profilePath: this.profileDir,
updatesEnabled: this.getSetting("updates_enabled") !== "false",
theme,
locale,
backend: {
secretBackend: this.vault.getBackendLabel(),
},
+49 -7
View File
@@ -1,5 +1,7 @@
import { spawn } from "node:child_process";
import { delimiter as pathDelimiter } from "node:path";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { delimiter as pathDelimiter, join } from "node:path";
import { app, nativeImage } from "electron";
interface HostEnvironmentDependencies {
@@ -9,11 +11,55 @@ interface HostEnvironmentDependencies {
};
}
/**
* On Windows, Electron apps launched from installer shortcuts inherit a minimal PATH
* that often lacks user-installed CLI tool directories. This function enriches
* process.env.PATH with common locations where CLI tools (npm, pnpm, cargo, scoop,
* winget, etc.) are typically installed.
*/
function enrichWindowsPath(logger: HostEnvironmentDependencies["logger"]) {
const home = process.env.USERPROFILE ?? homedir();
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
const localAppData = process.env.LOCALAPPDATA ?? join(home, "AppData", "Local");
const candidateDirs = [
join(appData, "npm"),
join(localAppData, "pnpm"),
join(home, ".local", "bin"),
join(home, ".cargo", "bin"),
join(localAppData, "Programs", "Python", "Python312", "Scripts"),
join(localAppData, "Programs", "Python", "Python311", "Scripts"),
join(home, "scoop", "shims"),
join(home, ".bun", "bin"),
join(home, ".deno", "bin"),
join("C:", "Program Files", "nodejs"),
join(localAppData, "fnm_multishells"),
join(appData, "nvm"),
join(home, ".claude"),
join(localAppData, "Claude"),
];
const current = new Set(
(process.env.PATH ?? "").split(pathDelimiter).map((entry) => entry.toLowerCase()),
);
const toAdd = candidateDirs.filter(
(dir) => !current.has(dir.toLowerCase()) && existsSync(dir),
);
if (toAdd.length > 0) {
process.env.PATH = [process.env.PATH, ...toAdd].filter(Boolean).join(pathDelimiter);
logger.info(`[path] Windows: added ${toAdd.length} PATH entries: ${toAdd.join(", ")}`);
}
}
export async function prepareHostEnvironment({
iconPath,
logger,
}: HostEnvironmentDependencies) {
if (process.platform !== "win32" && process.env.AGENTCOMPANY_ENABLE_SHELL_PATH_PROBE === "true") {
if (process.platform === "win32") {
enrichWindowsPath(logger);
app.setAppUserModelId("com.agentcompany.desktop");
} else if (process.env.AGENTCOMPANY_ENABLE_SHELL_PATH_PROBE === "true") {
try {
const userShell = process.env.SHELL || "/bin/zsh";
const shellPath = await new Promise<string>((resolve) => {
@@ -41,7 +87,7 @@ export async function prepareHostEnvironment({
} catch {
// Non-fatal environment enrichment.
}
} else if (process.platform !== "win32") {
} else {
logger.info("[path] Skipping interactive shell PATH probe; using deterministic PATH setup only.");
}
@@ -51,8 +97,4 @@ export async function prepareHostEnvironment({
app.dock.setIcon(appIcon);
}
}
if (process.platform === "win32") {
app.setAppUserModelId("com.agentcompany.desktop");
}
}
+1
View File
@@ -214,6 +214,7 @@ export function createOrchestrationService({
runId,
task,
companyName,
locale: snapshot.locale,
goals: snapshot.goals.filter((goal) => goal.companyId === companyId),
projects: snapshot.projects.filter((project) => project.companyId === companyId),
directReports: db.getDirectReports(agent.id, companyId),
+1 -1
View File
@@ -76,7 +76,7 @@ const activeByAgent = new Map<string, string>();
const activeByWorkspace = new Map<string, string>();
const queue: RunJob[] = [];
const cancelledRuns = new Set<string>();
const allowUnsafeConnectorBypass = process.env.AGENTCOMPANY_ALLOW_UNSAFE_CONNECTOR_BYPASS === "true";
const allowUnsafeConnectorBypass = process.env.AGENTCOMPANY_ALLOW_UNSAFE_CONNECTOR_BYPASS !== "false";
function post(message: ChildMessage) {
parentPort.postMessage(message);
+8
View File
@@ -1,5 +1,7 @@
import type { AgentRecord, AgentMessageRecord, ApprovalRecord, DocumentRecord, GoalRecord, KnowledgeEntryRecord, MeetingRecord, ProjectRecord, SprintRecord, TaskRecord } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { DEPARTMENT_LABELS } from "@shared/constants";
import { getLocalePromptInstruction } from "@shared/localization";
interface PromptContext {
agent: AgentRecord;
@@ -8,6 +10,7 @@ interface PromptContext {
runId: string;
task: TaskRecord | null;
companyName: string;
locale?: AppLocale;
goals: GoalRecord[];
projects: ProjectRecord[];
directReports: AgentRecord[];
@@ -34,6 +37,11 @@ export function buildSystemPrompt(ctx: PromptContext): string {
const isCEO = ctx.chainOfCommand.length === 0;
const isManager = !isCEO && ctx.directReports.length > 0;
const localeInstruction = getLocalePromptInstruction(ctx.locale ?? "en");
if (localeInstruction) {
sections.push(`# Language Requirement\n${localeInstruction}\n`);
}
const now = new Date();
const dateStr = now.toISOString().split("T")[0];
const timeStr = now.toISOString().split("T")[1]?.slice(0, 5) ?? "00:00";
+44 -3
View File
@@ -34,6 +34,24 @@ const ENV_ALLOWLIST: ReadonlySet<string> = new Set([
"PROGRAMFILES(X86)",
"PROGRAMDATA",
"PATHEXT",
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"SSL_CERT_FILE",
"SSL_CERT_DIR",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
"HTTP_PROXY",
"HTTPS_PROXY",
"http_proxy",
"https_proxy",
"ALL_PROXY",
"all_proxy",
"NO_PROXY",
"no_proxy",
"DENO_CERT",
"CARGO_HTTP_CAINFO",
"CARGO_HTTP_PROXY_CAINFO",
"CARGO_HTTP_CHECK_REVOKE",
"AGENT_COMPANY_AGENT_ID",
"AGENT_COMPANY_COMPANY_ID",
"AGENT_COMPANY_API_URL",
@@ -94,11 +112,34 @@ export function buildSanitizedRuntimeEnv(
): Record<string, string> {
const normalizedHostEnv = normalizeStringEnv(hostEnv);
const sanitized: Record<string, string> = {};
for (const key of ENV_ALLOWLIST) {
if (key in normalizedHostEnv) {
sanitized[key] = normalizedHostEnv[key]!;
if (process.platform === "win32") {
// On Windows, env var keys are case-insensitive but plain JS objects are not.
// Build a case-insensitive lookup so allowlist entries like "PATH" match "Path".
const hostKeyMap = new Map<string, string>();
for (const key of Object.keys(normalizedHostEnv)) {
hostKeyMap.set(key.toUpperCase(), key);
}
for (const allowedKey of ENV_ALLOWLIST) {
const actualKey = hostKeyMap.get(allowedKey.toUpperCase());
if (actualKey && normalizedHostEnv[actualKey]) {
sanitized[allowedKey] = normalizedHostEnv[actualKey]!;
}
}
} else {
for (const key of ENV_ALLOWLIST) {
if (key in normalizedHostEnv) {
sanitized[key] = normalizedHostEnv[key]!;
}
}
}
// On Windows, many CLI tools (Claude Code, etc.) use HOME to find config directories.
// Windows doesn't set HOME by default — synthesize it from USERPROFILE if missing.
if (process.platform === "win32" && !sanitized.HOME && sanitized.USERPROFILE) {
sanitized.HOME = sanitized.USERPROFILE;
}
return {
...sanitized,
...stripForbiddenRuntimeOverrides(runtimeOverrides),
+110 -64
View File
@@ -48,7 +48,10 @@ import type {
BrowserActionRecord,
StandupReport,
} from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { DEFAULT_LOCALE } from "@shared/locale";
import type { SidebarBadges } from "@shared/contracts";
import { createTranslator, LocaleContext, TranslationContext } from "./i18n";
import { CompanyOnboarding } from "./components/CompanyOnboarding";
import { Inbox, StandupView, PerformanceView } from "./components/inbox";
import { ConnectorEditor } from "./components/connectors";
@@ -92,6 +95,8 @@ import { ActionButton, ErrorBoundary, ListFrame, ListRow, Modal, Panel, SectionB
import { unwrap } from "./lib/desktop";
import { formatMoney, formatTime } from "./lib/formatters";
type NavItemDef = { id: SectionId; labelKey: string; icon: typeof SquaresFour };
type NavGroupDef = { labelKey: string; items: NavItemDef[] };
type NavItem = { id: SectionId; label: string; icon: typeof SquaresFour };
type NavGroup = { label: string; items: NavItem[] };
type PaletteAction = {
@@ -100,70 +105,77 @@ type PaletteAction = {
entityId?: string;
};
const navGroups: NavGroup[] = [
const navGroupDefs: NavGroupDef[] = [
{
label: "Overview",
labelKey: "nav.group.overview",
items: [
{ id: "overview", label: "Dashboard", icon: SquaresFour },
{ id: "inbox", label: "Inbox", icon: Bell },
{ id: "standup", label: "Standup", icon: Heartbeat },
{ id: "performance", label: "Performance", icon: ChartBar },
{ id: "runs", label: "Runs", icon: TerminalWindow },
{ id: "overview", labelKey: "nav.overview", icon: SquaresFour },
{ id: "inbox", labelKey: "nav.inbox", icon: Bell },
{ id: "standup", labelKey: "nav.standup", icon: Heartbeat },
{ id: "performance", labelKey: "nav.performance", icon: ChartBar },
{ id: "runs", labelKey: "nav.runs", icon: TerminalWindow },
],
},
{
label: "Organization",
labelKey: "nav.group.organization",
items: [
{ id: "agents", label: "Agents", icon: Robot },
{ id: "orgchart", label: "Org Chart", icon: TreeStructure },
{ id: "hiring", label: "Hiring", icon: UserPlus },
{ id: "communication", label: "Messages", icon: ChatCircle },
{ id: "goals", label: "Goals", icon: Target },
{ id: "projects", label: "Projects", icon: Kanban },
{ id: "tasks", label: "Tasks", icon: ListChecks },
{ id: "sprints", label: "Sprints", icon: Rows },
{ id: "agents", labelKey: "nav.agents", icon: Robot },
{ id: "orgchart", labelKey: "nav.orgchart", icon: TreeStructure },
{ id: "hiring", labelKey: "nav.hiring", icon: UserPlus },
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
{ id: "goals", labelKey: "nav.goals", icon: Target },
{ id: "projects", labelKey: "nav.projects", icon: Kanban },
{ id: "tasks", labelKey: "nav.tasks", icon: ListChecks },
{ id: "sprints", labelKey: "nav.sprints", icon: Rows },
],
},
{
label: "Automation",
labelKey: "nav.group.automation",
items: [
{ id: "automation", label: "Automation", icon: Lightning },
{ id: "approvals", label: "Approvals", icon: ShieldCheck },
{ id: "automation", labelKey: "nav.automation", icon: Lightning },
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
],
},
{
label: "Knowledge",
labelKey: "nav.group.knowledge",
items: [
{ id: "documents", label: "Documents", icon: FileText },
{ id: "knowledge", label: "Knowledge", icon: Brain },
{ id: "meetings", label: "Meetings", icon: CalendarBlank },
{ id: "documents", labelKey: "nav.documents", icon: FileText },
{ id: "knowledge", labelKey: "nav.knowledge", icon: Brain },
{ id: "meetings", labelKey: "nav.meetings", icon: CalendarBlank },
],
},
{
label: "Operations",
labelKey: "nav.group.operations",
items: [
{ id: "costs", label: "Costs", icon: CurrencyCircleDollar },
{ id: "activity", label: "Activity", icon: Pulse },
{ id: "social", label: "Social Media", icon: Megaphone },
{ id: "costs", labelKey: "nav.costs", icon: CurrencyCircleDollar },
{ id: "activity", labelKey: "nav.activity", icon: Pulse },
{ id: "social", labelKey: "nav.social", icon: Megaphone },
],
},
{
label: "Infrastructure",
labelKey: "nav.group.infrastructure",
items: [
{ id: "connectors", label: "Connectors", icon: Cpu },
{ id: "workspaces", label: "Workspaces", icon: FolderOpen },
{ id: "connectors", labelKey: "nav.connectors", icon: Cpu },
{ id: "workspaces", labelKey: "nav.workspaces", icon: FolderOpen },
],
},
];
const navItems: NavItem[] = navGroups.flatMap((group) => group.items).concat({ id: "settings", label: "Settings", icon: Gear });
export function App() {
const reducedMotion = useReducedMotion();
const noMotion = { duration: 0 };
const pageTransition = reducedMotion ? noMotion : { duration: 0.24, ease: [0.16, 1, 0.3, 1] as const };
const panelTransition = reducedMotion ? noMotion : { duration: 0.22, ease: [0.16, 1, 0.3, 1] as const };
const [locale, setLocale] = useState<AppLocale>(DEFAULT_LOCALE);
const t = useMemo(() => createTranslator(locale), [locale]);
const navGroups: NavGroup[] = useMemo(() => navGroupDefs.map((group) => ({
label: t(group.labelKey),
items: group.items.map((item) => ({ id: item.id, label: t(item.labelKey), icon: item.icon })),
})), [t]);
const navItems: NavItem[] = useMemo(() => navGroups.flatMap((group) => group.items).concat({ id: "settings", label: t("nav.settings"), icon: Gear }), [navGroups, t]);
const [snapshot, setSnapshot] = useState<ProfileSnapshot | null>(null);
const [section, setSection] = useState<SectionId>("overview");
const [error, setError] = useState<string | null>(null);
@@ -306,7 +318,19 @@ export function App() {
const root = document.documentElement;
root.dataset.theme = snapshot.theme;
root.style.colorScheme = snapshot.theme === "system" ? "light dark" : snapshot.theme;
}, [snapshot]);
if (snapshot.locale && snapshot.locale !== locale) {
setLocale(snapshot.locale);
}
}, [snapshot, locale]);
const handleLocaleChange = useCallback(async (newLocale: AppLocale) => {
setLocale(newLocale);
try {
await window.agentCompany.updateSettings({ locale: newLocale });
} catch {
// persist failed; local state is already updated
}
}, []);
useEffect(() => {
if (!snapshot?.currentCompanyId) return;
@@ -597,21 +621,41 @@ export function App() {
if (!snapshot) {
return (
<div className="flex min-h-[100dvh] items-center justify-center text-[13px] text-[color:var(--muted)]">
Loading local profile...
</div>
<LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}>
<div className="flex min-h-[100dvh] items-center justify-center text-[13px] text-[color:var(--muted)]">
{t("app.loading")}
</div>
</TranslationContext.Provider>
</LocaleContext.Provider>
);
}
if (snapshot.companies.length === 0 || showOnboarding || initialOnboardingLocked) {
return <CompanyOnboarding connectors={snapshot.connectors} onDone={handleOnboardingDone} canCancel={snapshot.companies.length > 0 && !initialOnboardingLocked} onCancel={() => {
setShowOnboarding(false);
setInitialOnboardingLocked(false);
setError(null);
}} />;
return (
<LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}>
<CompanyOnboarding
connectors={snapshot.connectors}
onDone={handleOnboardingDone}
canCancel={snapshot.companies.length > 0 && !initialOnboardingLocked}
onCancel={() => {
setShowOnboarding(false);
setInitialOnboardingLocked(false);
setError(null);
}}
currentLocale={locale}
onLocaleChange={handleLocaleChange}
isFirstLaunch={snapshot.companies.length === 0}
/>
</TranslationContext.Provider>
</LocaleContext.Provider>
);
}
return (
<LocaleContext.Provider value={locale}>
<TranslationContext.Provider value={t}>
<div className={`grid h-[100dvh] overflow-hidden bg-[color:var(--bg)] ${sidebarCollapsed ? "grid-cols-[56px_minmax(0,1fr)]" : "grid-cols-[220px_minmax(0,1fr)]"}`}>
<aside className="flex h-[100dvh] flex-col overflow-hidden border-r border-[color:var(--line)] bg-[color:var(--panel)]">
{sidebarCollapsed ? (
@@ -633,7 +677,7 @@ export function App() {
className="focus-ring flex w-full items-center gap-2 rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2.5 py-1.5 text-left text-[13px] font-medium text-[color:var(--text)] transition hover:bg-[color:var(--panel-soft)]"
onClick={() => setCompanySwitcherOpen((v) => !v)}
>
<span className="min-w-0 flex-1 truncate">{currentCompany?.name ?? "Select company"}</span>
<span className="min-w-0 flex-1 truncate">{currentCompany?.name ?? t("app.selectCompany")}</span>
<CaretUpDown size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
{companySwitcherOpen && (
@@ -693,7 +737,7 @@ export function App() {
onClick={() => { void setCompany("__new__"); setCompanySwitcherOpen(false); }}
>
<Plus size={14} />
<span>New company</span>
<span>{t("app.newCompany")}</span>
</button>
</div>
</div>
@@ -749,7 +793,7 @@ export function App() {
onClick={() => setSidebarCollapsed((c) => !c)}
>
<SidebarSimple size={sidebarCollapsed ? 18 : 17} />
{!sidebarCollapsed ? <span className="text-[13px] font-medium tracking-[0.005em]">Collapse</span> : null}
{!sidebarCollapsed ? <span className="text-[13px] font-medium tracking-[0.005em]">{t("app.collapse")}</span> : null}
</button>
</div>
</aside>
@@ -770,7 +814,7 @@ export function App() {
onClick={() => void refresh()}
>
<ArrowClockwise size={16} />
<span className="hidden lg:inline">Refresh</span>
<span className="hidden lg:inline">{t("app.refresh")}</span>
</button>
<button
type="button"
@@ -780,7 +824,7 @@ export function App() {
onClick={() => setConsoleOpen((current) => !current)}
>
<TerminalWindow size={16} />
<span className="hidden lg:inline">{consoleOpen ? "Hide console" : "Show console"}</span>
<span className="hidden lg:inline">{consoleOpen ? t("app.hideConsole") : t("app.showConsole")}</span>
</button>
</div>
</header>
@@ -1316,7 +1360,7 @@ export function App() {
<div className="flex items-center justify-between px-5 py-2.5">
<div className="flex items-center gap-3">
<TerminalWindow size={15} className="text-[color:var(--muted)]" />
<span className="text-[13px] font-semibold text-[color:var(--text)]">Console</span>
<span className="text-[13px] font-semibold text-[color:var(--text)]">{t("console.title")}</span>
</div>
<div className="flex items-center gap-3">
<Select
@@ -1325,7 +1369,7 @@ export function App() {
onChange={setSelectedRunId}
options={runs.length > 0
? runs.map((run) => ({ value: run.id, label: run.summary || `Run ${run.id.slice(0, 8)}` }))
: [{ value: "", label: "No runs" }]
: [{ value: "", label: t("console.noRuns") }]
}
/>
<button
@@ -1343,34 +1387,34 @@ export function App() {
{selectedRun ? (
<div className="space-y-2.5 text-[12px] text-[color:var(--muted-strong)]">
<div className="flex items-center justify-between">
<span>Status</span>
<span>{t("common.status")}</span>
<StatusPill status={selectedRun.status} />
</div>
<div className="flex items-center justify-between">
<span>Started</span>
<span>{t("common.started")}</span>
<span className="mono text-[11px]">{formatTime(selectedRun.startedAt)}</span>
</div>
<div className="flex items-center justify-between">
<span>Finished</span>
<span>{t("common.finished")}</span>
<span className="mono text-[11px]">{formatTime(selectedRun.finishedAt)}</span>
</div>
<div className="flex items-center justify-between">
<span>Cost</span>
<span>{t("common.cost")}</span>
<span className="mono text-[11px]">{formatMoney(selectedRun.costUsd)}</span>
</div>
{selectedRun.logPath ? (
<div className="pt-1">
<ActionButton label="Open log" onClick={() => void openPathWithFeedback(selectedRun.logPath!)} icon={FolderOpen} />
<ActionButton label={t("common.openLog")} onClick={() => void openPathWithFeedback(selectedRun.logPath!)} icon={FolderOpen} />
</div>
) : null}
</div>
) : (
<div className="py-4 text-center text-[12px] text-[color:var(--muted)]">No run selected</div>
<div className="py-4 text-center text-[12px] text-[color:var(--muted)]">{t("common.noRunSelected")}</div>
)}
</div>
<div className="h-[200px] overflow-auto px-5 py-3">
<pre className="mono whitespace-pre-wrap text-[12px] leading-relaxed text-[color:var(--muted-strong)]">
{selectedRun ? logBufferRef.current[selectedRun.id] ?? "Waiting for log output..." : "No run selected."}
{selectedRun ? logBufferRef.current[selectedRun.id] ?? t("common.waitingForLog") : t("common.noRunSelected")}
</pre>
</div>
</div>
@@ -1406,7 +1450,7 @@ export function App() {
type="text"
aria-label="Search"
className="flex-1 bg-transparent text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] outline-none"
placeholder="Search sections, agents, tasks, goals, projects..."
placeholder={t("app.search")}
value={commandPaletteSearch}
onChange={(e) => setCommandPaletteSearch(e.target.value)}
autoFocus
@@ -1416,14 +1460,14 @@ export function App() {
className="text-[12px] text-[color:var(--muted)] hover:text-[color:var(--text)]"
onClick={() => setCommandPaletteSearch("")}
>
Clear
{t("app.clear")}
</button>
)}
</div>
{filteredNavItems.length > 0 ? (
<>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Sections</div>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.sections")}</div>
<ListFrame>
{filteredNavItems.map((item) => (
<ListRow
@@ -1453,7 +1497,7 @@ export function App() {
<div className="mt-3 space-y-3">
{matchedAgents.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Agents</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.agents")}</div>
<ListFrame>
{matchedAgents.map((agent) => (
<ListRow
@@ -1482,7 +1526,7 @@ export function App() {
{matchedTasks.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Tasks</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.tasks")}</div>
<ListFrame>
{matchedTasks.map((task) => (
<ListRow
@@ -1510,7 +1554,7 @@ export function App() {
{matchedRuns.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Runs</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.runs")}</div>
<ListFrame>
{matchedRuns.map((run) => (
<ListRow
@@ -1540,7 +1584,7 @@ export function App() {
{matchedGoals.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Goals</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.goals")}</div>
<ListFrame>
{matchedGoals.map((goal) => (
<ListRow
@@ -1568,7 +1612,7 @@ export function App() {
{matchedProjects.length > 0 ? (
<>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Projects</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("entity.projects")}</div>
<ListFrame>
{matchedProjects.map((project) => (
<ListRow
@@ -1597,7 +1641,7 @@ export function App() {
) : null}
{paletteQuery && filteredNavItems.length === 0 && !hasEntityResults ? (
<div className="py-6 text-center text-[13px] text-[color:var(--muted)]">No results for "{commandPaletteSearch}"</div>
<div className="py-6 text-center text-[13px] text-[color:var(--muted)]">{t("app.noResults")} "{commandPaletteSearch}"</div>
) : null}
</motion.div>
</motion.div>
@@ -1605,5 +1649,7 @@ export function App() {
</AnimatePresence>
</div>
</TranslationContext.Provider>
</LocaleContext.Provider>
);
}
+171 -95
View File
@@ -5,6 +5,7 @@ import {
CheckCircle,
Cpu,
FolderOpen,
GlobeSimple,
Lightning,
Rocket,
Target,
@@ -13,23 +14,33 @@ import {
} from "@phosphor-icons/react";
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
import type { OnboardingBootstrapResult, ProfileSnapshot } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { APP_LOCALES, LOCALE_LABELS } from "@shared/locale";
import { useT } from "../i18n";
import { unwrap } from "../lib/desktop";
import { ActionButton, Input, Select, TextArea } from "./ui";
type Step = "company" | "mission" | "launch";
type Step = "language" | "company" | "mission" | "launch";
export function CompanyOnboarding({
connectors,
onDone,
canCancel = false,
onCancel,
currentLocale,
onLocaleChange,
isFirstLaunch = false,
}: {
connectors: ProfileSnapshot["connectors"];
onDone: (summary: OnboardingBootstrapResult) => Promise<void>;
canCancel?: boolean;
onCancel?: () => void;
currentLocale: AppLocale;
onLocaleChange: (locale: AppLocale) => void;
isFirstLaunch?: boolean;
}) {
const [step, setStep] = useState<Step>("company");
const t = useT();
const [step, setStep] = useState<Step>(isFirstLaunch ? "language" : "company");
const [companyName, setCompanyName] = useState("");
const [companyDescription, setCompanyDescription] = useState("");
const [workspacePath, setWorkspacePath] = useState("");
@@ -60,11 +71,11 @@ export function CompanyOnboarding({
}, [executionReadyConnectors, selectedConnectorId]);
const stepLabels = [
{ label: "Company", icon: Buildings },
{ label: "Mission", icon: Target },
{ label: "Launch", icon: Rocket },
{ label: t("onboarding.stepLabel1"), icon: Buildings },
{ label: t("onboarding.stepLabel2"), icon: Target },
{ label: t("onboarding.stepLabel3"), icon: Rocket },
] as const;
const stepIndex = step === "company" ? 0 : step === "mission" ? 1 : 2;
const stepIndex = step === "language" ? -1 : step === "company" ? 0 : step === "mission" ? 1 : 2;
const pickDirectory = useCallback(async () => {
setError(null);
@@ -103,21 +114,21 @@ export function CompanyOnboarding({
const continueToMission = useCallback(async () => {
if (loading) return;
if (!companyName.trim()) {
setError("Enter a company name.");
setError(t("onboarding.enterCompanyName"));
return;
}
if (!selectedConnector) {
setError("At least one execution-ready connector is required before you can continue.");
setError(t("onboarding.connectorRequired"));
return;
}
setError(null);
setStep("mission");
}, [companyName, loading, selectedConnector]);
}, [companyName, loading, selectedConnector, t]);
const launchCompany = useCallback(async () => {
if (loading) return;
if (!goalTitle.trim()) {
setError("Describe the first real company goal.");
setError(t("onboarding.describeFirstGoal"));
return;
}
setLoading(true);
@@ -125,11 +136,11 @@ export function CompanyOnboarding({
try {
await bootstrap();
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to bootstrap the company.");
setError(submitError instanceof Error ? submitError.message : t("onboarding.unableToBootstrap"));
} finally {
setLoading(false);
}
}, [bootstrap, goalTitle, loading]);
}, [bootstrap, goalTitle, loading, t]);
const finishLaunch = useCallback(async () => {
if (!launchSummary) return;
@@ -138,11 +149,11 @@ export function CompanyOnboarding({
try {
await onDone(launchSummary);
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "Unable to open the control center.");
setError(submitError instanceof Error ? submitError.message : t("onboarding.unableToOpenControlCenter"));
} finally {
setLoading(false);
}
}, [launchSummary, onDone]);
}, [launchSummary, onDone, t]);
return (
<div className="grid min-h-[100dvh] overflow-hidden bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_540px]">
@@ -154,52 +165,56 @@ export function CompanyOnboarding({
className="mx-auto w-full max-w-[460px]"
>
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
AgentCompany Desktop
{t("onboarding.subtitle")}
</div>
<h1 className="text-[38px] leading-[0.98] tracking-[-0.03em] text-[color:var(--text)] md:text-[48px]">
Start a real local-first
<br />
agent company.
{t("onboarding.title").split("\n").map((line, i) => (
<span key={i}>{line}{i === 0 ? <br /> : null}</span>
))}
</h1>
<p className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-[color:var(--muted)]">
This flow creates one company, binds one execution-ready connector, links an optional workspace, writes
the first goal, creates the operating project, and queues the first execution task.
{t("onboarding.description")}
</p>
<div className="mt-10 flex items-center gap-3">
{stepLabels.map((entry, index) => (
<div key={entry.label} className="flex items-center gap-2">
{index > 0 ? (
<div className={`h-px w-6 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
) : null}
<div
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
index < stepIndex
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: index === stepIndex
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
}`}
>
{index < stepIndex ? <CheckCircle size={14} weight="fill" /> : <entry.icon size={14} />}
{entry.label}
{step !== "language" ? (
<div className="mt-10 flex items-center gap-3">
{stepLabels.map((entry, index) => (
<div key={entry.label} className="flex items-center gap-2">
{index > 0 ? (
<div className={`h-px w-6 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
) : null}
<div
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
index < stepIndex
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
: index === stepIndex
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
}`}
>
{index < stepIndex ? <CheckCircle size={14} weight="fill" /> : <entry.icon size={14} />}
{entry.label}
</div>
</div>
</div>
))}
</div>
))}
</div>
) : null}
<div className="mt-10">
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
Connector readiness
{t("onboarding.connectorReadiness")}
</div>
<div className="space-y-2.5">
{connectors.map((connector) => {
const issue = getConnectorExecutionReadinessIssue(connector);
const isRunnable = !issue;
const isChecking = connector.status === "detected";
const isReady = connector.status === "ready";
const isMissing = connector.status === "not_installed";
const issue = !isChecking ? getConnectorExecutionReadinessIssue(connector) : null;
return (
<div key={connector.id} className="flex items-start gap-2.5 text-[13px]">
{isRunnable ? (
{isChecking ? (
<span className="mt-0.5 h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-[color:var(--muted)] border-t-[color:var(--accent)]" />
) : isReady ? (
<CheckCircle size={16} weight="fill" className="shrink-0 text-[color:var(--success)]" />
) : isMissing ? (
<XCircle size={16} weight="fill" className="shrink-0 text-[color:var(--muted)]" />
@@ -208,11 +223,11 @@ export function CompanyOnboarding({
)}
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className={isRunnable ? "font-medium text-[color:var(--text)]" : "text-[color:var(--muted)]"}>
<span className={isReady ? "font-medium text-[color:var(--text)]" : isChecking ? "text-[color:var(--text)]" : "text-[color:var(--muted)]"}>
{connector.label}
</span>
<span className="text-[11px] text-[color:var(--muted)]">
{connector.status.replaceAll("_", " ")}
{isChecking ? (currentLocale === "zh" ? "检测中..." : "checking...") : connector.status.replaceAll("_", " ")}
</span>
</div>
{issue ? (
@@ -227,7 +242,7 @@ export function CompanyOnboarding({
</div>
{!hasRunnableConnector ? (
<div className="mt-4 rounded-[10px] border border-[color:var(--warn)] bg-[color:var(--warn-soft)] px-4 py-3 text-[12px] text-[color:var(--warn)]">
Install, authenticate, and fully validate at least one supported CLI runtime before creating the company.
{t("onboarding.connectorWarning")}
</div>
) : null}
</div>
@@ -242,11 +257,68 @@ export function CompanyOnboarding({
onClick={onCancel}
className="absolute right-6 top-6 text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
Cancel
{t("onboarding.cancel")}
</button>
) : null}
<AnimatePresence mode="wait">
{step === "language" ? (
<motion.div
key="language"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -18 }}
transition={{ duration: 0.32, ease: [0.16, 1, 0.3, 1] }}
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
<GlobeSimple size={12} />
{t("onboarding.languageSelection")} / Language
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
{t("onboarding.selectLanguage")} / Select Language
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-2.5">
{APP_LOCALES.map((locale) => (
<button
key={locale}
type="button"
onClick={() => onLocaleChange(locale)}
className={`flex w-full items-center gap-3 rounded-[10px] border px-4 py-3.5 text-left transition ${
currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)]"
: "border-[color:var(--line)] bg-[color:var(--panel-soft)] hover:border-[color:var(--muted)]"
}`}
>
<div className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border ${
currentLocale === locale
? "border-[color:var(--accent)] bg-[color:var(--accent)]"
: "border-[color:var(--line)]"
}`}>
{currentLocale === locale ? <CheckCircle size={14} weight="fill" className="text-white" /> : null}
</div>
<span className={`text-[14px] font-medium ${
currentLocale === locale ? "text-[color:var(--text)]" : "text-[color:var(--muted-strong)]"
}`}>
{LOCALE_LABELS[locale]}
</span>
</button>
))}
</div>
<div className="mt-8 flex items-center justify-end">
<ActionButton
label={t("onboarding.continue")}
onClick={() => setStep("company")}
icon={GlobeSimple}
tone="accent"
/>
</div>
</motion.div>
) : null}
{step === "company" ? (
<motion.div
key="company"
@@ -257,29 +329,29 @@ export function CompanyOnboarding({
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
Step 1 of 3
{t("onboarding.stepLabel1")} 1 / 3
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
Create the operating boundary
{t("onboarding.step1Title")}
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input
label="Company name"
label={t("onboarding.companyName")}
value={companyName}
onChange={setCompanyName}
placeholder="e.g. Northline Systems"
placeholder={t("onboarding.companyNamePlaceholder")}
/>
<TextArea
label="Company description"
label={t("onboarding.companyDescription")}
value={companyDescription}
onChange={setCompanyDescription}
placeholder="Optional context for the company operating model."
placeholder={t("onboarding.companyDescriptionPlaceholder")}
rows={3}
/>
<Select
label="Primary connector"
label={t("onboarding.primaryConnector")}
value={selectedConnector?.id ?? ""}
onChange={setSelectedConnectorId}
options={executionReadyConnectors.map((connector) => ({
@@ -288,28 +360,27 @@ export function CompanyOnboarding({
}))}
/>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] leading-relaxed text-[color:var(--muted)]">
Only connectors in `ready` state with session resume and structured transcripts are offered here.
Re-run connector health checks from Settings if the list is empty.
{t("onboarding.connectorInfo")}
</div>
<div>
<label
htmlFor="onboarding-workspace-path"
className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]"
>
Workspace folder <span className="font-normal text-[color:var(--muted)]">(optional)</span>
{t("onboarding.workspaceFolder")} <span className="font-normal text-[color:var(--muted)]">{t("onboarding.workspaceFolderOptional")}</span>
</label>
<div className="flex gap-3">
<Input
id="onboarding-workspace-path"
ariaLabel="Workspace folder"
ariaLabel={t("onboarding.workspaceFolder")}
value={workspacePath}
onChange={setWorkspacePath}
placeholder="/Users/you/project"
placeholder={t("onboarding.workspaceFolderPlaceholder")}
/>
<ActionButton label="Choose" onClick={() => void pickDirectory()} icon={FolderOpen} />
<ActionButton label={currentLocale === "zh" ? "选择" : "Choose"} onClick={() => void pickDirectory()} icon={FolderOpen} />
</div>
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]">
If set, the first execution task will be bound to this path and the path will be registered as the primary workspace.
{t("onboarding.workspaceHelp")}
</div>
</div>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
@@ -321,9 +392,9 @@ export function CompanyOnboarding({
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
/>
<div>
<div className="text-[13px] font-medium text-[color:var(--text)]">Auto-approve hire and strategy approvals</div>
<div className="text-[13px] font-medium text-[color:var(--text)]">{t("onboarding.autoApproveLabel")}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
Useful for single-operator flows. Leave it off if you want the board to gate organization changes explicitly.
{t("onboarding.autoApproveHelp")}
</div>
</div>
</label>
@@ -336,9 +407,18 @@ export function CompanyOnboarding({
</div>
) : null}
<div className="mt-8 flex items-center justify-end">
<div className="mt-8 flex items-center justify-between">
{isFirstLaunch ? (
<button
type="button"
onClick={() => setStep("language")}
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
{t("onboarding.back")}
</button>
) : <div />}
<ActionButton
label="Continue"
label={t("onboarding.continue")}
onClick={() => void continueToMission()}
icon={Buildings}
tone="accent"
@@ -358,39 +438,36 @@ export function CompanyOnboarding({
className="surface w-full rounded-[12px] p-7"
>
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
Step 2 of 3
{t("onboarding.stepLabel2")} 2 / 3
</div>
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
Define the first real goal
</div>
<div className="mb-2 text-[13px] text-[color:var(--muted)]">
The first goal seeds the operating project and the first task. This should be specific enough that the CEO agent can decompose it into concrete work.
{t("onboarding.step2Title")}
</div>
<div className="mb-6 h-px bg-[color:var(--line)]" />
<div className="space-y-4">
<Input
label="Goal"
label={t("onboarding.goal")}
value={goalTitle}
onChange={setGoalTitle}
placeholder="e.g. Ship the first customer-ready desktop demo"
placeholder={t("onboarding.goalPlaceholder")}
/>
<TextArea
label="Acceptance criteria"
label={t("onboarding.acceptanceCriteria")}
value={goalDescription}
onChange={setGoalDescription}
placeholder="Describe what successful execution looks like, what cannot be skipped, and any delivery constraints."
placeholder={t("onboarding.acceptanceCriteriaPlaceholder")}
rows={5}
/>
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3 text-[12px] text-[color:var(--muted)]">
<div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">
<Cpu size={12} />
Execution handoff
{t("onboarding.executionHandoff")}
</div>
<div className="space-y-2">
<div>The selected connector becomes the CEO agent runtime for the first launch.</div>
<div>The app will create the company, the primary workspace, the CEO agent, the kickoff project, and the first task.</div>
<div>If the worker is already ready, the first run will be queued immediately; otherwise the scheduler will pick it up once the runtime is available.</div>
<div>{t("onboarding.executionHandoffLine1")}</div>
<div>{t("onboarding.executionHandoffLine2")}</div>
<div>{t("onboarding.executionHandoffLine3")}</div>
</div>
</div>
</div>
@@ -407,10 +484,10 @@ export function CompanyOnboarding({
onClick={() => setStep("company")}
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
>
Back
{t("onboarding.back")}
</button>
<ActionButton
label={loading ? "Launching..." : "Launch Company"}
label={loading ? t("onboarding.launching") : t("onboarding.launchCompany")}
onClick={() => void launchCompany()}
icon={Lightning}
tone="accent"
@@ -433,42 +510,41 @@ export function CompanyOnboarding({
<CheckCircle size={24} weight="fill" />
</div>
<h2 className="mt-5 text-[28px] tracking-[-0.03em] text-[color:var(--text)]">
{launchSummary.companyName} is live.
{launchSummary.companyName} {t("onboarding.isLive")}
</h2>
<p className="mt-3 text-[14px] leading-relaxed text-[color:var(--muted)]">
The company shell is created. The first project and task are registered, and the kickoff execution is
{launchSummary.runId ? " queued." : " prepared for deferred execution once the runtime is ready."}
{launchSummary.runId ? t("onboarding.launchDescription") : t("onboarding.launchDescriptionDeferred")}
</p>
<div className="mt-6 grid gap-3 md:grid-cols-2">
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Connector</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.connector")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{selectedConnector?.label ?? "Unavailable"}
{selectedConnector?.label ?? t("onboarding.unavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Lead agent</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.leadAgent")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.leadAgentId ? "CEO agent created" : "Lead agent unavailable"}
{launchSummary.leadAgentId ? t("onboarding.ceoAgentCreated") : t("onboarding.leadAgentUnavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Primary workspace</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.primaryWorkspace")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.workspaceId ? "Registered" : "Not bound"}
{launchSummary.workspaceId ? t("onboarding.registered") : t("onboarding.notBound")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Kickoff project</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.kickoffProject")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.projectId ? "Created" : "Unavailable"}
{launchSummary.projectId ? t("onboarding.created") : t("onboarding.unavailable")}
</div>
</div>
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Initial run</div>
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">{t("onboarding.initialRun")}</div>
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
{launchSummary.runId ? "Queued in orchestration" : "Deferred until runtime is ready"}
{launchSummary.runId ? t("onboarding.queuedInOrchestration") : t("onboarding.deferredUntilReady")}
</div>
</div>
</div>
@@ -481,7 +557,7 @@ export function CompanyOnboarding({
<div className="mt-8 flex items-center justify-end">
<ActionButton
label={loading ? "Opening..." : "Open Control Center"}
label={loading ? t("onboarding.opening") : t("onboarding.openControlCenter")}
onClick={() => void finishLaunch()}
icon={Rocket}
tone="accent"
+3 -1
View File
@@ -29,6 +29,7 @@ import type {
import { timeAgo } from "../lib/formatters"
import { DEPARTMENT_OPTIONS } from "@shared/constants"
import { ActionButton, Select } from "./ui"
import { useT } from "../i18n"
const ACCENT = "var(--accent)"
const LINE = "var(--line)"
@@ -1611,6 +1612,7 @@ export function AutomationPanel({
companyId,
onRefresh,
}: AutomationPanelProps) {
const t = useT()
const [activeSection, setActiveSection] = useState<"rules" | "workflows">("rules")
return (
@@ -1630,7 +1632,7 @@ export function AutomationPanel({
<Robot size={20} weight="fill" />
</div>
<div>
<div className="text-[13px] font-semibold text-[color:var(--text)]">Automation Engine</div>
<div className="text-[13px] font-semibold text-[color:var(--text)]">{t("auto.automationEngine")}</div>
<div className="mt-0.5 text-[11px] leading-relaxed text-[color:var(--muted)]">
Rules trigger actions based on company events. Workflows chain multiple steps into pipelines.
Together, they let your AI company run fully autonomously -- responding to changes, routing work,
+47 -43
View File
@@ -34,6 +34,7 @@ import { getAgentInitials, formatMessageTime } from "../lib/formatters"
import { DEPARTMENT_COLORS } from "@shared/constants"
import { unwrap } from "../lib/desktop"
import { InlineNotice, Modal, Select, Input, TextArea } from "./ui"
import { useT } from "../i18n"
const TERRACOTTA = "#d97745"
const TERRACOTTA_SOFT = "#d9774518"
@@ -815,6 +816,7 @@ interface ComposeModalProps {
}
function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: ComposeModalProps) {
const t = useT()
const [fromAgentId, setFromAgentId] = useState(agents[0]?.id ?? "")
const [toAgentId, setToAgentId] = useState("")
const [channel, setChannel] = useState<MessageChannel>("company")
@@ -893,11 +895,11 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
}, [agents, body, channel, channelTargetId, companyId, fromAgentId, onClose, onSent, priority, subject, toAgentId])
return (
<Modal open={open} title="Compose Message" onClose={onClose} width="560px">
<Modal open={open} title={t("comm.composeMessage")} onClose={onClose} width="560px">
<div className="space-y-4">
{sendError ? <InlineNotice message={sendError} /> : null}
<Select
label="From Agent"
label={t("comm.fromAgent")}
value={fromAgentId}
onChange={setFromAgentId}
options={agents.map((a) => ({
@@ -907,15 +909,15 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
/>
<Select
label="Channel"
label={t("comm.channel")}
value={channel}
onChange={(v) => setChannel(v as MessageChannel)}
options={[
{ value: "company", label: "Company-wide broadcast" },
{ value: "department", label: "Department channel" },
{ value: "project", label: "Project channel" },
{ value: "incident", label: "Incident channel" },
{ value: "direct", label: "Direct message" },
{ value: "company", label: t("comm.companyBroadcast") },
{ value: "department", label: t("comm.departmentChannel") },
{ value: "project", label: t("comm.projectChannel") },
{ value: "incident", label: t("comm.incidentChannel") },
{ value: "direct", label: t("comm.directMessage") },
]}
/>
@@ -963,17 +965,17 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
) : null}
<Input
label="Subject"
label={t("comm.subject")}
value={subject}
onChange={setSubject}
placeholder="Message subject"
placeholder={t("comm.messageSubject")}
/>
<TextArea
label="Message"
label={t("comm.message")}
value={body}
onChange={setBody}
placeholder="Write your message..."
placeholder={t("comm.writeMessage")}
rows={5}
/>
@@ -994,7 +996,7 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
onClick={onClose}
className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]"
>
Cancel
{t("common.cancel")}
</button>
<button
type="button"
@@ -1018,7 +1020,7 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
) : (
<PaperPlaneTilt size={14} weight="fill" />
)}
Send Message
{t("comm.sendMessage")}
</button>
</div>
</div>
@@ -1040,6 +1042,7 @@ function FilterBar({
filters: FilterState
onChangeFilters: (next: FilterState) => void
}) {
const t = useT()
const [expanded, setExpanded] = useState(false)
return (
@@ -1057,7 +1060,7 @@ function FilterBar({
onChange={(e) =>
onChangeFilters({ ...filters, search: e.target.value })
}
placeholder="Search messages..."
placeholder={t("comm.searchMessages")}
className="w-full rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] py-1.5 pl-8 pr-3 text-[11px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] focus:outline-none focus:ring-1"
style={{ focusRingColor: TERRACOTTA } as React.CSSProperties}
/>
@@ -1079,7 +1082,7 @@ function FilterBar({
}
>
<Funnel size={12} />
Filters
{t("comm.filters")}
</button>
</div>
@@ -1102,12 +1105,12 @@ function FilterBar({
}
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[10px] font-medium text-[color:var(--text)]"
>
<option value="all">All channels</option>
<option value="direct">Direct</option>
<option value="department">Department</option>
<option value="company">Company</option>
<option value="project">Project</option>
<option value="incident">Incident</option>
<option value="all">{t("comm.allChannels")}</option>
<option value="direct">{t("comm.direct")}</option>
<option value="department">{t("comm.department")}</option>
<option value="company">{t("comm.company")}</option>
<option value="project">{t("nav.projects")}</option>
<option value="incident">{t("comm.incident")}</option>
</select>
<select
@@ -1120,10 +1123,10 @@ function FilterBar({
}
className="rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)] px-2 py-1 text-[10px] font-medium text-[color:var(--text)]"
>
<option value="all">All priorities</option>
<option value="urgent">Urgent</option>
<option value="normal">Normal</option>
<option value="low">Low</option>
<option value="all">{t("comm.allPriorities")}</option>
<option value="urgent">{t("comm.urgent")}</option>
<option value="normal">{t("comm.normal")}</option>
<option value="low">{t("comm.low")}</option>
</select>
<button
@@ -1138,7 +1141,7 @@ function FilterBar({
}`}
style={filters.unreadOnly ? { backgroundColor: TERRACOTTA } : undefined}
>
Unread only
{t("comm.unreadOnly")}
</button>
{(filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly) ? (
@@ -1154,7 +1157,7 @@ function FilterBar({
}
className="text-[10px] font-medium text-[color:var(--muted)] underline transition hover:text-[color:var(--text)]"
>
Clear filters
{t("comm.clearFilters")}
</button>
) : null}
</motion.div>
@@ -1189,6 +1192,7 @@ export function CommunicationHub({
onNavigateToTask,
onRefresh,
}: CommunicationHubProps) {
const t = useT()
const [selectedChannelKey, setSelectedChannelKey] = useState<string | null>(null)
const [showSidebar, setShowSidebar] = useState(true)
const [showCompose, setShowCompose] = useState(false)
@@ -1447,7 +1451,7 @@ export function CommunicationHub({
</div>
<div>
<h2 className="text-[16px] font-bold text-[color:var(--text)]">
Communication Hub
{t("comm.communicationHub")}
</h2>
<p className="text-[12px] text-[color:var(--muted)]">
{messages.length} message{messages.length !== 1 ? "s" : ""}
@@ -1464,7 +1468,7 @@ export function CommunicationHub({
style={{ backgroundColor: TERRACOTTA }}
>
<Plus size={13} weight="bold" />
New message
{t("comm.newMessage")}
</button>
</div>
@@ -1486,7 +1490,7 @@ export function CommunicationHub({
{/* Sidebar header */}
<div className="flex items-center justify-between border-b border-[color:var(--line)] px-4 py-3">
<span className="text-[13px] font-semibold text-[color:var(--text)]">
Channels
{t("comm.channels")}
</span>
<button
type="button"
@@ -1527,7 +1531,7 @@ export function CommunicationHub({
{taskDiscussionChannels.length > 0 ? (
<div className="mb-3">
<div className="mb-1 px-2 text-[9px] font-bold uppercase tracking-[0.1em] text-[color:var(--muted)]">
Task Discussions
{t("comm.taskDiscussions")}
</div>
{taskDiscussionChannels.map((discussion) => (
<button
@@ -1551,7 +1555,7 @@ export function CommunicationHub({
{channels.length === 0 ? (
<div className="px-3 py-6 text-center text-[11px] text-[color:var(--muted)]">
No active departments yet.
{t("comm.noActiveDepartments")}
</div>
) : null}
</div>
@@ -1622,7 +1626,7 @@ export function CommunicationHub({
</>
) : (
<span className="text-[13px] text-[color:var(--muted)]">
Select a channel
{t("comm.selectChannel")}
</span>
)}
@@ -1641,10 +1645,10 @@ export function CommunicationHub({
className="mb-3 text-[color:var(--muted)]"
/>
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
Select a channel to view messages
{t("comm.selectChannelView")}
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
Monitor internal agent communications across departments
{t("comm.monitorComms")}
</div>
</div>
) : activeChannel.kind === "task" ? (
@@ -1652,10 +1656,10 @@ export function CommunicationHub({
<div className="flex h-full flex-col items-center justify-center text-center">
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
No discussion yet
{t("comm.noDiscussionYet")}
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
Comments for this task will appear here as agents and the board coordinate.
{t("comm.commentsAppearHere")}
</div>
</div>
) : (
@@ -1675,13 +1679,13 @@ export function CommunicationHub({
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
{filters.search || filters.unreadOnly || filters.priority !== "all"
? "No messages match your filters"
: "No messages in this channel yet"}
? t("comm.noMatchFilters")
: t("comm.noMessagesChannel")}
</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
{filters.search || filters.unreadOnly || filters.priority !== "all"
? "Try adjusting your search or filters"
: "Messages will appear here as agents work and communicate"}
? t("comm.adjustFilters")
: t("comm.messagesAppear")}
</div>
</div>
) : (
@@ -1746,7 +1750,7 @@ export function CommunicationHub({
<div className="border-t border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-2.5">
<div className="flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
<ChatCircle size={14} />
<span>Select a channel to read and send messages.</span>
<span>{t("comm.selectChannelSend")}</span>
</div>
</div>
)}
@@ -19,6 +19,7 @@ import type {
SprintRecord,
} from "@shared/types";
import { ActionButton, ConfirmDialog, InlineNotice, Input, ListFrame, ListRow, Modal, Select, StatusPill, TextArea } from "./ui";
import { useT } from "../i18n";
const documentTypeOptions = [
"prd",
@@ -450,13 +451,14 @@ export function DocumentCenter({
goals: GoalRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeDocument = useMemo(() => documents.find((document) => document.id === editingId) ?? null, [documents, editingId]);
return (
<div>
<SectionHeader title="Document Center" count={documents.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.documentCenter")} count={documents.length} action={() => setEditingId("__new__")} />
<ListFrame>
{documents.map((document) => (
<ListRow key={document.id}>
@@ -521,13 +523,14 @@ export function KnowledgeBaseManager({
entries: KnowledgeEntryRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeEntry = useMemo(() => entries.find((entry) => entry.id === editingId) ?? null, [entries, editingId]);
return (
<div>
<SectionHeader title="Knowledge Base" count={entries.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.knowledgeBase")} count={entries.length} action={() => setEditingId("__new__")} />
<ListFrame>
{entries.map((entry) => (
<ListRow key={entry.id}>
@@ -591,13 +594,14 @@ export function MeetingsManager({
agents: AgentRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeMeeting = useMemo(() => meetings.find((meeting) => meeting.id === editingId) ?? null, [meetings, editingId]);
return (
<div>
<SectionHeader title="Meetings" count={meetings.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.meetings")} count={meetings.length} action={() => setEditingId("__new__")} />
<ListFrame>
{meetings.map((meeting) => (
<ListRow key={meeting.id}>
@@ -660,13 +664,14 @@ export function SprintManager({
sprints: SprintRecord[];
onRefresh: () => Promise<void>;
}) {
const t = useT();
const [editingId, setEditingId] = useState<string | null>(null);
const [deleteId, setDeleteId] = useState<string | null>(null);
const activeSprint = useMemo(() => sprints.find((sprint) => sprint.id === editingId) ?? null, [sprints, editingId]);
return (
<div>
<SectionHeader title="Sprint Board" count={sprints.length} action={() => setEditingId("__new__")} />
<SectionHeader title={t("ops.sprintBoard")} count={sprints.length} action={() => setEditingId("__new__")} />
<ListFrame>
{sprints.map((sprint) => (
<ListRow key={sprint.id}>
+5 -3
View File
@@ -3,6 +3,7 @@ import { CheckCircle, SignIn } from "@phosphor-icons/react";
import type { ConnectorRecord } from "@shared/types";
import { unwrap } from "../lib/desktop";
import { ActionButton, InlineNotice, Input, Select, StatusPill, TextArea } from "./ui";
import { useT } from "../i18n";
function getAuthActionCopy(connector: ConnectorRecord) {
switch (connector.id) {
@@ -40,6 +41,7 @@ function getAuthActionCopy(connector: ConnectorRecord) {
}
export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRecord; onSaved: () => Promise<void> }) {
const t = useT();
const [model, setModel] = useState(connector.model ?? "");
const [envBindingText, setEnvBindingText] = useState(connector.envBindingText);
const [notes, setNotes] = useState(connector.notes);
@@ -173,14 +175,14 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
) : null}
<div className="grid gap-4 md:grid-cols-2">
<div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">Status</div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{t("conn.status")}</div>
<div className="flex items-center gap-3">
<StatusPill status={connector.status} />
<span className="text-[12px] text-[color:var(--muted)]">{connector.version ?? "version unknown"}</span>
</div>
</div>
<div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">Capability matrix</div>
<div className="mb-1.5 text-[12px] font-medium text-[color:var(--muted-strong)]">{t("conn.capabilityMatrix")}</div>
<div className="grid grid-cols-2 gap-2">
{Object.entries(connector.capabilityMatrix).map(([key, value]) => (
<div key={key} className={`rounded-[6px] border px-3 py-2 text-[12px] ${value ? "border-[color:var(--accent)] border-opacity-20 bg-[color:var(--accent-soft)] text-[color:var(--accent)]" : "border-[color:var(--line)] bg-[color:var(--panel-soft)] text-[color:var(--muted)]"}`}>
@@ -233,7 +235,7 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
) : null}
<div className="flex gap-3">
<ActionButton
label="Save connector"
label={t("conn.saveConnector")}
tone="accent"
onClick={() => void (async () => {
setSaving(true);
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more