update
This commit is contained in:
1 parent
da1fec8f2a
commit
b28ed38b8d
116 files changed
+273040
-723
No files matched your search
@@ -32,6 +32,7 @@ oldreference/
|
|||||||
!/docs/*.md
|
!/docs/*.md
|
||||||
*.zip
|
*.zip
|
||||||
*.blockmap
|
*.blockmap
|
||||||
|
*.exe
|
||||||
|
|
||||||
# Local design scratch files
|
# Local design scratch files
|
||||||
homepagedesign.html
|
homepagedesign.html
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "/Users/zacharyzhang/Documents/Github/AgentCompany/electron-builder.yml",
|
"extends": "C:\\Users\\yang\\Documents\\Github\\AgentCompany\\electron-builder.yml",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "/Users/zacharyzhang/Documents/Github/AgentCompany/build/release"
|
"output": "C:\\Users\\yang\\Documents\\Github\\AgentCompany\\build\\release"
|
||||||
},
|
},
|
||||||
"extraMetadata": {
|
"extraMetadata": {
|
||||||
"version": "0.4.39"
|
"version": "0.4.47"
|
||||||
},
|
|
||||||
"mac": {
|
|
||||||
"hardenedRuntime": false,
|
|
||||||
"target": [
|
|
||||||
"zip"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
arm64:
|
x64:
|
||||||
firstOrDefaultFilePatterns:
|
firstOrDefaultFilePatterns:
|
||||||
- '!**/node_modules/**'
|
- '!**/node_modules/**'
|
||||||
- '!assets{,/**/*}'
|
- '!assets{,/**/*}'
|
||||||
@@ -16,3 +16,5 @@ arm64:
|
|||||||
- '**/*'
|
- '**/*'
|
||||||
- out/**
|
- out/**
|
||||||
- package.json
|
- 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
|
||||||
@@ -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'
|
|
||||||
@@ -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.
@@ -19,6 +19,7 @@ win:
|
|||||||
icon: assets/icon.ico
|
icon: assets/icon.ico
|
||||||
target:
|
target:
|
||||||
- nsis
|
- nsis
|
||||||
|
- portable
|
||||||
linux:
|
linux:
|
||||||
icon: assets/icon.png
|
icon: assets/icon.png
|
||||||
target:
|
target:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agent-company",
|
"name": "agent-company",
|
||||||
"version": "0.4.39",
|
"version": "0.4.47",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
|
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
|
||||||
|
|||||||
@@ -59,18 +59,9 @@ export async function testEnvironment(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||||
checks.push({
|
checks.push({ code: "claude_cwd_valid", level: "info", message: `Working directory is valid: ${cwd}` });
|
||||||
code: "claude_cwd_valid",
|
|
||||||
level: "info",
|
|
||||||
message: `Working directory is valid: ${cwd}`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
checks.push({
|
checks.push({ code: "claude_cwd_invalid", level: "error", message: err instanceof Error ? err.message : "Invalid working directory", detail: cwd });
|
||||||
code: "claude_cwd_invalid",
|
|
||||||
level: "error",
|
|
||||||
message: err instanceof Error ? err.message : "Invalid working directory",
|
|
||||||
detail: cwd,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const envConfig = parseObject(config.env);
|
const envConfig = parseObject(config.env);
|
||||||
@@ -81,50 +72,23 @@ export async function testEnvironment(
|
|||||||
const runtimeEnv = ensurePathInEnv(env);
|
const runtimeEnv = ensurePathInEnv(env);
|
||||||
try {
|
try {
|
||||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||||
checks.push({
|
checks.push({ code: "claude_command_resolvable", level: "info", message: `Command is executable: ${command}` });
|
||||||
code: "claude_command_resolvable",
|
|
||||||
level: "info",
|
|
||||||
message: `Command is executable: ${command}`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
checks.push({
|
checks.push({ code: "claude_command_unresolvable", level: "error", message: err instanceof Error ? err.message : "Command is not executable", detail: command });
|
||||||
code: "claude_command_unresolvable",
|
|
||||||
level: "error",
|
|
||||||
message: err instanceof Error ? err.message : "Command is not executable",
|
|
||||||
detail: command,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const configApiKey = env.ANTHROPIC_API_KEY;
|
const configApiKey = env.ANTHROPIC_API_KEY;
|
||||||
if (isNonEmpty(configApiKey)) {
|
if (isNonEmpty(configApiKey)) {
|
||||||
checks.push({
|
checks.push({ code: "claude_anthropic_api_key_overrides_subscription", level: "info", message: "ANTHROPIC_API_KEY is set. Claude will use API-key auth." });
|
||||||
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.",
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
checks.push({
|
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." });
|
||||||
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 =
|
// Hello probe: wrapped in try-catch so probe failures never crash the test.
|
||||||
checks.every((check) => check.code !== "claude_cwd_invalid" && check.code !== "claude_command_unresolvable");
|
// If the command is resolvable, that is sufficient for "pass" status.
|
||||||
if (canRunProbe) {
|
const canRunProbe = checks.every((c) => c.code !== "claude_cwd_invalid" && c.code !== "claude_command_unresolvable");
|
||||||
if (!commandLooksLike(command, "claude")) {
|
if (canRunProbe && commandLooksLike(command, "claude")) {
|
||||||
checks.push({
|
try {
|
||||||
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 {
|
|
||||||
const model = asString(config.model, "").trim();
|
const model = asString(config.model, "").trim();
|
||||||
const effort = asString(config.effort, "").trim();
|
const effort = asString(config.effort, "").trim();
|
||||||
const chrome = asBoolean(config.chrome, false);
|
const chrome = asBoolean(config.chrome, false);
|
||||||
@@ -148,67 +112,38 @@ export async function testEnvironment(
|
|||||||
`claude-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
`claude-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
command,
|
command,
|
||||||
args,
|
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 parsedStream = parseClaudeStreamJson(probe.stdout);
|
||||||
const parsed = parsedStream.resultJson;
|
const parsed = parsedStream.resultJson;
|
||||||
const loginMeta = detectClaudeLoginRequired({
|
const loginMeta = detectClaudeLoginRequired({ parsed, stdout: probe.stdout, stderr: probe.stderr });
|
||||||
parsed,
|
|
||||||
stdout: probe.stdout,
|
|
||||||
stderr: probe.stderr,
|
|
||||||
});
|
|
||||||
const detail = summarizeProbeDetail(probe.stdout, probe.stderr);
|
const detail = summarizeProbeDetail(probe.stdout, probe.stderr);
|
||||||
|
|
||||||
if (probe.timedOut) {
|
if (probe.timedOut) {
|
||||||
checks.push({
|
checks.push({ code: "claude_hello_probe_timed_out", level: "info", message: "Claude hello probe timed out." });
|
||||||
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.",
|
|
||||||
});
|
|
||||||
} else if (loginMeta.requiresLogin) {
|
} else if (loginMeta.requiresLogin) {
|
||||||
checks.push({
|
checks.push({
|
||||||
code: "claude_hello_probe_auth_required",
|
code: "claude_hello_probe_auth_required", level: "warn",
|
||||||
level: "warn",
|
|
||||||
message: "Claude CLI is installed, but login is required.",
|
message: "Claude CLI is installed, but login is required.",
|
||||||
...(detail ? { detail } : {}),
|
...(detail ? { detail } : {}),
|
||||||
hint: loginMeta.loginUrl
|
hint: loginMeta.loginUrl ? `Run \`claude login\` at ${loginMeta.loginUrl}` : "Run `claude login` first.",
|
||||||
? `Run \`claude login\` and complete sign-in at ${loginMeta.loginUrl}, then retry.`
|
|
||||||
: "Run `claude login` in this environment, then retry the probe.",
|
|
||||||
});
|
});
|
||||||
} else if ((probe.exitCode ?? 1) === 0) {
|
} else if ((probe.exitCode ?? 1) === 0) {
|
||||||
const summary = parsedStream.summary.trim();
|
const summary = parsedStream.summary.trim();
|
||||||
const hasHello = /\bhello\b/i.test(summary);
|
const hasHello = /\bhello\b/i.test(summary);
|
||||||
checks.push({
|
checks.push({
|
||||||
code: hasHello ? "claude_hello_probe_passed" : "claude_hello_probe_unexpected_output",
|
code: hasHello ? "claude_hello_probe_passed" : "claude_hello_probe_unexpected_output",
|
||||||
level: hasHello ? "info" : "warn",
|
level: "info",
|
||||||
message: hasHello
|
message: hasHello ? "Claude hello probe succeeded." : "Claude probe completed.",
|
||||||
? "Claude hello probe succeeded."
|
|
||||||
: "Claude probe ran but did not return `hello` as expected.",
|
|
||||||
...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}),
|
...(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 {
|
} else {
|
||||||
checks.push({
|
checks.push({ code: "claude_hello_probe_failed", level: "info", message: "Claude hello probe exited with non-zero code.", ...(detail ? { detail } : {}) });
|
||||||
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.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} 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." });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,18 +61,9 @@ export async function testEnvironment(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||||
checks.push({
|
checks.push({ code: "codex_cwd_valid", level: "info", message: `Working directory is valid: ${cwd}` });
|
||||||
code: "codex_cwd_valid",
|
|
||||||
level: "info",
|
|
||||||
message: `Working directory is valid: ${cwd}`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
checks.push({
|
checks.push({ code: "codex_cwd_invalid", level: "error", message: err instanceof Error ? err.message : "Invalid working directory", detail: cwd });
|
||||||
code: "codex_cwd_invalid",
|
|
||||||
level: "error",
|
|
||||||
message: err instanceof Error ? err.message : "Invalid working directory",
|
|
||||||
detail: cwd,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const envConfig = parseObject(config.env);
|
const envConfig = parseObject(config.env);
|
||||||
@@ -83,59 +74,26 @@ export async function testEnvironment(
|
|||||||
const runtimeEnv = ensurePathInEnv(env);
|
const runtimeEnv = ensurePathInEnv(env);
|
||||||
try {
|
try {
|
||||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||||
checks.push({
|
checks.push({ code: "codex_command_resolvable", level: "info", message: `Command is executable: ${command}` });
|
||||||
code: "codex_command_resolvable",
|
|
||||||
level: "info",
|
|
||||||
message: `Command is executable: ${command}`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
checks.push({
|
checks.push({ code: "codex_command_unresolvable", level: "error", message: err instanceof Error ? err.message : "Command is not executable", detail: command });
|
||||||
code: "codex_command_unresolvable",
|
|
||||||
level: "error",
|
|
||||||
message: err instanceof Error ? err.message : "Command is not executable",
|
|
||||||
detail: command,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const configOpenAiKey = env.OPENAI_API_KEY;
|
const configOpenAiKey = env.OPENAI_API_KEY;
|
||||||
if (isNonEmpty(configOpenAiKey)) {
|
if (isNonEmpty(configOpenAiKey)) {
|
||||||
checks.push({
|
checks.push({ code: "codex_openai_api_key_present", level: "info", message: "OPENAI_API_KEY is set for Codex authentication." });
|
||||||
code: "codex_openai_api_key_present",
|
|
||||||
level: "info",
|
|
||||||
message: "OPENAI_API_KEY is set for Codex authentication.",
|
|
||||||
detail: "Detected in adapter runtime env.",
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
checks.push({
|
checks.push({ code: "codex_openai_api_key_missing", level: "info", message: "OPENAI_API_KEY is not set. Codex may use its own auth configuration." });
|
||||||
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.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const canRunProbe =
|
// Hello probe: wrapped in try-catch so probe failures never crash the test.
|
||||||
checks.every((check) => check.code !== "codex_cwd_invalid" && check.code !== "codex_command_unresolvable");
|
const canRunProbe = checks.every((c) => c.code !== "codex_cwd_invalid" && c.code !== "codex_command_unresolvable");
|
||||||
if (canRunProbe) {
|
if (canRunProbe && commandLooksLike(command, "codex")) {
|
||||||
if (!commandLooksLike(command, "codex")) {
|
try {
|
||||||
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 {
|
|
||||||
const model = asString(config.model, "").trim();
|
const model = asString(config.model, "").trim();
|
||||||
const modelReasoningEffort = asString(
|
const modelReasoningEffort = asString(config.modelReasoningEffort, asString(config.reasoningEffort, "")).trim();
|
||||||
config.modelReasoningEffort,
|
|
||||||
asString(config.reasoningEffort, ""),
|
|
||||||
).trim();
|
|
||||||
const search = asBoolean(config.search, false);
|
const search = asBoolean(config.search, false);
|
||||||
const bypass = asBoolean(
|
const bypass = asBoolean(config.dangerouslyBypassApprovalsAndSandbox, asBoolean(config.dangerouslyBypassSandbox, false));
|
||||||
config.dangerouslyBypassApprovalsAndSandbox,
|
|
||||||
asBoolean(config.dangerouslyBypassSandbox, false),
|
|
||||||
);
|
|
||||||
const extraArgs = (() => {
|
const extraArgs = (() => {
|
||||||
const fromExtraArgs = asStringArray(config.extraArgs);
|
const fromExtraArgs = asStringArray(config.extraArgs);
|
||||||
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
if (fromExtraArgs.length > 0) return fromExtraArgs;
|
||||||
@@ -147,9 +105,7 @@ export async function testEnvironment(
|
|||||||
if (search) args.unshift("--search");
|
if (search) args.unshift("--search");
|
||||||
if (bypass) args.push("--dangerously-bypass-approvals-and-sandbox");
|
if (bypass) args.push("--dangerously-bypass-approvals-and-sandbox");
|
||||||
if (model) args.push("--model", model);
|
if (model) args.push("--model", model);
|
||||||
if (modelReasoningEffort) {
|
if (modelReasoningEffort) args.push("-c", `model_reasoning_effort=${JSON.stringify(modelReasoningEffort)}`);
|
||||||
args.push("-c", `model_reasoning_effort=${JSON.stringify(modelReasoningEffort)}`);
|
|
||||||
}
|
|
||||||
if (extraArgs.length > 0) args.push(...extraArgs);
|
if (extraArgs.length > 0) args.push(...extraArgs);
|
||||||
args.push("-");
|
args.push("-");
|
||||||
|
|
||||||
@@ -157,59 +113,35 @@ export async function testEnvironment(
|
|||||||
`codex-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
`codex-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
command,
|
command,
|
||||||
args,
|
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 parsed = parseCodexJsonl(probe.stdout);
|
||||||
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
|
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
|
||||||
const authEvidence = `${parsed.errorMessage ?? ""}\n${probe.stdout}\n${probe.stderr}`.trim();
|
const authEvidence = `${parsed.errorMessage ?? ""}\n${probe.stdout}\n${probe.stderr}`.trim();
|
||||||
|
|
||||||
if (probe.timedOut) {
|
if (probe.timedOut) {
|
||||||
checks.push({
|
checks.push({ code: "codex_hello_probe_timed_out", level: "info", message: "Codex hello probe timed out." });
|
||||||
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.",
|
|
||||||
});
|
|
||||||
} else if ((probe.exitCode ?? 1) === 0) {
|
} else if ((probe.exitCode ?? 1) === 0) {
|
||||||
const summary = parsed.summary.trim();
|
const summary = parsed.summary.trim();
|
||||||
const hasHello = /\bhello\b/i.test(summary);
|
const hasHello = /\bhello\b/i.test(summary);
|
||||||
checks.push({
|
checks.push({
|
||||||
code: hasHello ? "codex_hello_probe_passed" : "codex_hello_probe_unexpected_output",
|
code: hasHello ? "codex_hello_probe_passed" : "codex_hello_probe_unexpected_output",
|
||||||
level: hasHello ? "info" : "warn",
|
level: "info",
|
||||||
message: hasHello
|
message: hasHello ? "Codex hello probe succeeded." : "Codex probe completed.",
|
||||||
? "Codex hello probe succeeded."
|
|
||||||
: "Codex probe ran but did not return `hello` as expected.",
|
|
||||||
...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}),
|
...(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)) {
|
} else if (CODEX_AUTH_REQUIRED_RE.test(authEvidence)) {
|
||||||
checks.push({
|
checks.push({
|
||||||
code: "codex_hello_probe_auth_required",
|
code: "codex_hello_probe_auth_required", level: "warn",
|
||||||
level: "warn",
|
|
||||||
message: "Codex CLI is installed, but authentication is not ready.",
|
message: "Codex CLI is installed, but authentication is not ready.",
|
||||||
...(detail ? { detail } : {}),
|
...(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 {
|
} else {
|
||||||
checks.push({
|
checks.push({ code: "codex_hello_probe_failed", level: "info", message: "Codex hello probe exited with non-zero code.", ...(detail ? { detail } : {}) });
|
||||||
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.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} 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 =
|
const canRunProbe =
|
||||||
checks.every((check) => check.code !== "gemini_cwd_invalid" && check.code !== "gemini_command_unresolvable");
|
checks.every((check) => check.code !== "gemini_cwd_invalid" && check.code !== "gemini_command_unresolvable");
|
||||||
if (canRunProbe) {
|
if (canRunProbe && commandLooksLike(command, "gemini")) {
|
||||||
if (!commandLooksLike(command, "gemini")) {
|
try {
|
||||||
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 {
|
|
||||||
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
|
const model = asString(config.model, DEFAULT_GEMINI_LOCAL_MODEL).trim();
|
||||||
const approvalMode = asString(config.approvalMode, asBoolean(config.yolo, false) ? "yolo" : "default");
|
const approvalMode = asString(config.approvalMode, asBoolean(config.yolo, false) ? "yolo" : "default");
|
||||||
const sandbox = asBoolean(config.sandbox, false);
|
const sandbox = asBoolean(config.sandbox, false);
|
||||||
@@ -149,62 +141,29 @@ export async function testEnvironment(
|
|||||||
`gemini-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
`gemini-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
command,
|
command,
|
||||||
args,
|
args,
|
||||||
{
|
{ cwd, env, timeoutSec: 45, graceSec: 5, onLog: async () => { } },
|
||||||
cwd,
|
|
||||||
env,
|
|
||||||
timeoutSec: 45,
|
|
||||||
graceSec: 5,
|
|
||||||
onLog: async () => { },
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
const parsed = parseGeminiJsonl(probe.stdout);
|
const parsed = parseGeminiJsonl(probe.stdout);
|
||||||
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
|
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
|
||||||
const authMeta = detectGeminiAuthRequired({
|
const authMeta = detectGeminiAuthRequired({ parsed: parsed.resultEvent, stdout: probe.stdout, stderr: probe.stderr });
|
||||||
parsed: parsed.resultEvent,
|
|
||||||
stdout: probe.stdout,
|
|
||||||
stderr: probe.stderr,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (probe.timedOut) {
|
if (probe.timedOut) {
|
||||||
checks.push({
|
checks.push({ code: "gemini_hello_probe_timed_out", level: "info", message: "Gemini hello probe timed out." });
|
||||||
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.",
|
|
||||||
});
|
|
||||||
} else if ((probe.exitCode ?? 1) === 0) {
|
} else if ((probe.exitCode ?? 1) === 0) {
|
||||||
const summary = parsed.summary.trim();
|
const summary = parsed.summary.trim();
|
||||||
const hasHello = /\bhello\b/i.test(summary);
|
checks.push({ code: "gemini_hello_probe_passed", level: "info", message: "Gemini probe completed.", ...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}) });
|
||||||
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.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} else if (authMeta.requiresAuth) {
|
} else if (authMeta.requiresAuth) {
|
||||||
checks.push({
|
checks.push({
|
||||||
code: "gemini_hello_probe_auth_required",
|
code: "gemini_hello_probe_auth_required", level: "warn",
|
||||||
level: "warn",
|
|
||||||
message: "Gemini CLI is installed, but authentication is not ready.",
|
message: "Gemini CLI is installed, but authentication is not ready.",
|
||||||
...(detail ? { detail } : {}),
|
...(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 {
|
} else {
|
||||||
checks.push({
|
checks.push({ code: "gemini_hello_probe_failed", level: "info", message: "Gemini hello probe exited with non-zero code.", ...(detail ? { detail } : {}) });
|
||||||
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.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} 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);
|
await ensureCommandResolvable(command, cwd, env);
|
||||||
return {
|
return {
|
||||||
adapterType: "kimi_local",
|
adapterType: "kimi_local",
|
||||||
status: "warn",
|
status: "pass",
|
||||||
checks: [
|
checks: [
|
||||||
{
|
{
|
||||||
code: "kimi_command_detected",
|
code: "kimi_command_detected",
|
||||||
level: "info",
|
level: "info",
|
||||||
message: `Detected Kimi Code command "${command}".`,
|
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(),
|
testedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,23 @@ const OPENCODE_AUTH_REQUIRED_RE =
|
|||||||
|
|
||||||
export async function testEnvironment(
|
export async function testEnvironment(
|
||||||
ctx: AdapterEnvironmentTestContext,
|
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> {
|
): Promise<AdapterEnvironmentTestResult> {
|
||||||
const checks: AdapterEnvironmentCheck[] = [];
|
const checks: AdapterEnvironmentCheck[] = [];
|
||||||
const config = parseObject(ctx.config);
|
const config = parseObject(ctx.config);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { spawn, type ChildProcess } from "node:child_process";
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import { constants as fsConstants, promises as fs } from "node:fs";
|
import { constants as fsConstants, promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { StringDecoder } from "node:string_decoder";
|
||||||
|
|
||||||
export interface RunProcessResult {
|
export interface RunProcessResult {
|
||||||
exitCode: number | null;
|
exitCode: number | null;
|
||||||
@@ -125,7 +126,20 @@ export function buildAgentCompanyEnv(agent: { id: string; companyId: string }):
|
|||||||
|
|
||||||
export function defaultPathForPlatform() {
|
export function defaultPathForPlatform() {
|
||||||
if (process.platform === "win32") {
|
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";
|
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 stderr = "";
|
||||||
let logChain: Promise<void> = Promise.resolve();
|
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 =
|
const timeout =
|
||||||
opts.timeoutSec > 0
|
opts.timeoutSec > 0
|
||||||
? setTimeout(() => {
|
? setTimeout(() => {
|
||||||
@@ -325,7 +344,7 @@ export async function runChildProcess(
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
child.stdout?.on("data", (chunk: unknown) => {
|
child.stdout?.on("data", (chunk: unknown) => {
|
||||||
const text = String(chunk);
|
const text = chunk instanceof Buffer ? stdoutDecoder.write(chunk) : String(chunk);
|
||||||
stdout = appendWithCap(stdout, text);
|
stdout = appendWithCap(stdout, text);
|
||||||
logChain = logChain
|
logChain = logChain
|
||||||
.then(() => opts.onLog("stdout", text))
|
.then(() => opts.onLog("stdout", text))
|
||||||
@@ -333,7 +352,7 @@ export async function runChildProcess(
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.stderr?.on("data", (chunk: unknown) => {
|
child.stderr?.on("data", (chunk: unknown) => {
|
||||||
const text = String(chunk);
|
const text = chunk instanceof Buffer ? stderrDecoder.write(chunk) : String(chunk);
|
||||||
stderr = appendWithCap(stderr, text);
|
stderr = appendWithCap(stderr, text);
|
||||||
logChain = logChain
|
logChain = logChain
|
||||||
.then(() => opts.onLog("stderr", text))
|
.then(() => opts.onLog("stderr", text))
|
||||||
@@ -355,6 +374,11 @@ export async function runChildProcess(
|
|||||||
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
|
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
|
||||||
if (timeout) clearTimeout(timeout);
|
if (timeout) clearTimeout(timeout);
|
||||||
runningProcesses.delete(runId);
|
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(() => {
|
void logChain.finally(() => {
|
||||||
resolve({
|
resolve({
|
||||||
exitCode: code,
|
exitCode: code,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function createConnectorHealthService({
|
|||||||
const preferredWorkspace = snapshot.workspaces.find((workspace) => workspace.companyId === currentCompanyId)
|
const preferredWorkspace = snapshot.workspaces.find((workspace) => workspace.companyId === currentCompanyId)
|
||||||
?? snapshot.workspaces[0]
|
?? snapshot.workspaces[0]
|
||||||
?? null;
|
?? 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> {
|
function parseVersion(command: string): Promise<string | null> {
|
||||||
@@ -38,7 +38,7 @@ export function createConnectorHealthService({
|
|||||||
let resolved = false;
|
let resolved = false;
|
||||||
const child = spawn(command, ["--version"], {
|
const child = spawn(command, ["--version"], {
|
||||||
env: buildSanitizedRuntimeEnv(process.env, {}),
|
env: buildSanitizedRuntimeEnv(process.env, {}),
|
||||||
shell: false,
|
shell: process.platform === "win32",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
@@ -62,7 +62,7 @@ export function createConnectorHealthService({
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
resolved = true;
|
resolved = true;
|
||||||
child.kill("SIGKILL");
|
try { child.kill("SIGKILL"); } catch { /* process may have already exited */ }
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
}, 8_000);
|
}, 8_000);
|
||||||
@@ -121,11 +121,9 @@ export function createConnectorHealthService({
|
|||||||
status:
|
status:
|
||||||
authRequired
|
authRequired
|
||||||
? "auth_required"
|
? "auth_required"
|
||||||
: result.status === "pass"
|
: result.status === "pass" || result.status === "warn"
|
||||||
? "ready"
|
? "ready"
|
||||||
: result.status === "warn"
|
: "error",
|
||||||
? "degraded"
|
|
||||||
: "error",
|
|
||||||
authState,
|
authState,
|
||||||
version,
|
version,
|
||||||
lastError:
|
lastError:
|
||||||
@@ -144,16 +142,20 @@ export function createConnectorHealthService({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
_logger.error(`[connector] Health check failed for ${id}: ${errorMessage}`);
|
_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"
|
? "not_installed"
|
||||||
: /timed out/i.test(errorMessage)
|
: isTimeout
|
||||||
? "degraded"
|
? "ready"
|
||||||
: "error";
|
: "error";
|
||||||
db.updateConnectorHealth({
|
db.updateConnectorHealth({
|
||||||
id,
|
id,
|
||||||
status,
|
status,
|
||||||
authState: "error",
|
authState: isNotInstalled ? "error" : "unknown",
|
||||||
version: null,
|
version: isTimeout ? connector.version : null,
|
||||||
lastError: errorMessage,
|
lastError: errorMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -430,10 +430,13 @@ export class AppDatabase {
|
|||||||
return runLogs.some((run) => run.logPath ? safeRealpath(run.logPath) === resolvedTarget : false);
|
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) {
|
if (input.theme) {
|
||||||
this.setSetting("theme", input.theme);
|
this.setSetting("theme", input.theme);
|
||||||
}
|
}
|
||||||
|
if (input.locale) {
|
||||||
|
this.setSetting("locale", input.locale);
|
||||||
|
}
|
||||||
if (typeof input.updatesEnabled === "boolean") {
|
if (typeof input.updatesEnabled === "boolean") {
|
||||||
this.setSetting("updates_enabled", input.updatesEnabled ? "true" : "false");
|
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 boardReadReceiptParams: SQLInputValue[] = companyId ? [BOARD_READER_ID, companyId] : [BOARD_READER_ID];
|
||||||
const connectors = this.readConnectors();
|
const connectors = this.readConnectors();
|
||||||
const theme = (this.getSetting("theme") as "system" | "light" | "dark" | null) ?? "system";
|
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(
|
const boardReadReceiptMap = new Map(
|
||||||
this.readAll<{ messageId: string; readAt: string }>(
|
this.readAll<{ messageId: string; readAt: string }>(
|
||||||
`select reads.message_id as messageId, reads.read_at as readAt
|
`select reads.message_id as messageId, reads.read_at as readAt
|
||||||
@@ -493,6 +497,7 @@ export class AppDatabase {
|
|||||||
profilePath: this.profileDir,
|
profilePath: this.profileDir,
|
||||||
updatesEnabled: this.getSetting("updates_enabled") !== "false",
|
updatesEnabled: this.getSetting("updates_enabled") !== "false",
|
||||||
theme,
|
theme,
|
||||||
|
locale,
|
||||||
backend: {
|
backend: {
|
||||||
secretBackend: this.vault.getBackendLabel(),
|
secretBackend: this.vault.getBackendLabel(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { spawn } from "node:child_process";
|
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";
|
import { app, nativeImage } from "electron";
|
||||||
|
|
||||||
interface HostEnvironmentDependencies {
|
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({
|
export async function prepareHostEnvironment({
|
||||||
iconPath,
|
iconPath,
|
||||||
logger,
|
logger,
|
||||||
}: HostEnvironmentDependencies) {
|
}: 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 {
|
try {
|
||||||
const userShell = process.env.SHELL || "/bin/zsh";
|
const userShell = process.env.SHELL || "/bin/zsh";
|
||||||
const shellPath = await new Promise<string>((resolve) => {
|
const shellPath = await new Promise<string>((resolve) => {
|
||||||
@@ -41,7 +87,7 @@ export async function prepareHostEnvironment({
|
|||||||
} catch {
|
} catch {
|
||||||
// Non-fatal environment enrichment.
|
// Non-fatal environment enrichment.
|
||||||
}
|
}
|
||||||
} else if (process.platform !== "win32") {
|
} else {
|
||||||
logger.info("[path] Skipping interactive shell PATH probe; using deterministic PATH setup only.");
|
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);
|
app.dock.setIcon(appIcon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
app.setAppUserModelId("com.agentcompany.desktop");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -214,6 +214,7 @@ export function createOrchestrationService({
|
|||||||
runId,
|
runId,
|
||||||
task,
|
task,
|
||||||
companyName,
|
companyName,
|
||||||
|
locale: snapshot.locale,
|
||||||
goals: snapshot.goals.filter((goal) => goal.companyId === companyId),
|
goals: snapshot.goals.filter((goal) => goal.companyId === companyId),
|
||||||
projects: snapshot.projects.filter((project) => project.companyId === companyId),
|
projects: snapshot.projects.filter((project) => project.companyId === companyId),
|
||||||
directReports: db.getDirectReports(agent.id, companyId),
|
directReports: db.getDirectReports(agent.id, companyId),
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const activeByAgent = new Map<string, string>();
|
|||||||
const activeByWorkspace = new Map<string, string>();
|
const activeByWorkspace = new Map<string, string>();
|
||||||
const queue: RunJob[] = [];
|
const queue: RunJob[] = [];
|
||||||
const cancelledRuns = new Set<string>();
|
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) {
|
function post(message: ChildMessage) {
|
||||||
parentPort.postMessage(message);
|
parentPort.postMessage(message);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { AgentRecord, AgentMessageRecord, ApprovalRecord, DocumentRecord, GoalRecord, KnowledgeEntryRecord, MeetingRecord, ProjectRecord, SprintRecord, TaskRecord } from "@shared/types";
|
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 { DEPARTMENT_LABELS } from "@shared/constants";
|
||||||
|
import { getLocalePromptInstruction } from "@shared/localization";
|
||||||
|
|
||||||
interface PromptContext {
|
interface PromptContext {
|
||||||
agent: AgentRecord;
|
agent: AgentRecord;
|
||||||
@@ -8,6 +10,7 @@ interface PromptContext {
|
|||||||
runId: string;
|
runId: string;
|
||||||
task: TaskRecord | null;
|
task: TaskRecord | null;
|
||||||
companyName: string;
|
companyName: string;
|
||||||
|
locale?: AppLocale;
|
||||||
goals: GoalRecord[];
|
goals: GoalRecord[];
|
||||||
projects: ProjectRecord[];
|
projects: ProjectRecord[];
|
||||||
directReports: AgentRecord[];
|
directReports: AgentRecord[];
|
||||||
@@ -34,6 +37,11 @@ export function buildSystemPrompt(ctx: PromptContext): string {
|
|||||||
const isCEO = ctx.chainOfCommand.length === 0;
|
const isCEO = ctx.chainOfCommand.length === 0;
|
||||||
const isManager = !isCEO && ctx.directReports.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 now = new Date();
|
||||||
const dateStr = now.toISOString().split("T")[0];
|
const dateStr = now.toISOString().split("T")[0];
|
||||||
const timeStr = now.toISOString().split("T")[1]?.slice(0, 5) ?? "00:00";
|
const timeStr = now.toISOString().split("T")[1]?.slice(0, 5) ?? "00:00";
|
||||||
|
|||||||
+44
-3
@@ -34,6 +34,24 @@ const ENV_ALLOWLIST: ReadonlySet<string> = new Set([
|
|||||||
"PROGRAMFILES(X86)",
|
"PROGRAMFILES(X86)",
|
||||||
"PROGRAMDATA",
|
"PROGRAMDATA",
|
||||||
"PATHEXT",
|
"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_AGENT_ID",
|
||||||
"AGENT_COMPANY_COMPANY_ID",
|
"AGENT_COMPANY_COMPANY_ID",
|
||||||
"AGENT_COMPANY_API_URL",
|
"AGENT_COMPANY_API_URL",
|
||||||
@@ -94,11 +112,34 @@ export function buildSanitizedRuntimeEnv(
|
|||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const normalizedHostEnv = normalizeStringEnv(hostEnv);
|
const normalizedHostEnv = normalizeStringEnv(hostEnv);
|
||||||
const sanitized: Record<string, string> = {};
|
const sanitized: Record<string, string> = {};
|
||||||
for (const key of ENV_ALLOWLIST) {
|
|
||||||
if (key in normalizedHostEnv) {
|
if (process.platform === "win32") {
|
||||||
sanitized[key] = normalizedHostEnv[key]!;
|
// 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 {
|
return {
|
||||||
...sanitized,
|
...sanitized,
|
||||||
...stripForbiddenRuntimeOverrides(runtimeOverrides),
|
...stripForbiddenRuntimeOverrides(runtimeOverrides),
|
||||||
|
|||||||
+110
-64
@@ -48,7 +48,10 @@ import type {
|
|||||||
BrowserActionRecord,
|
BrowserActionRecord,
|
||||||
StandupReport,
|
StandupReport,
|
||||||
} from "@shared/types";
|
} from "@shared/types";
|
||||||
|
import type { AppLocale } from "@shared/locale";
|
||||||
|
import { DEFAULT_LOCALE } from "@shared/locale";
|
||||||
import type { SidebarBadges } from "@shared/contracts";
|
import type { SidebarBadges } from "@shared/contracts";
|
||||||
|
import { createTranslator, LocaleContext, TranslationContext } from "./i18n";
|
||||||
import { CompanyOnboarding } from "./components/CompanyOnboarding";
|
import { CompanyOnboarding } from "./components/CompanyOnboarding";
|
||||||
import { Inbox, StandupView, PerformanceView } from "./components/inbox";
|
import { Inbox, StandupView, PerformanceView } from "./components/inbox";
|
||||||
import { ConnectorEditor } from "./components/connectors";
|
import { ConnectorEditor } from "./components/connectors";
|
||||||
@@ -92,6 +95,8 @@ import { ActionButton, ErrorBoundary, ListFrame, ListRow, Modal, Panel, SectionB
|
|||||||
import { unwrap } from "./lib/desktop";
|
import { unwrap } from "./lib/desktop";
|
||||||
import { formatMoney, formatTime } from "./lib/formatters";
|
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 NavItem = { id: SectionId; label: string; icon: typeof SquaresFour };
|
||||||
type NavGroup = { label: string; items: NavItem[] };
|
type NavGroup = { label: string; items: NavItem[] };
|
||||||
type PaletteAction = {
|
type PaletteAction = {
|
||||||
@@ -100,70 +105,77 @@ type PaletteAction = {
|
|||||||
entityId?: string;
|
entityId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const navGroups: NavGroup[] = [
|
const navGroupDefs: NavGroupDef[] = [
|
||||||
{
|
{
|
||||||
label: "Overview",
|
labelKey: "nav.group.overview",
|
||||||
items: [
|
items: [
|
||||||
{ id: "overview", label: "Dashboard", icon: SquaresFour },
|
{ id: "overview", labelKey: "nav.overview", icon: SquaresFour },
|
||||||
{ id: "inbox", label: "Inbox", icon: Bell },
|
{ id: "inbox", labelKey: "nav.inbox", icon: Bell },
|
||||||
{ id: "standup", label: "Standup", icon: Heartbeat },
|
{ id: "standup", labelKey: "nav.standup", icon: Heartbeat },
|
||||||
{ id: "performance", label: "Performance", icon: ChartBar },
|
{ id: "performance", labelKey: "nav.performance", icon: ChartBar },
|
||||||
{ id: "runs", label: "Runs", icon: TerminalWindow },
|
{ id: "runs", labelKey: "nav.runs", icon: TerminalWindow },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Organization",
|
labelKey: "nav.group.organization",
|
||||||
items: [
|
items: [
|
||||||
{ id: "agents", label: "Agents", icon: Robot },
|
{ id: "agents", labelKey: "nav.agents", icon: Robot },
|
||||||
{ id: "orgchart", label: "Org Chart", icon: TreeStructure },
|
{ id: "orgchart", labelKey: "nav.orgchart", icon: TreeStructure },
|
||||||
{ id: "hiring", label: "Hiring", icon: UserPlus },
|
{ id: "hiring", labelKey: "nav.hiring", icon: UserPlus },
|
||||||
{ id: "communication", label: "Messages", icon: ChatCircle },
|
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
|
||||||
{ id: "goals", label: "Goals", icon: Target },
|
{ id: "goals", labelKey: "nav.goals", icon: Target },
|
||||||
{ id: "projects", label: "Projects", icon: Kanban },
|
{ id: "projects", labelKey: "nav.projects", icon: Kanban },
|
||||||
{ id: "tasks", label: "Tasks", icon: ListChecks },
|
{ id: "tasks", labelKey: "nav.tasks", icon: ListChecks },
|
||||||
{ id: "sprints", label: "Sprints", icon: Rows },
|
{ id: "sprints", labelKey: "nav.sprints", icon: Rows },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Automation",
|
labelKey: "nav.group.automation",
|
||||||
items: [
|
items: [
|
||||||
{ id: "automation", label: "Automation", icon: Lightning },
|
{ id: "automation", labelKey: "nav.automation", icon: Lightning },
|
||||||
{ id: "approvals", label: "Approvals", icon: ShieldCheck },
|
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Knowledge",
|
labelKey: "nav.group.knowledge",
|
||||||
items: [
|
items: [
|
||||||
{ id: "documents", label: "Documents", icon: FileText },
|
{ id: "documents", labelKey: "nav.documents", icon: FileText },
|
||||||
{ id: "knowledge", label: "Knowledge", icon: Brain },
|
{ id: "knowledge", labelKey: "nav.knowledge", icon: Brain },
|
||||||
{ id: "meetings", label: "Meetings", icon: CalendarBlank },
|
{ id: "meetings", labelKey: "nav.meetings", icon: CalendarBlank },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Operations",
|
labelKey: "nav.group.operations",
|
||||||
items: [
|
items: [
|
||||||
{ id: "costs", label: "Costs", icon: CurrencyCircleDollar },
|
{ id: "costs", labelKey: "nav.costs", icon: CurrencyCircleDollar },
|
||||||
{ id: "activity", label: "Activity", icon: Pulse },
|
{ id: "activity", labelKey: "nav.activity", icon: Pulse },
|
||||||
{ id: "social", label: "Social Media", icon: Megaphone },
|
{ id: "social", labelKey: "nav.social", icon: Megaphone },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Infrastructure",
|
labelKey: "nav.group.infrastructure",
|
||||||
items: [
|
items: [
|
||||||
{ id: "connectors", label: "Connectors", icon: Cpu },
|
{ id: "connectors", labelKey: "nav.connectors", icon: Cpu },
|
||||||
{ id: "workspaces", label: "Workspaces", icon: FolderOpen },
|
{ 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() {
|
export function App() {
|
||||||
const reducedMotion = useReducedMotion();
|
const reducedMotion = useReducedMotion();
|
||||||
const noMotion = { duration: 0 };
|
const noMotion = { duration: 0 };
|
||||||
const pageTransition = reducedMotion ? noMotion : { duration: 0.24, ease: [0.16, 1, 0.3, 1] as const };
|
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 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 [snapshot, setSnapshot] = useState<ProfileSnapshot | null>(null);
|
||||||
const [section, setSection] = useState<SectionId>("overview");
|
const [section, setSection] = useState<SectionId>("overview");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -306,7 +318,19 @@ export function App() {
|
|||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.dataset.theme = snapshot.theme;
|
root.dataset.theme = snapshot.theme;
|
||||||
root.style.colorScheme = snapshot.theme === "system" ? "light dark" : 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(() => {
|
useEffect(() => {
|
||||||
if (!snapshot?.currentCompanyId) return;
|
if (!snapshot?.currentCompanyId) return;
|
||||||
@@ -597,21 +621,41 @@ export function App() {
|
|||||||
|
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[100dvh] items-center justify-center text-[13px] text-[color:var(--muted)]">
|
<LocaleContext.Provider value={locale}>
|
||||||
Loading local profile...
|
<TranslationContext.Provider value={t}>
|
||||||
</div>
|
<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) {
|
if (snapshot.companies.length === 0 || showOnboarding || initialOnboardingLocked) {
|
||||||
return <CompanyOnboarding connectors={snapshot.connectors} onDone={handleOnboardingDone} canCancel={snapshot.companies.length > 0 && !initialOnboardingLocked} onCancel={() => {
|
return (
|
||||||
setShowOnboarding(false);
|
<LocaleContext.Provider value={locale}>
|
||||||
setInitialOnboardingLocked(false);
|
<TranslationContext.Provider value={t}>
|
||||||
setError(null);
|
<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 (
|
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)]"}`}>
|
<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)]">
|
<aside className="flex h-[100dvh] flex-col overflow-hidden border-r border-[color:var(--line)] bg-[color:var(--panel)]">
|
||||||
{sidebarCollapsed ? (
|
{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)]"
|
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)}
|
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)]" />
|
<CaretUpDown size={14} className="shrink-0 text-[color:var(--muted)]" />
|
||||||
</button>
|
</button>
|
||||||
{companySwitcherOpen && (
|
{companySwitcherOpen && (
|
||||||
@@ -693,7 +737,7 @@ export function App() {
|
|||||||
onClick={() => { void setCompany("__new__"); setCompanySwitcherOpen(false); }}
|
onClick={() => { void setCompany("__new__"); setCompanySwitcherOpen(false); }}
|
||||||
>
|
>
|
||||||
<Plus size={14} />
|
<Plus size={14} />
|
||||||
<span>New company</span>
|
<span>{t("app.newCompany")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -749,7 +793,7 @@ export function App() {
|
|||||||
onClick={() => setSidebarCollapsed((c) => !c)}
|
onClick={() => setSidebarCollapsed((c) => !c)}
|
||||||
>
|
>
|
||||||
<SidebarSimple size={sidebarCollapsed ? 18 : 17} />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -770,7 +814,7 @@ export function App() {
|
|||||||
onClick={() => void refresh()}
|
onClick={() => void refresh()}
|
||||||
>
|
>
|
||||||
<ArrowClockwise size={16} />
|
<ArrowClockwise size={16} />
|
||||||
<span className="hidden lg:inline">Refresh</span>
|
<span className="hidden lg:inline">{t("app.refresh")}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -780,7 +824,7 @@ export function App() {
|
|||||||
onClick={() => setConsoleOpen((current) => !current)}
|
onClick={() => setConsoleOpen((current) => !current)}
|
||||||
>
|
>
|
||||||
<TerminalWindow size={16} />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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 justify-between px-5 py-2.5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<TerminalWindow size={15} className="text-[color:var(--muted)]" />
|
<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>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Select
|
<Select
|
||||||
@@ -1325,7 +1369,7 @@ export function App() {
|
|||||||
onChange={setSelectedRunId}
|
onChange={setSelectedRunId}
|
||||||
options={runs.length > 0
|
options={runs.length > 0
|
||||||
? runs.map((run) => ({ value: run.id, label: run.summary || `Run ${run.id.slice(0, 8)}` }))
|
? 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
|
<button
|
||||||
@@ -1343,34 +1387,34 @@ export function App() {
|
|||||||
{selectedRun ? (
|
{selectedRun ? (
|
||||||
<div className="space-y-2.5 text-[12px] text-[color:var(--muted-strong)]">
|
<div className="space-y-2.5 text-[12px] text-[color:var(--muted-strong)]">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>Status</span>
|
<span>{t("common.status")}</span>
|
||||||
<StatusPill status={selectedRun.status} />
|
<StatusPill status={selectedRun.status} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>Started</span>
|
<span>{t("common.started")}</span>
|
||||||
<span className="mono text-[11px]">{formatTime(selectedRun.startedAt)}</span>
|
<span className="mono text-[11px]">{formatTime(selectedRun.startedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>Finished</span>
|
<span>{t("common.finished")}</span>
|
||||||
<span className="mono text-[11px]">{formatTime(selectedRun.finishedAt)}</span>
|
<span className="mono text-[11px]">{formatTime(selectedRun.finishedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>Cost</span>
|
<span>{t("common.cost")}</span>
|
||||||
<span className="mono text-[11px]">{formatMoney(selectedRun.costUsd)}</span>
|
<span className="mono text-[11px]">{formatMoney(selectedRun.costUsd)}</span>
|
||||||
</div>
|
</div>
|
||||||
{selectedRun.logPath ? (
|
{selectedRun.logPath ? (
|
||||||
<div className="pt-1">
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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>
|
||||||
<div className="h-[200px] overflow-auto px-5 py-3">
|
<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)]">
|
<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>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1406,7 +1450,7 @@ export function App() {
|
|||||||
type="text"
|
type="text"
|
||||||
aria-label="Search"
|
aria-label="Search"
|
||||||
className="flex-1 bg-transparent text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)] outline-none"
|
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}
|
value={commandPaletteSearch}
|
||||||
onChange={(e) => setCommandPaletteSearch(e.target.value)}
|
onChange={(e) => setCommandPaletteSearch(e.target.value)}
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -1416,14 +1460,14 @@ export function App() {
|
|||||||
className="text-[12px] text-[color:var(--muted)] hover:text-[color:var(--text)]"
|
className="text-[12px] text-[color:var(--muted)] hover:text-[color:var(--text)]"
|
||||||
onClick={() => setCommandPaletteSearch("")}
|
onClick={() => setCommandPaletteSearch("")}
|
||||||
>
|
>
|
||||||
Clear
|
{t("app.clear")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredNavItems.length > 0 ? (
|
{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>
|
<ListFrame>
|
||||||
{filteredNavItems.map((item) => (
|
{filteredNavItems.map((item) => (
|
||||||
<ListRow
|
<ListRow
|
||||||
@@ -1453,7 +1497,7 @@ export function App() {
|
|||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
{matchedAgents.length > 0 ? (
|
{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>
|
<ListFrame>
|
||||||
{matchedAgents.map((agent) => (
|
{matchedAgents.map((agent) => (
|
||||||
<ListRow
|
<ListRow
|
||||||
@@ -1482,7 +1526,7 @@ export function App() {
|
|||||||
|
|
||||||
{matchedTasks.length > 0 ? (
|
{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>
|
<ListFrame>
|
||||||
{matchedTasks.map((task) => (
|
{matchedTasks.map((task) => (
|
||||||
<ListRow
|
<ListRow
|
||||||
@@ -1510,7 +1554,7 @@ export function App() {
|
|||||||
|
|
||||||
{matchedRuns.length > 0 ? (
|
{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>
|
<ListFrame>
|
||||||
{matchedRuns.map((run) => (
|
{matchedRuns.map((run) => (
|
||||||
<ListRow
|
<ListRow
|
||||||
@@ -1540,7 +1584,7 @@ export function App() {
|
|||||||
|
|
||||||
{matchedGoals.length > 0 ? (
|
{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>
|
<ListFrame>
|
||||||
{matchedGoals.map((goal) => (
|
{matchedGoals.map((goal) => (
|
||||||
<ListRow
|
<ListRow
|
||||||
@@ -1568,7 +1612,7 @@ export function App() {
|
|||||||
|
|
||||||
{matchedProjects.length > 0 ? (
|
{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>
|
<ListFrame>
|
||||||
{matchedProjects.map((project) => (
|
{matchedProjects.map((project) => (
|
||||||
<ListRow
|
<ListRow
|
||||||
@@ -1597,7 +1641,7 @@ export function App() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{paletteQuery && filteredNavItems.length === 0 && !hasEntityResults ? (
|
{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}
|
) : null}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -1605,5 +1649,7 @@ export function App() {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</TranslationContext.Provider>
|
||||||
|
</LocaleContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
CheckCircle,
|
CheckCircle,
|
||||||
Cpu,
|
Cpu,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
|
GlobeSimple,
|
||||||
Lightning,
|
Lightning,
|
||||||
Rocket,
|
Rocket,
|
||||||
Target,
|
Target,
|
||||||
@@ -13,23 +14,33 @@ import {
|
|||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
|
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
|
||||||
import type { OnboardingBootstrapResult, ProfileSnapshot } from "@shared/types";
|
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 { unwrap } from "../lib/desktop";
|
||||||
import { ActionButton, Input, Select, TextArea } from "./ui";
|
import { ActionButton, Input, Select, TextArea } from "./ui";
|
||||||
|
|
||||||
type Step = "company" | "mission" | "launch";
|
type Step = "language" | "company" | "mission" | "launch";
|
||||||
|
|
||||||
export function CompanyOnboarding({
|
export function CompanyOnboarding({
|
||||||
connectors,
|
connectors,
|
||||||
onDone,
|
onDone,
|
||||||
canCancel = false,
|
canCancel = false,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
currentLocale,
|
||||||
|
onLocaleChange,
|
||||||
|
isFirstLaunch = false,
|
||||||
}: {
|
}: {
|
||||||
connectors: ProfileSnapshot["connectors"];
|
connectors: ProfileSnapshot["connectors"];
|
||||||
onDone: (summary: OnboardingBootstrapResult) => Promise<void>;
|
onDone: (summary: OnboardingBootstrapResult) => Promise<void>;
|
||||||
canCancel?: boolean;
|
canCancel?: boolean;
|
||||||
onCancel?: () => void;
|
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 [companyName, setCompanyName] = useState("");
|
||||||
const [companyDescription, setCompanyDescription] = useState("");
|
const [companyDescription, setCompanyDescription] = useState("");
|
||||||
const [workspacePath, setWorkspacePath] = useState("");
|
const [workspacePath, setWorkspacePath] = useState("");
|
||||||
@@ -60,11 +71,11 @@ export function CompanyOnboarding({
|
|||||||
}, [executionReadyConnectors, selectedConnectorId]);
|
}, [executionReadyConnectors, selectedConnectorId]);
|
||||||
|
|
||||||
const stepLabels = [
|
const stepLabels = [
|
||||||
{ label: "Company", icon: Buildings },
|
{ label: t("onboarding.stepLabel1"), icon: Buildings },
|
||||||
{ label: "Mission", icon: Target },
|
{ label: t("onboarding.stepLabel2"), icon: Target },
|
||||||
{ label: "Launch", icon: Rocket },
|
{ label: t("onboarding.stepLabel3"), icon: Rocket },
|
||||||
] as const;
|
] 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 () => {
|
const pickDirectory = useCallback(async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -103,21 +114,21 @@ export function CompanyOnboarding({
|
|||||||
const continueToMission = useCallback(async () => {
|
const continueToMission = useCallback(async () => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
if (!companyName.trim()) {
|
if (!companyName.trim()) {
|
||||||
setError("Enter a company name.");
|
setError(t("onboarding.enterCompanyName"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!selectedConnector) {
|
if (!selectedConnector) {
|
||||||
setError("At least one execution-ready connector is required before you can continue.");
|
setError(t("onboarding.connectorRequired"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
setStep("mission");
|
setStep("mission");
|
||||||
}, [companyName, loading, selectedConnector]);
|
}, [companyName, loading, selectedConnector, t]);
|
||||||
|
|
||||||
const launchCompany = useCallback(async () => {
|
const launchCompany = useCallback(async () => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
if (!goalTitle.trim()) {
|
if (!goalTitle.trim()) {
|
||||||
setError("Describe the first real company goal.");
|
setError(t("onboarding.describeFirstGoal"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -125,11 +136,11 @@ export function CompanyOnboarding({
|
|||||||
try {
|
try {
|
||||||
await bootstrap();
|
await bootstrap();
|
||||||
} catch (submitError) {
|
} catch (submitError) {
|
||||||
setError(submitError instanceof Error ? submitError.message : "Unable to bootstrap the company.");
|
setError(submitError instanceof Error ? submitError.message : t("onboarding.unableToBootstrap"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [bootstrap, goalTitle, loading]);
|
}, [bootstrap, goalTitle, loading, t]);
|
||||||
|
|
||||||
const finishLaunch = useCallback(async () => {
|
const finishLaunch = useCallback(async () => {
|
||||||
if (!launchSummary) return;
|
if (!launchSummary) return;
|
||||||
@@ -138,11 +149,11 @@ export function CompanyOnboarding({
|
|||||||
try {
|
try {
|
||||||
await onDone(launchSummary);
|
await onDone(launchSummary);
|
||||||
} catch (submitError) {
|
} catch (submitError) {
|
||||||
setError(submitError instanceof Error ? submitError.message : "Unable to open the control center.");
|
setError(submitError instanceof Error ? submitError.message : t("onboarding.unableToOpenControlCenter"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [launchSummary, onDone]);
|
}, [launchSummary, onDone, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid min-h-[100dvh] overflow-hidden bg-[color:var(--bg)] md:grid-cols-[minmax(0,1fr)_540px]">
|
<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]"
|
className="mx-auto w-full max-w-[460px]"
|
||||||
>
|
>
|
||||||
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||||
AgentCompany Desktop
|
{t("onboarding.subtitle")}
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-[38px] leading-[0.98] tracking-[-0.03em] text-[color:var(--text)] md:text-[48px]">
|
<h1 className="text-[38px] leading-[0.98] tracking-[-0.03em] text-[color:var(--text)] md:text-[48px]">
|
||||||
Start a real local-first
|
{t("onboarding.title").split("\n").map((line, i) => (
|
||||||
<br />
|
<span key={i}>{line}{i === 0 ? <br /> : null}</span>
|
||||||
agent company.
|
))}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4 max-w-[400px] text-[14px] leading-relaxed text-[color:var(--muted)]">
|
<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
|
{t("onboarding.description")}
|
||||||
the first goal, creates the operating project, and queues the first execution task.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-10 flex items-center gap-3">
|
{step !== "language" ? (
|
||||||
{stepLabels.map((entry, index) => (
|
<div className="mt-10 flex items-center gap-3">
|
||||||
<div key={entry.label} className="flex items-center gap-2">
|
{stepLabels.map((entry, index) => (
|
||||||
{index > 0 ? (
|
<div key={entry.label} className="flex items-center gap-2">
|
||||||
<div className={`h-px w-6 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
|
{index > 0 ? (
|
||||||
) : null}
|
<div className={`h-px w-6 ${index <= stepIndex ? "bg-[color:var(--accent)]" : "bg-[color:var(--line)]"}`} />
|
||||||
<div
|
) : null}
|
||||||
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
|
<div
|
||||||
index < stepIndex
|
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-[11px] font-medium ${
|
||||||
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
|
index < stepIndex
|
||||||
: index === stepIndex
|
? "bg-[color:var(--success-soft)] text-[color:var(--success)]"
|
||||||
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
|
: index === stepIndex
|
||||||
: "bg-[color:var(--panel-soft)] text-[color:var(--muted)]"
|
? "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}
|
{index < stepIndex ? <CheckCircle size={14} weight="fill" /> : <entry.icon size={14} />}
|
||||||
|
{entry.label}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
<div className="mb-3 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
||||||
Connector readiness
|
{t("onboarding.connectorReadiness")}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
{connectors.map((connector) => {
|
{connectors.map((connector) => {
|
||||||
const issue = getConnectorExecutionReadinessIssue(connector);
|
const isChecking = connector.status === "detected";
|
||||||
const isRunnable = !issue;
|
const isReady = connector.status === "ready";
|
||||||
const isMissing = connector.status === "not_installed";
|
const isMissing = connector.status === "not_installed";
|
||||||
|
const issue = !isChecking ? getConnectorExecutionReadinessIssue(connector) : null;
|
||||||
return (
|
return (
|
||||||
<div key={connector.id} className="flex items-start gap-2.5 text-[13px]">
|
<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)]" />
|
<CheckCircle size={16} weight="fill" className="shrink-0 text-[color:var(--success)]" />
|
||||||
) : isMissing ? (
|
) : isMissing ? (
|
||||||
<XCircle size={16} weight="fill" className="shrink-0 text-[color:var(--muted)]" />
|
<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="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<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}
|
{connector.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[11px] text-[color:var(--muted)]">
|
<span className="text-[11px] text-[color:var(--muted)]">
|
||||||
{connector.status.replaceAll("_", " ")}
|
{isChecking ? (currentLocale === "zh" ? "检测中..." : "checking...") : connector.status.replaceAll("_", " ")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{issue ? (
|
{issue ? (
|
||||||
@@ -227,7 +242,7 @@ export function CompanyOnboarding({
|
|||||||
</div>
|
</div>
|
||||||
{!hasRunnableConnector ? (
|
{!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)]">
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -242,11 +257,68 @@ export function CompanyOnboarding({
|
|||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="absolute right-6 top-6 text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
|
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>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<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" ? (
|
{step === "company" ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="company"
|
key="company"
|
||||||
@@ -257,29 +329,29 @@ export function CompanyOnboarding({
|
|||||||
className="surface w-full rounded-[12px] p-7"
|
className="surface w-full rounded-[12px] p-7"
|
||||||
>
|
>
|
||||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
<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>
|
||||||
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
|
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
|
||||||
Create the operating boundary
|
{t("onboarding.step1Title")}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-6 h-px bg-[color:var(--line)]" />
|
<div className="mb-6 h-px bg-[color:var(--line)]" />
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
label="Company name"
|
label={t("onboarding.companyName")}
|
||||||
value={companyName}
|
value={companyName}
|
||||||
onChange={setCompanyName}
|
onChange={setCompanyName}
|
||||||
placeholder="e.g. Northline Systems"
|
placeholder={t("onboarding.companyNamePlaceholder")}
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
label="Company description"
|
label={t("onboarding.companyDescription")}
|
||||||
value={companyDescription}
|
value={companyDescription}
|
||||||
onChange={setCompanyDescription}
|
onChange={setCompanyDescription}
|
||||||
placeholder="Optional context for the company operating model."
|
placeholder={t("onboarding.companyDescriptionPlaceholder")}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="Primary connector"
|
label={t("onboarding.primaryConnector")}
|
||||||
value={selectedConnector?.id ?? ""}
|
value={selectedConnector?.id ?? ""}
|
||||||
onChange={setSelectedConnectorId}
|
onChange={setSelectedConnectorId}
|
||||||
options={executionReadyConnectors.map((connector) => ({
|
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)]">
|
<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.
|
{t("onboarding.connectorInfo")}
|
||||||
Re-run connector health checks from Settings if the list is empty.
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="onboarding-workspace-path"
|
htmlFor="onboarding-workspace-path"
|
||||||
className="mb-1.5 block text-[12px] font-medium text-[color:var(--muted-strong)]"
|
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>
|
</label>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Input
|
<Input
|
||||||
id="onboarding-workspace-path"
|
id="onboarding-workspace-path"
|
||||||
ariaLabel="Workspace folder"
|
ariaLabel={t("onboarding.workspaceFolder")}
|
||||||
value={workspacePath}
|
value={workspacePath}
|
||||||
onChange={setWorkspacePath}
|
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>
|
||||||
<div className="mt-1.5 text-[11px] text-[color:var(--muted)]">
|
<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>
|
</div>
|
||||||
<div className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
<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)]"
|
className="h-4 w-4 rounded border-[color:var(--line)] accent-[color:var(--accent)]"
|
||||||
/>
|
/>
|
||||||
<div>
|
<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)]">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@@ -336,9 +407,18 @@ export function CompanyOnboarding({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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
|
<ActionButton
|
||||||
label="Continue"
|
label={t("onboarding.continue")}
|
||||||
onClick={() => void continueToMission()}
|
onClick={() => void continueToMission()}
|
||||||
icon={Buildings}
|
icon={Buildings}
|
||||||
tone="accent"
|
tone="accent"
|
||||||
@@ -358,39 +438,36 @@ export function CompanyOnboarding({
|
|||||||
className="surface w-full rounded-[12px] p-7"
|
className="surface w-full rounded-[12px] p-7"
|
||||||
>
|
>
|
||||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
|
<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>
|
||||||
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
|
<div className="mb-1 text-[18px] font-semibold tracking-[-0.01em] text-[color:var(--text)]">
|
||||||
Define the first real goal
|
{t("onboarding.step2Title")}
|
||||||
</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.
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-6 h-px bg-[color:var(--line)]" />
|
<div className="mb-6 h-px bg-[color:var(--line)]" />
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
label="Goal"
|
label={t("onboarding.goal")}
|
||||||
value={goalTitle}
|
value={goalTitle}
|
||||||
onChange={setGoalTitle}
|
onChange={setGoalTitle}
|
||||||
placeholder="e.g. Ship the first customer-ready desktop demo"
|
placeholder={t("onboarding.goalPlaceholder")}
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
label="Acceptance criteria"
|
label={t("onboarding.acceptanceCriteria")}
|
||||||
value={goalDescription}
|
value={goalDescription}
|
||||||
onChange={setGoalDescription}
|
onChange={setGoalDescription}
|
||||||
placeholder="Describe what successful execution looks like, what cannot be skipped, and any delivery constraints."
|
placeholder={t("onboarding.acceptanceCriteriaPlaceholder")}
|
||||||
rows={5}
|
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="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)]">
|
<div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.06em] text-[color:var(--muted)]">
|
||||||
<Cpu size={12} />
|
<Cpu size={12} />
|
||||||
Execution handoff
|
{t("onboarding.executionHandoff")}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>The selected connector becomes the CEO agent runtime for the first launch.</div>
|
<div>{t("onboarding.executionHandoffLine1")}</div>
|
||||||
<div>The app will create the company, the primary workspace, the CEO agent, the kickoff project, and the first task.</div>
|
<div>{t("onboarding.executionHandoffLine2")}</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.executionHandoffLine3")}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -407,10 +484,10 @@ export function CompanyOnboarding({
|
|||||||
onClick={() => setStep("company")}
|
onClick={() => setStep("company")}
|
||||||
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
|
className="text-[12px] font-medium text-[color:var(--muted)] transition hover:text-[color:var(--text)]"
|
||||||
>
|
>
|
||||||
Back
|
{t("onboarding.back")}
|
||||||
</button>
|
</button>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
label={loading ? "Launching..." : "Launch Company"}
|
label={loading ? t("onboarding.launching") : t("onboarding.launchCompany")}
|
||||||
onClick={() => void launchCompany()}
|
onClick={() => void launchCompany()}
|
||||||
icon={Lightning}
|
icon={Lightning}
|
||||||
tone="accent"
|
tone="accent"
|
||||||
@@ -433,42 +510,41 @@ export function CompanyOnboarding({
|
|||||||
<CheckCircle size={24} weight="fill" />
|
<CheckCircle size={24} weight="fill" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mt-5 text-[28px] tracking-[-0.03em] text-[color:var(--text)]">
|
<h2 className="mt-5 text-[28px] tracking-[-0.03em] text-[color:var(--text)]">
|
||||||
{launchSummary.companyName} is live.
|
{launchSummary.companyName} {t("onboarding.isLive")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3 text-[14px] leading-relaxed text-[color:var(--muted)]">
|
<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 ? t("onboarding.launchDescription") : t("onboarding.launchDescriptionDeferred")}
|
||||||
{launchSummary.runId ? " queued." : " prepared for deferred execution once the runtime is ready."}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-6 grid gap-3 md:grid-cols-2">
|
<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="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)]">
|
<div className="mt-1 text-[13px] font-medium text-[color:var(--text)]">
|
||||||
{selectedConnector?.label ?? "Unavailable"}
|
{selectedConnector?.label ?? t("onboarding.unavailable")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
<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)]">
|
<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>
|
</div>
|
||||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
<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)]">
|
<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>
|
</div>
|
||||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
<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)]">
|
<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>
|
</div>
|
||||||
<div className="rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel-soft)] px-4 py-3">
|
<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)]">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -481,7 +557,7 @@ export function CompanyOnboarding({
|
|||||||
|
|
||||||
<div className="mt-8 flex items-center justify-end">
|
<div className="mt-8 flex items-center justify-end">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
label={loading ? "Opening..." : "Open Control Center"}
|
label={loading ? t("onboarding.opening") : t("onboarding.openControlCenter")}
|
||||||
onClick={() => void finishLaunch()}
|
onClick={() => void finishLaunch()}
|
||||||
icon={Rocket}
|
icon={Rocket}
|
||||||
tone="accent"
|
tone="accent"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import type {
|
|||||||
import { timeAgo } from "../lib/formatters"
|
import { timeAgo } from "../lib/formatters"
|
||||||
import { DEPARTMENT_OPTIONS } from "@shared/constants"
|
import { DEPARTMENT_OPTIONS } from "@shared/constants"
|
||||||
import { ActionButton, Select } from "./ui"
|
import { ActionButton, Select } from "./ui"
|
||||||
|
import { useT } from "../i18n"
|
||||||
|
|
||||||
const ACCENT = "var(--accent)"
|
const ACCENT = "var(--accent)"
|
||||||
const LINE = "var(--line)"
|
const LINE = "var(--line)"
|
||||||
@@ -1611,6 +1612,7 @@ export function AutomationPanel({
|
|||||||
companyId,
|
companyId,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
}: AutomationPanelProps) {
|
}: AutomationPanelProps) {
|
||||||
|
const t = useT()
|
||||||
const [activeSection, setActiveSection] = useState<"rules" | "workflows">("rules")
|
const [activeSection, setActiveSection] = useState<"rules" | "workflows">("rules")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1630,7 +1632,7 @@ export function AutomationPanel({
|
|||||||
<Robot size={20} weight="fill" />
|
<Robot size={20} weight="fill" />
|
||||||
</div>
|
</div>
|
||||||
<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)]">
|
<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.
|
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,
|
Together, they let your AI company run fully autonomously -- responding to changes, routing work,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { getAgentInitials, formatMessageTime } from "../lib/formatters"
|
|||||||
import { DEPARTMENT_COLORS } from "@shared/constants"
|
import { DEPARTMENT_COLORS } from "@shared/constants"
|
||||||
import { unwrap } from "../lib/desktop"
|
import { unwrap } from "../lib/desktop"
|
||||||
import { InlineNotice, Modal, Select, Input, TextArea } from "./ui"
|
import { InlineNotice, Modal, Select, Input, TextArea } from "./ui"
|
||||||
|
import { useT } from "../i18n"
|
||||||
|
|
||||||
const TERRACOTTA = "#d97745"
|
const TERRACOTTA = "#d97745"
|
||||||
const TERRACOTTA_SOFT = "#d9774518"
|
const TERRACOTTA_SOFT = "#d9774518"
|
||||||
@@ -815,6 +816,7 @@ interface ComposeModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: ComposeModalProps) {
|
function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: ComposeModalProps) {
|
||||||
|
const t = useT()
|
||||||
const [fromAgentId, setFromAgentId] = useState(agents[0]?.id ?? "")
|
const [fromAgentId, setFromAgentId] = useState(agents[0]?.id ?? "")
|
||||||
const [toAgentId, setToAgentId] = useState("")
|
const [toAgentId, setToAgentId] = useState("")
|
||||||
const [channel, setChannel] = useState<MessageChannel>("company")
|
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])
|
}, [agents, body, channel, channelTargetId, companyId, fromAgentId, onClose, onSent, priority, subject, toAgentId])
|
||||||
|
|
||||||
return (
|
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">
|
<div className="space-y-4">
|
||||||
{sendError ? <InlineNotice message={sendError} /> : null}
|
{sendError ? <InlineNotice message={sendError} /> : null}
|
||||||
<Select
|
<Select
|
||||||
label="From Agent"
|
label={t("comm.fromAgent")}
|
||||||
value={fromAgentId}
|
value={fromAgentId}
|
||||||
onChange={setFromAgentId}
|
onChange={setFromAgentId}
|
||||||
options={agents.map((a) => ({
|
options={agents.map((a) => ({
|
||||||
@@ -907,15 +909,15 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="Channel"
|
label={t("comm.channel")}
|
||||||
value={channel}
|
value={channel}
|
||||||
onChange={(v) => setChannel(v as MessageChannel)}
|
onChange={(v) => setChannel(v as MessageChannel)}
|
||||||
options={[
|
options={[
|
||||||
{ value: "company", label: "Company-wide broadcast" },
|
{ value: "company", label: t("comm.companyBroadcast") },
|
||||||
{ value: "department", label: "Department channel" },
|
{ value: "department", label: t("comm.departmentChannel") },
|
||||||
{ value: "project", label: "Project channel" },
|
{ value: "project", label: t("comm.projectChannel") },
|
||||||
{ value: "incident", label: "Incident channel" },
|
{ value: "incident", label: t("comm.incidentChannel") },
|
||||||
{ value: "direct", label: "Direct message" },
|
{ value: "direct", label: t("comm.directMessage") },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -963,17 +965,17 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Subject"
|
label={t("comm.subject")}
|
||||||
value={subject}
|
value={subject}
|
||||||
onChange={setSubject}
|
onChange={setSubject}
|
||||||
placeholder="Message subject"
|
placeholder={t("comm.messageSubject")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextArea
|
<TextArea
|
||||||
label="Message"
|
label={t("comm.message")}
|
||||||
value={body}
|
value={body}
|
||||||
onChange={setBody}
|
onChange={setBody}
|
||||||
placeholder="Write your message..."
|
placeholder={t("comm.writeMessage")}
|
||||||
rows={5}
|
rows={5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -994,7 +996,7 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="rounded-[8px] px-4 py-2 text-[12px] font-medium text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)]"
|
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>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1018,7 +1020,7 @@ function ComposeModal({ open, onClose, agents, projects, companyId, onSent }: Co
|
|||||||
) : (
|
) : (
|
||||||
<PaperPlaneTilt size={14} weight="fill" />
|
<PaperPlaneTilt size={14} weight="fill" />
|
||||||
)}
|
)}
|
||||||
Send Message
|
{t("comm.sendMessage")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1040,6 +1042,7 @@ function FilterBar({
|
|||||||
filters: FilterState
|
filters: FilterState
|
||||||
onChangeFilters: (next: FilterState) => void
|
onChangeFilters: (next: FilterState) => void
|
||||||
}) {
|
}) {
|
||||||
|
const t = useT()
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1057,7 +1060,7 @@ function FilterBar({
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChangeFilters({ ...filters, search: e.target.value })
|
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"
|
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}
|
style={{ focusRingColor: TERRACOTTA } as React.CSSProperties}
|
||||||
/>
|
/>
|
||||||
@@ -1079,7 +1082,7 @@ function FilterBar({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Funnel size={12} />
|
<Funnel size={12} />
|
||||||
Filters
|
{t("comm.filters")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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)]"
|
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="all">{t("comm.allChannels")}</option>
|
||||||
<option value="direct">Direct</option>
|
<option value="direct">{t("comm.direct")}</option>
|
||||||
<option value="department">Department</option>
|
<option value="department">{t("comm.department")}</option>
|
||||||
<option value="company">Company</option>
|
<option value="company">{t("comm.company")}</option>
|
||||||
<option value="project">Project</option>
|
<option value="project">{t("nav.projects")}</option>
|
||||||
<option value="incident">Incident</option>
|
<option value="incident">{t("comm.incident")}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<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)]"
|
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="all">{t("comm.allPriorities")}</option>
|
||||||
<option value="urgent">Urgent</option>
|
<option value="urgent">{t("comm.urgent")}</option>
|
||||||
<option value="normal">Normal</option>
|
<option value="normal">{t("comm.normal")}</option>
|
||||||
<option value="low">Low</option>
|
<option value="low">{t("comm.low")}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -1138,7 +1141,7 @@ function FilterBar({
|
|||||||
}`}
|
}`}
|
||||||
style={filters.unreadOnly ? { backgroundColor: TERRACOTTA } : undefined}
|
style={filters.unreadOnly ? { backgroundColor: TERRACOTTA } : undefined}
|
||||||
>
|
>
|
||||||
Unread only
|
{t("comm.unreadOnly")}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{(filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly) ? (
|
{(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)]"
|
className="text-[10px] font-medium text-[color:var(--muted)] underline transition hover:text-[color:var(--text)]"
|
||||||
>
|
>
|
||||||
Clear filters
|
{t("comm.clearFilters")}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -1189,6 +1192,7 @@ export function CommunicationHub({
|
|||||||
onNavigateToTask,
|
onNavigateToTask,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
}: CommunicationHubProps) {
|
}: CommunicationHubProps) {
|
||||||
|
const t = useT()
|
||||||
const [selectedChannelKey, setSelectedChannelKey] = useState<string | null>(null)
|
const [selectedChannelKey, setSelectedChannelKey] = useState<string | null>(null)
|
||||||
const [showSidebar, setShowSidebar] = useState(true)
|
const [showSidebar, setShowSidebar] = useState(true)
|
||||||
const [showCompose, setShowCompose] = useState(false)
|
const [showCompose, setShowCompose] = useState(false)
|
||||||
@@ -1447,7 +1451,7 @@ export function CommunicationHub({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-[16px] font-bold text-[color:var(--text)]">
|
<h2 className="text-[16px] font-bold text-[color:var(--text)]">
|
||||||
Communication Hub
|
{t("comm.communicationHub")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[12px] text-[color:var(--muted)]">
|
<p className="text-[12px] text-[color:var(--muted)]">
|
||||||
{messages.length} message{messages.length !== 1 ? "s" : ""}
|
{messages.length} message{messages.length !== 1 ? "s" : ""}
|
||||||
@@ -1464,7 +1468,7 @@ export function CommunicationHub({
|
|||||||
style={{ backgroundColor: TERRACOTTA }}
|
style={{ backgroundColor: TERRACOTTA }}
|
||||||
>
|
>
|
||||||
<Plus size={13} weight="bold" />
|
<Plus size={13} weight="bold" />
|
||||||
New message
|
{t("comm.newMessage")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1486,7 +1490,7 @@ export function CommunicationHub({
|
|||||||
{/* Sidebar header */}
|
{/* Sidebar header */}
|
||||||
<div className="flex items-center justify-between border-b border-[color:var(--line)] px-4 py-3">
|
<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)]">
|
<span className="text-[13px] font-semibold text-[color:var(--text)]">
|
||||||
Channels
|
{t("comm.channels")}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1527,7 +1531,7 @@ export function CommunicationHub({
|
|||||||
{taskDiscussionChannels.length > 0 ? (
|
{taskDiscussionChannels.length > 0 ? (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="mb-1 px-2 text-[9px] font-bold uppercase tracking-[0.1em] text-[color:var(--muted)]">
|
<div className="mb-1 px-2 text-[9px] font-bold uppercase tracking-[0.1em] text-[color:var(--muted)]">
|
||||||
Task Discussions
|
{t("comm.taskDiscussions")}
|
||||||
</div>
|
</div>
|
||||||
{taskDiscussionChannels.map((discussion) => (
|
{taskDiscussionChannels.map((discussion) => (
|
||||||
<button
|
<button
|
||||||
@@ -1551,7 +1555,7 @@ export function CommunicationHub({
|
|||||||
|
|
||||||
{channels.length === 0 ? (
|
{channels.length === 0 ? (
|
||||||
<div className="px-3 py-6 text-center text-[11px] text-[color:var(--muted)]">
|
<div className="px-3 py-6 text-center text-[11px] text-[color:var(--muted)]">
|
||||||
No active departments yet.
|
{t("comm.noActiveDepartments")}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -1622,7 +1626,7 @@ export function CommunicationHub({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-[13px] text-[color:var(--muted)]">
|
<span className="text-[13px] text-[color:var(--muted)]">
|
||||||
Select a channel
|
{t("comm.selectChannel")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1641,10 +1645,10 @@ export function CommunicationHub({
|
|||||||
className="mb-3 text-[color:var(--muted)]"
|
className="mb-3 text-[color:var(--muted)]"
|
||||||
/>
|
/>
|
||||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
|
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
|
||||||
Select a channel to view messages
|
{t("comm.selectChannelView")}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
|
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
|
||||||
Monitor internal agent communications across departments
|
{t("comm.monitorComms")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : activeChannel.kind === "task" ? (
|
) : activeChannel.kind === "task" ? (
|
||||||
@@ -1652,10 +1656,10 @@ export function CommunicationHub({
|
|||||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||||
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
|
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
|
||||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
|
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
|
||||||
No discussion yet
|
{t("comm.noDiscussionYet")}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -1675,13 +1679,13 @@ export function CommunicationHub({
|
|||||||
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
|
<Tray size={36} className="mb-3 text-[color:var(--muted)]" />
|
||||||
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
|
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">
|
||||||
{filters.search || filters.unreadOnly || filters.priority !== "all"
|
{filters.search || filters.unreadOnly || filters.priority !== "all"
|
||||||
? "No messages match your filters"
|
? t("comm.noMatchFilters")
|
||||||
: "No messages in this channel yet"}
|
: t("comm.noMessagesChannel")}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
|
<div className="mt-1 text-[12px] text-[color:var(--muted)]">
|
||||||
{filters.search || filters.unreadOnly || filters.priority !== "all"
|
{filters.search || filters.unreadOnly || filters.priority !== "all"
|
||||||
? "Try adjusting your search or filters"
|
? t("comm.adjustFilters")
|
||||||
: "Messages will appear here as agents work and communicate"}
|
: t("comm.messagesAppear")}
|
||||||
</div>
|
</div>
|
||||||
</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="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)]">
|
<div className="flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
|
||||||
<ChatCircle size={14} />
|
<ChatCircle size={14} />
|
||||||
<span>Select a channel to read and send messages.</span>
|
<span>{t("comm.selectChannelSend")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
SprintRecord,
|
SprintRecord,
|
||||||
} from "@shared/types";
|
} from "@shared/types";
|
||||||
import { ActionButton, ConfirmDialog, InlineNotice, Input, ListFrame, ListRow, Modal, Select, StatusPill, TextArea } from "./ui";
|
import { ActionButton, ConfirmDialog, InlineNotice, Input, ListFrame, ListRow, Modal, Select, StatusPill, TextArea } from "./ui";
|
||||||
|
import { useT } from "../i18n";
|
||||||
|
|
||||||
const documentTypeOptions = [
|
const documentTypeOptions = [
|
||||||
"prd",
|
"prd",
|
||||||
@@ -450,13 +451,14 @@ export function DocumentCenter({
|
|||||||
goals: GoalRecord[];
|
goals: GoalRecord[];
|
||||||
onRefresh: () => Promise<void>;
|
onRefresh: () => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useT();
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||||
const activeDocument = useMemo(() => documents.find((document) => document.id === editingId) ?? null, [documents, editingId]);
|
const activeDocument = useMemo(() => documents.find((document) => document.id === editingId) ?? null, [documents, editingId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SectionHeader title="Document Center" count={documents.length} action={() => setEditingId("__new__")} />
|
<SectionHeader title={t("ops.documentCenter")} count={documents.length} action={() => setEditingId("__new__")} />
|
||||||
<ListFrame>
|
<ListFrame>
|
||||||
{documents.map((document) => (
|
{documents.map((document) => (
|
||||||
<ListRow key={document.id}>
|
<ListRow key={document.id}>
|
||||||
@@ -521,13 +523,14 @@ export function KnowledgeBaseManager({
|
|||||||
entries: KnowledgeEntryRecord[];
|
entries: KnowledgeEntryRecord[];
|
||||||
onRefresh: () => Promise<void>;
|
onRefresh: () => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useT();
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||||
const activeEntry = useMemo(() => entries.find((entry) => entry.id === editingId) ?? null, [entries, editingId]);
|
const activeEntry = useMemo(() => entries.find((entry) => entry.id === editingId) ?? null, [entries, editingId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SectionHeader title="Knowledge Base" count={entries.length} action={() => setEditingId("__new__")} />
|
<SectionHeader title={t("ops.knowledgeBase")} count={entries.length} action={() => setEditingId("__new__")} />
|
||||||
<ListFrame>
|
<ListFrame>
|
||||||
{entries.map((entry) => (
|
{entries.map((entry) => (
|
||||||
<ListRow key={entry.id}>
|
<ListRow key={entry.id}>
|
||||||
@@ -591,13 +594,14 @@ export function MeetingsManager({
|
|||||||
agents: AgentRecord[];
|
agents: AgentRecord[];
|
||||||
onRefresh: () => Promise<void>;
|
onRefresh: () => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useT();
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||||
const activeMeeting = useMemo(() => meetings.find((meeting) => meeting.id === editingId) ?? null, [meetings, editingId]);
|
const activeMeeting = useMemo(() => meetings.find((meeting) => meeting.id === editingId) ?? null, [meetings, editingId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SectionHeader title="Meetings" count={meetings.length} action={() => setEditingId("__new__")} />
|
<SectionHeader title={t("ops.meetings")} count={meetings.length} action={() => setEditingId("__new__")} />
|
||||||
<ListFrame>
|
<ListFrame>
|
||||||
{meetings.map((meeting) => (
|
{meetings.map((meeting) => (
|
||||||
<ListRow key={meeting.id}>
|
<ListRow key={meeting.id}>
|
||||||
@@ -660,13 +664,14 @@ export function SprintManager({
|
|||||||
sprints: SprintRecord[];
|
sprints: SprintRecord[];
|
||||||
onRefresh: () => Promise<void>;
|
onRefresh: () => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useT();
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||||
const activeSprint = useMemo(() => sprints.find((sprint) => sprint.id === editingId) ?? null, [sprints, editingId]);
|
const activeSprint = useMemo(() => sprints.find((sprint) => sprint.id === editingId) ?? null, [sprints, editingId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SectionHeader title="Sprint Board" count={sprints.length} action={() => setEditingId("__new__")} />
|
<SectionHeader title={t("ops.sprintBoard")} count={sprints.length} action={() => setEditingId("__new__")} />
|
||||||
<ListFrame>
|
<ListFrame>
|
||||||
{sprints.map((sprint) => (
|
{sprints.map((sprint) => (
|
||||||
<ListRow key={sprint.id}>
|
<ListRow key={sprint.id}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { CheckCircle, SignIn } from "@phosphor-icons/react";
|
|||||||
import type { ConnectorRecord } from "@shared/types";
|
import type { ConnectorRecord } from "@shared/types";
|
||||||
import { unwrap } from "../lib/desktop";
|
import { unwrap } from "../lib/desktop";
|
||||||
import { ActionButton, InlineNotice, Input, Select, StatusPill, TextArea } from "./ui";
|
import { ActionButton, InlineNotice, Input, Select, StatusPill, TextArea } from "./ui";
|
||||||
|
import { useT } from "../i18n";
|
||||||
|
|
||||||
function getAuthActionCopy(connector: ConnectorRecord) {
|
function getAuthActionCopy(connector: ConnectorRecord) {
|
||||||
switch (connector.id) {
|
switch (connector.id) {
|
||||||
@@ -40,6 +41,7 @@ function getAuthActionCopy(connector: ConnectorRecord) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRecord; onSaved: () => Promise<void> }) {
|
export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRecord; onSaved: () => Promise<void> }) {
|
||||||
|
const t = useT();
|
||||||
const [model, setModel] = useState(connector.model ?? "");
|
const [model, setModel] = useState(connector.model ?? "");
|
||||||
const [envBindingText, setEnvBindingText] = useState(connector.envBindingText);
|
const [envBindingText, setEnvBindingText] = useState(connector.envBindingText);
|
||||||
const [notes, setNotes] = useState(connector.notes);
|
const [notes, setNotes] = useState(connector.notes);
|
||||||
@@ -173,14 +175,14 @@ export function ConnectorEditor({ connector, onSaved }: { connector: ConnectorRe
|
|||||||
) : null}
|
) : null}
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div>
|
<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">
|
<div className="flex items-center gap-3">
|
||||||
<StatusPill status={connector.status} />
|
<StatusPill status={connector.status} />
|
||||||
<span className="text-[12px] text-[color:var(--muted)]">{connector.version ?? "version unknown"}</span>
|
<span className="text-[12px] text-[color:var(--muted)]">{connector.version ?? "version unknown"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{Object.entries(connector.capabilityMatrix).map(([key, value]) => (
|
{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)]"}`}>
|
<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}
|
) : null}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
label="Save connector"
|
label={t("conn.saveConnector")}
|
||||||
tone="accent"
|
tone="accent"
|
||||||
onClick={() => void (async () => {
|
onClick={() => void (async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|||||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user