From bf817d8bd1c22ac7194ea27c89f70c048057680b Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Sat, 3 Oct 2026 21:48:38 +0800 Subject: [PATCH] feat(ai): add remote coding agent sessions for SSH hosts (#1545) * feat(ai): add host coding agent sessions with configured providers * Wait for agent initialization and shutdown before resuming sessions * Keep Pi transcripts outside its configuration migration directory * Cover Pi session directory separation * Update core permission catalog expectations for remote agents * Install remote agent runtimes from verified official sources * Wait for the identity panel credentials request in its test * Add scoped SSH forwarding setup for remote agents * Verify forwarding setup through a fresh SSH connection * Bound the SSH forwarding verification timeout * Return the forwarding verification callback result * fix(ai): support agent send and interrupt keyboard shortcuts * feat(ai): add agent queues attachments worktrees and session recovery * fix(ai): handle deleted workspace paths and duplicate status events * fix(ai): accept file references in root workspaces * fix(ai): refresh paused queues after agent errors * fix(ai): store agent sessions in plugin tables and lock down OpenCode Agent sessions lived in a single ctx.kv value holding up to 2000 events, which passed the 256 KB kv limit within minutes of a normal run. Every save after that failed, so history, nativeId, queue and archive changes were lost and every event wrote a kv audit row. Sessions, events and queued prompts now have their own tables. A running agent appends events in batches at most once a second with a lazy save; session and queue changes save immediately. Prompts, drafts and stored event text are capped in UTF-8 bytes to fit MySQL TEXT. kv:own is no longer requested. The OpenCode server now requires a per-session password, so other local accounts on the host cannot drive the agent through its loopback port. * test(upgrade): include agent session tables in the reviewed upgrade SQL --- plugins/ai/AGENTS-FEATURE.md | 47 ++ plugins/ai/locales/en.json | 76 ++ plugins/ai/locales/translated/af_ZA.json | 76 ++ plugins/ai/locales/translated/ar_SA.json | 76 ++ plugins/ai/locales/translated/bg_BG.json | 76 ++ plugins/ai/locales/translated/bn_BD.json | 76 ++ plugins/ai/locales/translated/ca_ES.json | 76 ++ plugins/ai/locales/translated/cs_CZ.json | 76 ++ plugins/ai/locales/translated/da_DK.json | 76 ++ plugins/ai/locales/translated/de_DE.json | 76 ++ plugins/ai/locales/translated/el_GR.json | 76 ++ plugins/ai/locales/translated/es_ES.json | 76 ++ plugins/ai/locales/translated/fi_FI.json | 76 ++ plugins/ai/locales/translated/fr_FR.json | 76 ++ plugins/ai/locales/translated/he_IL.json | 76 ++ plugins/ai/locales/translated/hi_IN.json | 76 ++ plugins/ai/locales/translated/hu_HU.json | 76 ++ plugins/ai/locales/translated/id_ID.json | 76 ++ plugins/ai/locales/translated/it_IT.json | 76 ++ plugins/ai/locales/translated/ja_JP.json | 76 ++ plugins/ai/locales/translated/ko_KR.json | 76 ++ plugins/ai/locales/translated/nl_NL.json | 76 ++ plugins/ai/locales/translated/no_NO.json | 76 ++ plugins/ai/locales/translated/pl_PL.json | 76 ++ plugins/ai/locales/translated/pt_BR.json | 76 ++ plugins/ai/locales/translated/pt_PT.json | 76 ++ plugins/ai/locales/translated/ro_RO.json | 76 ++ plugins/ai/locales/translated/ru_RU.json | 76 ++ plugins/ai/locales/translated/sr_SP.json | 76 ++ plugins/ai/locales/translated/sv_SE.json | 76 ++ plugins/ai/locales/translated/th_TH.json | 76 ++ plugins/ai/locales/translated/tr_TR.json | 76 ++ plugins/ai/locales/translated/uk_UA.json | 76 ++ plugins/ai/locales/translated/vi_VN.json | 76 ++ plugins/ai/locales/translated/zh_CN.json | 76 ++ plugins/ai/locales/translated/zh_TW.json | 76 ++ plugins/ai/manifest.json | 15 +- .../migrations/mysql/0003_agent_sessions.sql | 46 ++ .../postgres/0003_agent_sessions.sql | 46 ++ plugins/ai/migrations/snapshot.json | 172 +++++ .../migrations/sqlite/0003_agent_sessions.sql | 46 ++ plugins/ai/src/backend/agents/forwarding.ts | 95 +++ plugins/ai/src/backend/agents/install.ts | 250 ++++++ .../ai/src/backend/agents/remote-runner.ts | 192 +++++ plugins/ai/src/backend/agents/routes.ts | 709 ++++++++++++++++++ .../ai/src/backend/agents/session-state.ts | 64 ++ plugins/ai/src/backend/agents/store.ts | 221 ++++++ plugins/ai/src/backend/agents/types.ts | 84 +++ plugins/ai/src/backend/agents/workspace.ts | 153 ++++ plugins/ai/src/backend/index.ts | 6 +- plugins/ai/src/backend/tables.ts | 86 ++- .../ai/src/frontend/agents/AgentComposer.tsx | 236 ++++++ plugins/ai/src/frontend/agents/AgentPanel.tsx | 487 ++++++++++++ .../ai/src/frontend/agents/Attachments.tsx | 138 ++++ .../ai/src/frontend/agents/InstallRuntime.tsx | 119 +++ .../ai/src/frontend/agents/SessionTools.tsx | 148 ++++ .../ai/src/frontend/agents/useAgentStream.ts | 104 +++ plugins/ai/src/frontend/index.tsx | 27 + .../ai/tests/backend/agents/access.test.ts | 140 ++++ .../ai/tests/backend/agents/agents.test.ts | 151 ++++ .../tests/backend/agents/forwarding.test.ts | 99 +++ .../ai/tests/backend/agents/install.test.ts | 74 ++ plugins/ai/tests/backend/agents/queue.test.ts | 110 +++ .../backend/agents/session-state.test.ts | 48 ++ plugins/ai/tests/backend/agents/store.test.ts | 101 +++ .../ai/tests/backend/agents/workspace.test.ts | 94 +++ plugins/ai/tests/backend/migrations.test.ts | 6 + plugins/ai/tests/backend/permissions.test.ts | 5 +- plugins/ai/tests/frontend/AgentPanel.test.tsx | 145 ++++ plugins/ai/tests/frontend/activate.test.tsx | 4 +- .../ai/tests/frontend/agent-stream.test.tsx | 71 ++ .../tests/frontend/activate.test.tsx | 2 +- .../tests/fixtures/upgrade/upgrade-mysql.sql | 48 ++ .../fixtures/upgrade/upgrade-postgres.sql | 48 ++ .../tests/plugins/permissions-rbac.test.ts | 1 + 75 files changed, 7292 insertions(+), 6 deletions(-) create mode 100644 plugins/ai/AGENTS-FEATURE.md create mode 100644 plugins/ai/migrations/mysql/0003_agent_sessions.sql create mode 100644 plugins/ai/migrations/postgres/0003_agent_sessions.sql create mode 100644 plugins/ai/migrations/sqlite/0003_agent_sessions.sql create mode 100644 plugins/ai/src/backend/agents/forwarding.ts create mode 100644 plugins/ai/src/backend/agents/install.ts create mode 100644 plugins/ai/src/backend/agents/remote-runner.ts create mode 100644 plugins/ai/src/backend/agents/routes.ts create mode 100644 plugins/ai/src/backend/agents/session-state.ts create mode 100644 plugins/ai/src/backend/agents/store.ts create mode 100644 plugins/ai/src/backend/agents/types.ts create mode 100644 plugins/ai/src/backend/agents/workspace.ts create mode 100644 plugins/ai/src/frontend/agents/AgentComposer.tsx create mode 100644 plugins/ai/src/frontend/agents/AgentPanel.tsx create mode 100644 plugins/ai/src/frontend/agents/Attachments.tsx create mode 100644 plugins/ai/src/frontend/agents/InstallRuntime.tsx create mode 100644 plugins/ai/src/frontend/agents/SessionTools.tsx create mode 100644 plugins/ai/src/frontend/agents/useAgentStream.ts create mode 100644 plugins/ai/tests/backend/agents/access.test.ts create mode 100644 plugins/ai/tests/backend/agents/agents.test.ts create mode 100644 plugins/ai/tests/backend/agents/forwarding.test.ts create mode 100644 plugins/ai/tests/backend/agents/install.test.ts create mode 100644 plugins/ai/tests/backend/agents/queue.test.ts create mode 100644 plugins/ai/tests/backend/agents/session-state.test.ts create mode 100644 plugins/ai/tests/backend/agents/store.test.ts create mode 100644 plugins/ai/tests/backend/agents/workspace.test.ts create mode 100644 plugins/ai/tests/frontend/AgentPanel.test.tsx create mode 100644 plugins/ai/tests/frontend/agent-stream.test.tsx diff --git a/plugins/ai/AGENTS-FEATURE.md b/plugins/ai/AGENTS-FEATURE.md new file mode 100644 index 000000000..9b3c4434b --- /dev/null +++ b/plugins/ai/AGENTS-FEATURE.md @@ -0,0 +1,47 @@ +# Remote AI agents + +The **AI Agent** host action appears on the host card and in the SSH terminal toolbar. It opens a host-scoped session tab. Enable AI globally, opt in for the user, and grant `ai.agents` (administrators by default). + +Choose Pi, OpenCode, Claude Code, or Codex; an existing Termix AI provider; a model; and an absolute working directory. The target needs POSIX SSH, Node.js 22.19+, the selected CLI on its login-shell PATH (or an explicit executable path), and SSH remote forwarding to `127.0.0.1`. Missing dependencies fail visibly. The explicit Install button installs the selected CLI and an isolated Node.js 24 LTS runtime; starting an agent never installs software automatically. + +Termix owns the SSH connection, session history, streaming UI, cancellation, and permission responses. The remote runner uses the CLI's native protocol: Pi RPC, Claude streaming JSON, Codex app-server, or OpenCode's loopback HTTP API. No Paseo runtime or daemon is required. + +Model requests travel over an SSH reverse tunnel to a session-scoped proxy. The proxy authenticates a random token, checks host access and AI permissions, restricts inference paths and the selected model, and uses the current user's saved Provider key and the existing AI private-endpoint allowlist. Provider keys never go to the browser or target. OpenAI/compatible endpoints support Pi and OpenCode; Claude needs Anthropic; Codex needs **Responses**, not just Chat Completions. Selecting an OpenAI-compatible record does not prove Responses support. + +Agent state lives in the target user's `~/.local/state/termix-agents/` and the AI plugin's own tables (`agent_sessions`, with events and queued prompts as separate rows). Prompts, drafts and stored event text are limited to 65,535 UTF-8 bytes, the MySQL TEXT limit. Closing a tab leaves the session running; stop ends it. A server restart stops active transports; Resume reopens the native session where supported. Sessions expire after eight hours. Up to four sessions may run per user. The UI retains the most recent 2,000 events; native history remains on the target. Deleting a Termix session does not delete its target-side directory. + +The agent runs with the SSH user's authority. Claude/OpenCode/Codex approval requests are shown in the tab; Pi's ordinary tools run without per-tool approval. Pi extension prompts are forwarded. Structured native questions currently accept JSON answers. Root SSH gives the agent root privileges; select the account and directory accordingly. No global agent config is overwritten. + +The initial host action asks for the working directory explicitly; it does not infer one from terminal buffer text. Mobile-native UI and cross-host session migration are outside this release. + +## Validation + +`tests/backend/agents/agents.test.ts` covers target/command validation, inference-path restrictions, protocol compatibility, and four native-protocol fixture processes including an approval round trip. A real provider turn must also be checked separately; fixture success is not model access verification. + +## Official runtime installation + +The host panel provides **Install selected agent and Node.js**, with streamed progress and version verification. It uses the same host access, AI opt-in, and `ai.agents` permission as remote sessions, and records an audit event. Installation needs curl, tar, a SHA-256 utility, and Linux/macOS x64 or arm64. It requires no sudo and does not change system packages, shell profiles, existing CLI installations, or SSH forwarding policy. Stop active agents on the host before installing. + +Node.js 24 LTS comes from https://nodejs.org/dist/ and must match the official SHA-256 manifest. Agent package names are fixed to the official documentation: [Pi](https://pi.dev/docs/latest/quickstart), [OpenCode](https://opencode.ai/docs/), [Claude Code](https://code.claude.com/docs/en/setup), and [Codex](https://developers.openai.com/cookbook/examples/codex/secure_quality_gitlab). Only https://registry.npmjs.org dependencies are accepted; missing SHA-512 lock entries are filled from matching official version metadata. npm ci checks the locked integrity values. User/global npm configuration and lifecycle scripts are disabled. Claude/OpenCode use the verified optional platform binary directly, without running download scripts. No third-party mirror or arbitrary installer URL is accepted. + +Files live under `~/.local/share/termix-agent-runtime`. Termix automatically uses this runtime for launches with the default executable. A verified replacement retains the previous installation in `.previous`. Unsupported systems, missing prerequisites, download failures, checksum mismatches, and unavailable platform binaries fail visibly without activating the replacement. SSH remote forwarding remains a separate administrator setting. + +## Enable Agent forwarding + +The separate **Enable Agent forwarding** button configures standard Linux/systemd OpenSSH servers using root or non-interactive sudo. It derives the SSH account and actual peer address from the SSH connection, rather than accepting them from the browser. Existing working remote forwarding is left unchanged. Otherwise, it inserts a scoped `Match User ... Address ...` block allowing `127.0.0.1:*` remote listeners with `GatewayPorts no`; existing local forwarding is preserved. This uses the same host access and `ai.agents` gates and records an audit event. + +The operation backs up sshd_config under `/etc/ssh/termix-agent-backups`, validates both syntax and effective rules before activation, and reloads the active SSH service. Failed activation/reload restores the previous configuration. Existing `DisableForwarding` policies are not overridden. Start/resume creates a new SSH connection after a successful change. Idempotency, preservation of local forwarding, invalid configuration rejection, and reload-failure rollback are covered by executable shell fixtures. + +After configuration, Termix opens a fresh SSH connection and verifies an actual loopback remote listener before reporting success. Key-level forwarding restrictions or a different active server configuration therefore remain visible failures. + +The conversation composer sends with Enter and inserts a newline with Shift+Enter. Escape interrupts a running agent when focus is inside its panel. IME composition and repeated keydown events do not submit or interrupt; a failed send preserves the draft. + +## Conversation workflow + +- Enter sends an idle turn or queues a message while running. A session stores up to 20 queued messages, each editable/removable until dispatched. Dispatch is FIFO after completion; cancellation, archive, and remote errors pause the queue. Continue explicitly resumes it. A stopped session must be resumed first. Messages already dispatched are not automatically replayed after a transport failure. +- Rename and search sessions by name/model/directory. Archive stops the remote process and pauses its queue; restore preserves its transcript. Composer drafts are saved per session after 500 ms and on tab/session unmount. Unsent attachments remain reusable in the session attachment picker. +- Upload a file, paste an image into the composer, or reference a regular file within the working directory (symlinks outside it are rejected). Limits: 1 MiB per file, four per message, 100 per session. Uploads live in the remote user's private session directory. Image MIME is detected from bytes and sent through native Pi/Claude/Codex/OpenCode image inputs; the selected provider/model must support vision. Other files are referenced by their host path for the agent's tools. No file is sent to the model until a message using it is dispatched. +- Git review lists changed/untracked files and shows unstaged and staged diffs, including deleted files. Worktree creation makes a new branch from HEAD in the remote user's Termix state directory. It preserves uncommitted changes in the original workspace, does not overwrite an existing branch, and offers a new session in the resulting directory. Repository hooks, external diff commands, and textconv are disabled for these operations. Worktrees are retained until the user removes them explicitly. +- Browser event streams reconnect with bounded backoff and refresh a persisted snapshot before continuing from its sequence cursor. Duplicate events are suppressed and events are written in batches at most once a second. Access-denied sessions stop retrying. A server/SSH restart still requires Resume; the UI never replays a potentially executed command. The 2,000-event UI retention limit still applies. + +Tests cover FIFO/cancel behavior through the HTTP routes, cross-user metadata/attachment access, native image payloads for four CLI protocols, real temporary Git repositories, path/size boundaries, archive persistence, keyboard/IME handling, and dropped-stream recovery. diff --git a/plugins/ai/locales/en.json b/plugins/ai/locales/en.json index 25ac9b8bb..9bf378297 100644 --- a/plugins/ai/locales/en.json +++ b/plugins/ai/locales/en.json @@ -116,6 +116,82 @@ "hosts": { "docsLink": "View docs" }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" + }, "plugin": { "name": "AI Assistant" } diff --git a/plugins/ai/locales/translated/af_ZA.json b/plugins/ai/locales/translated/af_ZA.json index 339bb3883..c252bc8be 100644 --- a/plugins/ai/locales/translated/af_ZA.json +++ b/plugins/ai/locales/translated/af_ZA.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Bekyk dokumente" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/ar_SA.json b/plugins/ai/locales/translated/ar_SA.json index 9be5b2dea..dcf0d8080 100644 --- a/plugins/ai/locales/translated/ar_SA.json +++ b/plugins/ai/locales/translated/ar_SA.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "عرض المستندات" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/bg_BG.json b/plugins/ai/locales/translated/bg_BG.json index 07231463b..d9f7a3f46 100644 --- a/plugins/ai/locales/translated/bg_BG.json +++ b/plugins/ai/locales/translated/bg_BG.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Преглед на документи" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/bn_BD.json b/plugins/ai/locales/translated/bn_BD.json index 7b5758ffa..3605ad2be 100644 --- a/plugins/ai/locales/translated/bn_BD.json +++ b/plugins/ai/locales/translated/bn_BD.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "View docs" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/ca_ES.json b/plugins/ai/locales/translated/ca_ES.json index a9184d446..f09b681c7 100644 --- a/plugins/ai/locales/translated/ca_ES.json +++ b/plugins/ai/locales/translated/ca_ES.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Veure documents" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/cs_CZ.json b/plugins/ai/locales/translated/cs_CZ.json index 9a47a96ec..ec49754ce 100644 --- a/plugins/ai/locales/translated/cs_CZ.json +++ b/plugins/ai/locales/translated/cs_CZ.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Zobrazit dokumenty" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/da_DK.json b/plugins/ai/locales/translated/da_DK.json index 72dc289ba..498080017 100644 --- a/plugins/ai/locales/translated/da_DK.json +++ b/plugins/ai/locales/translated/da_DK.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Se dokumenter" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/de_DE.json b/plugins/ai/locales/translated/de_DE.json index e6d6d4ef0..4d05faf1d 100644 --- a/plugins/ai/locales/translated/de_DE.json +++ b/plugins/ai/locales/translated/de_DE.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Dokumentation anzeigen" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/el_GR.json b/plugins/ai/locales/translated/el_GR.json index cb2ddde53..8598f1f05 100644 --- a/plugins/ai/locales/translated/el_GR.json +++ b/plugins/ai/locales/translated/el_GR.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Προβολή εγγράφων" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/es_ES.json b/plugins/ai/locales/translated/es_ES.json index 14430a48d..3ce7e9946 100644 --- a/plugins/ai/locales/translated/es_ES.json +++ b/plugins/ai/locales/translated/es_ES.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Ver documentación" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/fi_FI.json b/plugins/ai/locales/translated/fi_FI.json index 1145b28d3..55d58e04b 100644 --- a/plugins/ai/locales/translated/fi_FI.json +++ b/plugins/ai/locales/translated/fi_FI.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Näytä dokumentit" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/fr_FR.json b/plugins/ai/locales/translated/fr_FR.json index ef5361462..f77498c9f 100644 --- a/plugins/ai/locales/translated/fr_FR.json +++ b/plugins/ai/locales/translated/fr_FR.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Voir la documentation" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/he_IL.json b/plugins/ai/locales/translated/he_IL.json index f5e2c4e34..606ddd78e 100644 --- a/plugins/ai/locales/translated/he_IL.json +++ b/plugins/ai/locales/translated/he_IL.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "הצג מסמכים" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/hi_IN.json b/plugins/ai/locales/translated/hi_IN.json index e75d94f64..42fa2a32e 100644 --- a/plugins/ai/locales/translated/hi_IN.json +++ b/plugins/ai/locales/translated/hi_IN.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "दस्तावेज़ देखें" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/hu_HU.json b/plugins/ai/locales/translated/hu_HU.json index 29f3df4a5..913ee669e 100644 --- a/plugins/ai/locales/translated/hu_HU.json +++ b/plugins/ai/locales/translated/hu_HU.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Dokumentumok megtekintése" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/id_ID.json b/plugins/ai/locales/translated/id_ID.json index 7b5758ffa..3605ad2be 100644 --- a/plugins/ai/locales/translated/id_ID.json +++ b/plugins/ai/locales/translated/id_ID.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "View docs" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/it_IT.json b/plugins/ai/locales/translated/it_IT.json index 5a0a51a67..cf6e9f0a1 100644 --- a/plugins/ai/locales/translated/it_IT.json +++ b/plugins/ai/locales/translated/it_IT.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Visualizza documentazione" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/ja_JP.json b/plugins/ai/locales/translated/ja_JP.json index 16c7460dd..b477851ec 100644 --- a/plugins/ai/locales/translated/ja_JP.json +++ b/plugins/ai/locales/translated/ja_JP.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "ドキュメントを表示" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/ko_KR.json b/plugins/ai/locales/translated/ko_KR.json index c95fc4f25..807818108 100644 --- a/plugins/ai/locales/translated/ko_KR.json +++ b/plugins/ai/locales/translated/ko_KR.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "문서 보기" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/nl_NL.json b/plugins/ai/locales/translated/nl_NL.json index c601bcef6..8a6b30df4 100644 --- a/plugins/ai/locales/translated/nl_NL.json +++ b/plugins/ai/locales/translated/nl_NL.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Documenten bekijken" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/no_NO.json b/plugins/ai/locales/translated/no_NO.json index 3dd895392..b5141fd4e 100644 --- a/plugins/ai/locales/translated/no_NO.json +++ b/plugins/ai/locales/translated/no_NO.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "View docs" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/pl_PL.json b/plugins/ai/locales/translated/pl_PL.json index a1fd9c256..8695a871d 100644 --- a/plugins/ai/locales/translated/pl_PL.json +++ b/plugins/ai/locales/translated/pl_PL.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Zobacz dokumentację" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/pt_BR.json b/plugins/ai/locales/translated/pt_BR.json index 9cc13f482..a48c4d640 100644 --- a/plugins/ai/locales/translated/pt_BR.json +++ b/plugins/ai/locales/translated/pt_BR.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Documentação" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/pt_PT.json b/plugins/ai/locales/translated/pt_PT.json index 6c9aee940..e03aae197 100644 --- a/plugins/ai/locales/translated/pt_PT.json +++ b/plugins/ai/locales/translated/pt_PT.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Ver documentação" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/ro_RO.json b/plugins/ai/locales/translated/ro_RO.json index 9aa460137..71174b46a 100644 --- a/plugins/ai/locales/translated/ro_RO.json +++ b/plugins/ai/locales/translated/ro_RO.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Vizualizați documentele" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/ru_RU.json b/plugins/ai/locales/translated/ru_RU.json index bb9ef6af6..7214aa114 100644 --- a/plugins/ai/locales/translated/ru_RU.json +++ b/plugins/ai/locales/translated/ru_RU.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Документация" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/sr_SP.json b/plugins/ai/locales/translated/sr_SP.json index 7b5758ffa..3605ad2be 100644 --- a/plugins/ai/locales/translated/sr_SP.json +++ b/plugins/ai/locales/translated/sr_SP.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "View docs" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/sv_SE.json b/plugins/ai/locales/translated/sv_SE.json index 7b5758ffa..3605ad2be 100644 --- a/plugins/ai/locales/translated/sv_SE.json +++ b/plugins/ai/locales/translated/sv_SE.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "View docs" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/th_TH.json b/plugins/ai/locales/translated/th_TH.json index 7b5758ffa..3605ad2be 100644 --- a/plugins/ai/locales/translated/th_TH.json +++ b/plugins/ai/locales/translated/th_TH.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "View docs" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/tr_TR.json b/plugins/ai/locales/translated/tr_TR.json index e2c58918d..00725ba3b 100644 --- a/plugins/ai/locales/translated/tr_TR.json +++ b/plugins/ai/locales/translated/tr_TR.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Dokümanları görüntüle" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/uk_UA.json b/plugins/ai/locales/translated/uk_UA.json index 7b5758ffa..3605ad2be 100644 --- a/plugins/ai/locales/translated/uk_UA.json +++ b/plugins/ai/locales/translated/uk_UA.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "View docs" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/vi_VN.json b/plugins/ai/locales/translated/vi_VN.json index e73b28930..7272afdb3 100644 --- a/plugins/ai/locales/translated/vi_VN.json +++ b/plugins/ai/locales/translated/vi_VN.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "Xem tài liệu" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/locales/translated/zh_CN.json b/plugins/ai/locales/translated/zh_CN.json index cc88ec8f6..b039a3dc5 100644 --- a/plugins/ai/locales/translated/zh_CN.json +++ b/plugins/ai/locales/translated/zh_CN.json @@ -116,6 +116,82 @@ "hosts": { "docsLink": "查看文档" }, + "agents": { + "title": "AI Agent", + "permissionTitle": "运行远程 AI Agent", + "permissionDescription": "使用已配置的 AI Provider 在 SSH 主机上启动 Agent。Agent 可使用 SSH 账号权限执行命令和修改文件。", + "new": "新建会话", + "description": "在此主机的目录中运行 Agent,使用 Termix 配置的模型服务。", + "runtime": "Agent", + "provider": "Termix Provider", + "chooseProvider": "选择兼容的 Provider", + "model": "模型", + "directory": "工作目录", + "executable": "可执行文件(可选绝对路径)", + "requirements": "主机需要 Node.js 22.19+ 及所选 Agent,并允许 SSH 回环地址远程转发。不覆盖已有 Agent 配置。", + "responsesRequired": "Codex 需要 Responses API;仅支持 Chat Completions 的接口无法用于 Codex。", + "piPermissions": "Pi 工具使用当前 SSH 账号权限执行。请只在允许 Agent 修改的目录中启动。", + "start": "启动 Agent", + "resume": "恢复会话", + "stop": "停止会话", + "interrupt": "中断任务", + "send": "发送", + "prompt": "让 Agent 在此目录中完成任务…", + "noSessions": "此主机还没有 Agent 会话。", + "approval": "Agent 需要你的决定", + "answer": "填写答复(结构化问题使用 JSON)", + "allow": "允许一次", + "deny": "拒绝", + "tool": "工具执行", + "streamFailed": "Agent 消息连接断开,请重新打开会话连接。", + "status": { + "starting": "正在启动", + "ready": "就绪", + "running": "执行中", + "stopped": "已停止", + "error": "出错" + }, + "installRuntime": "安装所选 Agent 和 Node.js", + "installSource": "仅从官方来源安装:nodejs.org(SHA-256 校验)、registry.npmjs.org(锁定依赖与完整性校验)。安装到当前 SSH 用户的 ~/.local/share/termix-agent-runtime;需要 curl、tar 和 Linux/macOS x64 或 arm64。不修改 SSH 转发策略。", + "installSucceeded": "安装及版本校验成功。现在可以启动 Agent;SSH 还需允许回环地址远程转发。", + "installFailed": "安装失败,请查看日志后重试。", + "installLog": "安装日志", + "enableForwarding": "一键启用 Agent 转发", + "forwardingScope": "需要 Linux、systemd/OpenSSH,以及 root 或免密 sudo。自动备份、校验并重载 SSH,仅为当前 SSH 账号与实际连接来源开放回环远程转发,保留已有本地转发权限。失败自动回滚。", + "forwardingSucceeded": "SSH 回环远程转发已就绪。请启动或恢复 Agent,建立新的 SSH 连接。", + "forwardingFailed": "SSH 转发配置失败,请查看日志;未通过校验的配置不会保留。", + "search": "搜索会话", + "showArchived": "显示已归档", + "sessionName": "会话名称", + "rename": "重命名", + "archive": "归档并停止", + "restore": "恢复归档", + "queue": "待发消息", + "enqueue": "加入队列", + "continueQueue": "继续队列", + "pauseQueue": "暂停队列", + "editQueued": "编辑待发消息", + "save": "保存", + "remove": "移除", + "upload": "上传附件", + "remoteFile": "工作目录内的文件路径", + "attach": "引用文件", + "attachments": "复用会话附件", + "pasteImage": "可在输入框粘贴图片。每条最多 4 个附件,每个不超过 1 MiB。", + "attachmentLimit": "每条最多 4 个附件,每个不超过 1 MiB", + "review": "Git 改动与 worktree", + "gitStatus": "刷新 Git 状态", + "diffFile": "要审阅的文件路径", + "diff": "查看 diff", + "branch": "新分支名称", + "createWorktree": "创建 worktree", + "useWorktree": "在此 worktree 新建会话", + "worktreeHint": "从 HEAD 创建新分支。未提交改动保留在当前目录,不会自动删除已有 worktree。", + "clean": "没有改动", + "reconnecting": "正在重连并补齐消息…", + "unavailable": "无法访问会话,请检查权限或重新登录。", + "shortcuts": "Enter 发送或排队 · Shift+Enter 换行 · Esc 中断并暂停队列" + }, "plugin": { "name": "AI 助手" } diff --git a/plugins/ai/locales/translated/zh_TW.json b/plugins/ai/locales/translated/zh_TW.json index 57535668c..dc5a3d049 100644 --- a/plugins/ai/locales/translated/zh_TW.json +++ b/plugins/ai/locales/translated/zh_TW.json @@ -115,5 +115,81 @@ }, "hosts": { "docsLink": "檢視文件" + }, + "agents": { + "title": "AI Agent", + "permissionTitle": "Run remote AI agents", + "permissionDescription": "Start coding agents on SSH hosts using configured AI providers. Agents can execute commands and change files as the SSH user.", + "new": "New session", + "description": "Run an agent in a directory on this host using a Termix AI provider.", + "runtime": "Agent", + "provider": "Termix provider", + "chooseProvider": "Choose a compatible provider", + "model": "Model", + "directory": "Working directory", + "executable": "Executable (optional absolute path)", + "requirements": "The host needs Node.js 22.19+ and the selected agent installed. SSH remote loopback forwarding must be allowed. Existing agent configurations are not overwritten.", + "responsesRequired": "Codex requires a Responses API endpoint. A Chat Completions-only endpoint cannot run Codex.", + "piPermissions": "Pi tools run with this SSH account’s permissions. Only launch in a directory you authorize the agent to modify.", + "start": "Start agent", + "resume": "Resume", + "stop": "Stop session", + "interrupt": "Interrupt turn", + "send": "Send", + "prompt": "Ask the agent to work in this directory…", + "noSessions": "No agent sessions for this host.", + "approval": "Agent needs your decision", + "answer": "Answer (JSON for structured questions)", + "allow": "Allow once", + "deny": "Deny", + "tool": "Tool activity", + "streamFailed": "Agent stream disconnected. Reopen the session to reconnect.", + "status": { + "starting": "Starting", + "ready": "Ready", + "running": "Working", + "stopped": "Stopped", + "error": "Error" + }, + "installRuntime": "Install selected agent and Node.js", + "installSource": "Official sources only: nodejs.org (SHA-256 verified) and registry.npmjs.org (locked package integrity). Installs for the SSH user in ~/.local/share/termix-agent-runtime; requires curl, tar, and Linux/macOS x64 or arm64. Does not change SSH forwarding policy.", + "installSucceeded": "Installation verified. You can now start the agent; SSH loopback remote forwarding must also be allowed.", + "installFailed": "Installation failed. Review the log and retry.", + "installLog": "Installation log", + "enableForwarding": "Enable Agent forwarding", + "forwardingScope": "Linux with systemd/OpenSSH; requires root or passwordless sudo. Backs up and validates SSH configuration, then reloads it. Only the current SSH account and observed connection source receive loopback remote forwarding; existing local forwarding is preserved. Failed changes are rolled back.", + "forwardingSucceeded": "SSH loopback remote forwarding is ready. Start or resume the agent to open a new SSH connection.", + "forwardingFailed": "SSH forwarding configuration failed. Review the log; no unvalidated configuration is kept.", + "search": "Search sessions", + "showArchived": "Show archived", + "sessionName": "Session name", + "rename": "Rename", + "archive": "Archive and stop", + "restore": "Restore session", + "queue": "Queued messages", + "enqueue": "Queue message", + "continueQueue": "Continue queue", + "pauseQueue": "Pause queue", + "editQueued": "Edit queued message", + "save": "Save", + "remove": "Remove", + "upload": "Upload attachment", + "remoteFile": "File path inside working directory", + "attach": "Attach file", + "attachments": "Reuse a session attachment", + "pasteImage": "Paste an image in the composer. Up to 4 attachments, 1 MiB each.", + "attachmentLimit": "Up to 4 attachments, 1 MiB each", + "review": "Git changes and worktrees", + "gitStatus": "Refresh Git status", + "diffFile": "File path to review", + "diff": "View diff", + "branch": "New branch name", + "createWorktree": "Create worktree", + "useWorktree": "New session in this worktree", + "worktreeHint": "Creates a new branch from HEAD. Uncommitted changes stay in the current directory. Existing worktrees are never deleted automatically.", + "clean": "No changes", + "reconnecting": "Reconnecting and restoring messages…", + "unavailable": "Session is no longer accessible. Check your permissions or sign in again.", + "shortcuts": "Enter sends or queues · Shift+Enter adds a line · Esc interrupts and pauses the queue" } } diff --git a/plugins/ai/manifest.json b/plugins/ai/manifest.json index 0375022b8..22801d41b 100644 --- a/plugins/ai/manifest.json +++ b/plugins/ai/manifest.json @@ -25,7 +25,8 @@ "notify:send", "network:serve", "ui:surface", - "users:impersonate" + "users:impersonate", + "events:core" ], "optionalDependencies": { "workspaces": "^1.0.0", @@ -87,6 +88,12 @@ "titleKey": "nav.ai", "icon": "Sparkles", "openFrom": ["rail", "palette"] + }, + { + "id": "ai-agents", + "titleKey": "agents.title", + "icon": "Bot", + "openFrom": ["host-context-menu", "palette"] } ], "actions": [ @@ -130,6 +137,12 @@ "titleKey": "permissions.secrets.share.title", "descriptionKey": "permissions.secrets.share.description", "defaultRoles": ["user"] + }, + { + "name": "agents", + "titleKey": "agents.permissionTitle", + "descriptionKey": "agents.permissionDescription", + "defaultRoles": ["admin"] } ], "settings": { diff --git a/plugins/ai/migrations/mysql/0003_agent_sessions.sql b/plugins/ai/migrations/mysql/0003_agent_sessions.sql new file mode 100644 index 000000000..76c6e8ce2 --- /dev/null +++ b/plugins/ai/migrations/mysql/0003_agent_sessions.sql @@ -0,0 +1,46 @@ +-- ai 0003: agent_sessions +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS `p_ai_agent_sessions` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `session_id` varchar(36) NOT NULL UNIQUE, + `user_id` varchar(255) NOT NULL, + `host_id` int NOT NULL, + `provider_id` int NOT NULL, + `agent` varchar(32) NOT NULL, + `model` text NOT NULL, + `cwd` text NOT NULL, + `executable` text NOT NULL, + `native_id` text, + `title` text, + `archived` boolean NOT NULL DEFAULT false, + `queue_paused` boolean NOT NULL DEFAULT false, + `draft` text, + `attachments` text NOT NULL DEFAULT ('[]'), + `updated_at` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); +CREATE INDEX `idx_ai_agent_sessions_user` ON `p_ai_agent_sessions` (`user_id`); +CREATE TABLE IF NOT EXISTS `p_ai_agent_events` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `session_id` varchar(36) NOT NULL, + `user_id` varchar(255) NOT NULL, + `seq` int NOT NULL, + `kind` varchar(16) NOT NULL, + `text` text NOT NULL, + `request_id` text, + `choices` text, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); +CREATE UNIQUE INDEX `idx_ai_agent_events_session_seq` ON `p_ai_agent_events` (`session_id`, `seq`); +CREATE TABLE IF NOT EXISTS `p_ai_agent_queue` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `session_id` varchar(36) NOT NULL, + `user_id` varchar(255) NOT NULL, + `prompt_id` varchar(36) NOT NULL, + `position` int NOT NULL, + `text` text NOT NULL, + `attachment_ids` text NOT NULL DEFAULT ('[]'), + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); +CREATE INDEX `idx_ai_agent_queue_session` ON `p_ai_agent_queue` (`session_id`, `position`); diff --git a/plugins/ai/migrations/postgres/0003_agent_sessions.sql b/plugins/ai/migrations/postgres/0003_agent_sessions.sql new file mode 100644 index 000000000..b79c1e8ee --- /dev/null +++ b/plugins/ai/migrations/postgres/0003_agent_sessions.sql @@ -0,0 +1,46 @@ +-- ai 0003: agent_sessions +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "p_ai_agent_sessions" ( + "id" serial PRIMARY KEY, + "session_id" varchar(36) NOT NULL UNIQUE, + "user_id" varchar(255) NOT NULL, + "host_id" integer NOT NULL, + "provider_id" integer NOT NULL, + "agent" varchar(32) NOT NULL, + "model" text NOT NULL, + "cwd" text NOT NULL, + "executable" text NOT NULL, + "native_id" text, + "title" text, + "archived" boolean NOT NULL DEFAULT false, + "queue_paused" boolean NOT NULL DEFAULT false, + "draft" text, + "attachments" text NOT NULL DEFAULT '[]', + "updated_at" text NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_ai_agent_sessions_user" ON "p_ai_agent_sessions" ("user_id"); +CREATE TABLE IF NOT EXISTS "p_ai_agent_events" ( + "id" serial PRIMARY KEY, + "session_id" varchar(36) NOT NULL, + "user_id" varchar(255) NOT NULL, + "seq" integer NOT NULL, + "kind" varchar(16) NOT NULL, + "text" text NOT NULL, + "request_id" text, + "choices" text, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_ai_agent_events_session_seq" ON "p_ai_agent_events" ("session_id", "seq"); +CREATE TABLE IF NOT EXISTS "p_ai_agent_queue" ( + "id" serial PRIMARY KEY, + "session_id" varchar(36) NOT NULL, + "user_id" varchar(255) NOT NULL, + "prompt_id" varchar(36) NOT NULL, + "position" integer NOT NULL, + "text" text NOT NULL, + "attachment_ids" text NOT NULL DEFAULT '[]', + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_ai_agent_queue_session" ON "p_ai_agent_queue" ("session_id", "position"); diff --git a/plugins/ai/migrations/snapshot.json b/plugins/ai/migrations/snapshot.json index 5d98ed661..872577b88 100644 --- a/plugins/ai/migrations/snapshot.json +++ b/plugins/ai/migrations/snapshot.json @@ -8,6 +8,11 @@ "primaryKey": true, "notNull": true }, + "syncId": { + "type": "varchar", + "length": 255, + "unique": true + }, "userId": { "type": "refUser", "length": 255, @@ -191,6 +196,173 @@ } ], "adopts": "ai_proposals" + }, + "agent_sessions": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "sessionId": { + "type": "varchar", + "length": 36, + "notNull": true, + "unique": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "hostId": { + "type": "integer", + "notNull": true + }, + "providerId": { + "type": "integer", + "notNull": true + }, + "agent": { + "type": "varchar", + "length": 32, + "notNull": true + }, + "model": { + "type": "text", + "notNull": true + }, + "cwd": { + "type": "text", + "notNull": true + }, + "executable": { + "type": "text", + "notNull": true + }, + "nativeId": { + "type": "text" + }, + "title": { + "type": "text" + }, + "archived": { + "type": "boolean", + "notNull": true, + "defaultValue": false + }, + "queuePaused": { + "type": "boolean", + "notNull": true, + "defaultValue": false + }, + "draft": { + "type": "text" + }, + "attachments": { + "type": "text", + "notNull": true, + "defaultValue": "[]" + }, + "updatedAt": { + "type": "text", + "notNull": true + } + }, + "indexes": [ + { + "name": "idx_ai_agent_sessions_user", + "columns": ["userId"] + } + ] + }, + "agent_events": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "sessionId": { + "type": "varchar", + "length": 36, + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "seq": { + "type": "integer", + "notNull": true + }, + "kind": { + "type": "varchar", + "length": 16, + "notNull": true + }, + "text": { + "type": "text", + "notNull": true + }, + "requestId": { + "type": "text" + }, + "choices": { + "type": "text" + } + }, + "indexes": [ + { + "name": "idx_ai_agent_events_session_seq", + "columns": ["sessionId", "seq"], + "unique": true + } + ] + }, + "agent_queue": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "sessionId": { + "type": "varchar", + "length": 36, + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "promptId": { + "type": "varchar", + "length": 36, + "notNull": true + }, + "position": { + "type": "integer", + "notNull": true + }, + "text": { + "type": "text", + "notNull": true + }, + "attachmentIds": { + "type": "text", + "notNull": true, + "defaultValue": "[]" + } + }, + "indexes": [ + { + "name": "idx_ai_agent_queue_session", + "columns": ["sessionId", "position"] + } + ] } } } diff --git a/plugins/ai/migrations/sqlite/0003_agent_sessions.sql b/plugins/ai/migrations/sqlite/0003_agent_sessions.sql new file mode 100644 index 000000000..dd245d2d3 --- /dev/null +++ b/plugins/ai/migrations/sqlite/0003_agent_sessions.sql @@ -0,0 +1,46 @@ +-- ai 0003: agent_sessions +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "p_ai_agent_sessions" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "session_id" text NOT NULL UNIQUE, + "user_id" text NOT NULL, + "host_id" integer NOT NULL, + "provider_id" integer NOT NULL, + "agent" text NOT NULL, + "model" text NOT NULL, + "cwd" text NOT NULL, + "executable" text NOT NULL, + "native_id" text, + "title" text, + "archived" integer NOT NULL DEFAULT 0, + "queue_paused" integer NOT NULL DEFAULT 0, + "draft" text, + "attachments" text NOT NULL DEFAULT '[]', + "updated_at" text NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_ai_agent_sessions_user" ON "p_ai_agent_sessions" ("user_id"); +CREATE TABLE IF NOT EXISTS "p_ai_agent_events" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "session_id" text NOT NULL, + "user_id" text NOT NULL, + "seq" integer NOT NULL, + "kind" text NOT NULL, + "text" text NOT NULL, + "request_id" text, + "choices" text, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS "idx_ai_agent_events_session_seq" ON "p_ai_agent_events" ("session_id", "seq"); +CREATE TABLE IF NOT EXISTS "p_ai_agent_queue" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "session_id" text NOT NULL, + "user_id" text NOT NULL, + "prompt_id" text NOT NULL, + "position" integer NOT NULL, + "text" text NOT NULL, + "attachment_ids" text NOT NULL DEFAULT '[]', + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS "idx_ai_agent_queue_session" ON "p_ai_agent_queue" ("session_id", "position"); diff --git a/plugins/ai/src/backend/agents/forwarding.ts b/plugins/ai/src/backend/agents/forwarding.ts new file mode 100644 index 000000000..1b7f8c178 --- /dev/null +++ b/plugins/ai/src/backend/agents/forwarding.ts @@ -0,0 +1,95 @@ +import { shellQuote } from "./types.js"; + +export const FORWARDING_ADMIN_SCRIPT = String.raw`set -eu +account=$1 +peer=$2 +[ "$(uname -s)" = Linux ] || { echo 'Automatic SSH configuration currently requires Linux with systemd and OpenSSH'; exit 1; } +sshd=/usr/sbin/sshd +config=/etc/ssh/sshd_config +[ -x "$sshd" ] && [ -f "$config" ] || { echo 'OpenSSH server configuration not found'; exit 1; } +"$sshd" -t +if systemctl is-active --quiet ssh; then service=ssh; elif systemctl is-active --quiet sshd; then service=sshd; else echo 'No active systemd SSH service found'; exit 1; fi +lock=/etc/ssh/.termix-agent-forward.lock +mkdir "$lock" 2>/dev/null || { echo 'Another SSH configuration operation is running'; exit 1; } +candidate= +backup= +changed=0 +committed=0 +cleanup() { + code=$? + trap - EXIT + if [ "$changed" = 1 ] && [ "$committed" = 0 ]; then + echo 'Restoring previous SSH configuration' + cp -p "$backup" "$config" + "$sshd" -t && systemctl reload "$service" || echo "SSH rollback reload failed; restore $backup manually" + fi + if [ -n "$candidate" ] && [ -f "$candidate" ]; then rm "$candidate"; fi + rmdir "$lock" + exit "$code" +} +trap cleanup EXIT +trap 'exit 130' HUP INT TERM +context="user=$account,addr=$peer,host=termix" +effective=$("$sshd" -T -C "$context") +echo "SSH account: $account; observed connection source: $peer" +if printf '%s\n' "$effective" | grep -qx 'disableforwarding yes'; then + echo 'DisableForwarding is enabled; an administrator must review this policy first' + exit 1 +fi +tcp=$(printf '%s\n' "$effective" | awk '$1 == "allowtcpforwarding" {print $2}') +listen=$(printf '%s\n' "$effective" | awk '$1 == "permitlisten" {$1=""; print}') +case "$tcp" in + yes|all|remote) + for rule in $listen; do + case "$rule" in any|'127.0.0.1:*'|'*:*') echo 'Loopback remote forwarding is already allowed'; exit 0;; esac + done;; +esac +# Preserve an already allowed local forwarding capability. +case "$tcp" in yes|all|local) tcp=yes;; *) tcp=remote;; esac +begin="# BEGIN Termix Agent $account $peer" +end="# END Termix Agent $account $peer" +block=$(printf '%s\nMatch User %s Address %s\n AllowTcpForwarding %s\n PermitListen 127.0.0.1:*\n GatewayPorts no\n%s\n' "$begin" "$account" "$peer" "$tcp" "$end") +candidate=$(mktemp /etc/ssh/.termix-sshd.XXXXXX) +cp -p "$config" "$candidate" +awk -v begin="$begin" -v end="$end" -v block="$block" ' + $0 == begin { skip=1; next } + $0 == end { skip=0; next } + !skip { + if (!inserted && ($0 ~ /^[ \t]*Match[ \t]/ || $0 ~ /^# BEGIN Termix Agent /)) { print block; inserted=1 } + print + } + END { if (!inserted) print block } +' "$config" > "$candidate" +"$sshd" -t -f "$candidate" +check=$("$sshd" -T -f "$candidate" -C "$context") +printf '%s\n' "$check" | grep -qx "allowtcpforwarding $tcp" +printf '%s\n' "$check" | grep -Fqx 'permitlisten 127.0.0.1:*' +printf '%s\n' "$check" | grep -qx 'gatewayports no' +printf '%s\n' "$check" | grep -qx 'disableforwarding no' +mkdir -p /etc/ssh/termix-agent-backups +chmod 700 /etc/ssh/termix-agent-backups +backup=$(mktemp /etc/ssh/termix-agent-backups/sshd_config.XXXXXX) +cp -p "$config" "$backup" +echo "Backup: $backup" +changed=1 +mv "$candidate" "$config" +systemctl reload "$service" +"$sshd" -t +committed=1 +echo 'Enabled loopback remote forwarding for this SSH account and source only' +`; + +export function forwardingScript(): string { + return `set -eu +account=$(id -un) +peer=$(printf '%s' "$SSH_CONNECTION" | cut -d ' ' -f 1) +printf '%s' "$account" | grep -Eq '^[A-Za-z0-9_.-]+$' || { echo 'Unsupported SSH account name'; exit 1; } +printf '%s' "$peer" | grep -Eq '^[0-9a-fA-F:.]+$' || { echo 'Unable to determine the SSH connection source'; exit 1; } +script=${shellQuote(FORWARDING_ADMIN_SCRIPT)} +if [ "$(id -u)" = 0 ]; then + exec sh -c "$script" termix "$account" "$peer" +else + exec sudo -n sh -c "$script" termix "$account" "$peer" +fi +`; +} diff --git a/plugins/ai/src/backend/agents/install.ts b/plugins/ai/src/backend/agents/install.ts new file mode 100644 index 000000000..177321fe9 --- /dev/null +++ b/plugins/ai/src/backend/agents/install.ts @@ -0,0 +1,250 @@ +import type { Router } from "express"; +import type { Client, ClientChannel } from "ssh2"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { forwardingScript } from "./forwarding.js"; +import { AGENTS, shellQuote, type AgentKind } from "./types.js"; + +export const PACKAGES: Record = { + pi: "@earendil-works/pi-coding-agent", + opencode: "opencode-ai", + claude: "@anthropic-ai/claude-code", + codex: "@openai/codex", +}; + +// No caller-supplied commands, versions, registries, or download URLs. +export function installScript(agent: AgentKind): string { + if (!AGENTS.includes(agent)) throw Error("Unsupported agent"); + return `set -eu +umask 077 +agent=${shellQuote(agent)} +package=${shellQuote(PACKAGES[agent])} +root="$HOME/.local/share/termix-agent-runtime" +mkdir -p "$root" +lock="$root/install.lock" +mkdir "$lock" 2>/dev/null || { echo 'Another installation is running'; exit 1; } +stage=$(mktemp -d "$root/staging.XXXXXX") +trap 'rm -rf "$stage"; rmdir "$lock"' EXIT +trap 'exit 130' HUP INT TERM +case "$(uname -s)" in Linux) platform=linux;; Darwin) platform=darwin;; *) echo 'Only Linux and macOS are supported'; exit 1;; esac +case "$(uname -m)" in x86_64|amd64) arch=x64;; aarch64|arm64) arch=arm64;; *) echo 'Only x64 and arm64 are supported'; exit 1;; esac +for tool in curl tar; do command -v "$tool" >/dev/null || { echo "Missing prerequisite: $tool"; exit 1; }; done +fetch() { curl --fail --silent --show-error --proto '=https' --tlsv1.2 --max-redirs 0 --connect-timeout 20 --max-time 300 "$1" -o "$2"; } +echo "Detected $platform / $arch; installing for $(id -un)" +if [ ! -x "$root/node/bin/node" ]; then + echo 'Downloading Node.js 24 LTS from https://nodejs.org' + fetch https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt "$stage/SHASUMS256.txt" + artifact=$(awk -v suffix="-$platform-$arch.tar.gz" '$2 ~ (suffix "$") {print $2}' "$stage/SHASUMS256.txt") + printf '%s' "$artifact" | grep -Eq '^node-v24\\.[0-9]+\\.[0-9]+-(linux|darwin)-(x64|arm64)\\.tar\\.gz$' || { echo 'Invalid official Node.js manifest'; exit 1; } + fetch "https://nodejs.org/dist/latest-v24.x/$artifact" "$stage/$artifact" + expected=$(awk -v file="$artifact" '$2 == file {print $1}' "$stage/SHASUMS256.txt") + if command -v sha256sum >/dev/null; then actual=$(sha256sum "$stage/$artifact" | awk '{print $1}'); else actual=$(shasum -a 256 "$stage/$artifact" | awk '{print $1}'); fi + [ "$actual" = "$expected" ] || { echo 'Node.js SHA-256 verification failed'; exit 1; } + mkdir "$stage/node" + tar -xzf "$stage/$artifact" -C "$stage/node" --strip-components=1 + "$stage/node/bin/node" --version + mv "$stage/node" "$root/node" + echo 'Node.js SHA-256 verified' +fi +PATH="$root/node/bin:$PATH" +export PATH +touch "$stage/user.npmrc" "$stage/global.npmrc" +mkdir "$stage/package" +printf '{"name":"termix-agent-runtime","version":"1.0.0","private":true}' > "$stage/package/package.json" +# Ignore host npmrc, scoped registry overrides and lifecycle download scripts. +npm_official() { env -i HOME="$HOME" PATH="$PATH" npm_config_userconfig="$stage/user.npmrc" npm_config_globalconfig="$stage/global.npmrc" npm_config_registry=https://registry.npmjs.org npm_config_strict_ssl=true npm_config_update_notifier=false npm_config_cache="$root/npm-cache" npm "$@"; } +cd "$stage/package" +echo "Resolving $package from https://registry.npmjs.org" +npm_official install --package-lock-only --ignore-scripts --no-audit --no-fund --save-exact "$package@latest" +node - <<'VERIFY' +const fs=require('fs'); +const lock=JSON.parse(fs.readFileSync('package-lock.json','utf8')); +(async () => { + for(const [name,p] of Object.entries(lock.packages)) { + if(!name) continue; + const u=new URL(p.resolved); + if(u.origin!=='https://registry.npmjs.org' || u.username || u.password) throw Error('Non-official dependency: '+name); + if(!/^sha512-/.test(p.integrity||'')) { + const packageName=p.name || name.split('node_modules/').pop(); + const response=await fetch('https://registry.npmjs.org/'+encodeURIComponent(packageName)+'/'+encodeURIComponent(p.version),{redirect:'error',signal:AbortSignal.timeout(30000)}); + if(!response.ok) throw Error('Unable to verify dependency: '+name); + const metadata=await response.json(); + if(metadata.dist?.tarball!==p.resolved || !/^sha512-/.test(metadata.dist?.integrity||'')) throw Error('Unverified dependency: '+name); + p.integrity=metadata.dist.integrity; + } + } + fs.writeFileSync('package-lock.json',JSON.stringify(lock,null,2)); + console.log('All package sources and integrity hashes verified'); +})().catch(error=>{console.error(error.message);process.exitCode=1;}); +VERIFY +npm_official ci --ignore-scripts --no-audit --no-fund +# Link verified optional platform binaries without running download-capable postinstall scripts. +case "$agent" in + claude) relative="@anthropic-ai/claude-code-$platform-$arch/claude";; + opencode) + variant="opencode-$platform-$arch" + if [ "$arch" = x64 ]; then variant="$variant-baseline"; fi + relative="$variant/bin/opencode";; + *) relative=;; +esac +if [ -n "$relative" ]; then + binary="$stage/package/node_modules/$relative" + [ -x "$binary" ] || { echo 'Official platform binary is unavailable'; exit 1; } + mkdir -p "$stage/package/node_modules/.bin" + # Relative links remain valid when the verified package directory is activated. + ln -sf "../$relative" "$stage/package/node_modules/.bin/$agent" +fi +"$stage/package/node_modules/.bin/$agent" --version +mkdir -p "$root/agents" +old="$root/agents/$agent.previous" +rm -rf "$old" +if [ -d "$root/agents/$agent" ]; then mv "$root/agents/$agent" "$old"; fi +if ! mv "$stage/package" "$root/agents/$agent"; then + if [ -d "$old" ]; then mv "$old" "$root/agents/$agent"; fi + exit 1 +fi +echo "Installed $agent from the official npm registry" +echo "Executable: $root/agents/$agent/node_modules/.bin/$agent" +`; +} + +export function registerInstallRoute( + router: Router, + ctx: PluginContext, + isRunning: (hostId: number) => boolean, +) { + const active = new Set(); + for (const operation of ["install", "forwarding"] as const) + router.post(`/agents/${operation}`, async (req, res) => { + const { hostId, agent } = req.body ?? {}; + if ( + !Number.isSafeInteger(hostId) || + hostId < 1 || + (operation === "install" && !AGENTS.includes(agent)) + ) { + res.status(400).json({ error: "Choose a host and supported agent" }); + return; + } + let dispose: (() => void) | undefined; + let channel: ClientChannel | undefined; + let timer: ReturnType | undefined; + let acquired = false; + const write = (data: object) => { + if (!res.destroyed) res.write(JSON.stringify(data) + "\n"); + }; + try { + if (!(await ctx.hosts.checkAccess(hostId, "connect")).hasAccess) + throw Error("SSH access is required"); + if (isRunning(hostId)) + throw Error( + "Stop active agents on this host before changing its runtime or SSH policy", + ); + if (active.has(hostId)) + throw Error( + "Another agent setup operation is already running on this host", + ); + active.add(hostId); + acquired = true; + const connection = await ctx.ssh.connect(hostId, { + purpose: `ai-agent-${operation}`, + profile: "session", + timeoutMs: 15000, + }); + dispose = connection.dispose; + if (res.destroyed) throw Error("Agent setup request disconnected"); + channel = await new Promise((resolve, reject) => + connection.client.exec( + "sh -lc " + + shellQuote( + operation === "install" + ? installScript(agent) + : forwardingScript(), + ), + (err, ch) => (err ? reject(err) : resolve(ch)), + ), + ); + if (res.destroyed) throw Error("Agent setup request disconnected"); + res.set({ + "Content-Type": "application/x-ndjson", + "Cache-Control": "no-store", + "X-Accel-Buffering": "no", + }); + res.flushHeaders(); + res.once("close", () => { + channel?.signal("TERM"); + dispose?.(); + }); + let bytes = 0; + const log = (data: Buffer) => { + bytes += data.length; + if (bytes <= 256 * 1024) write({ log: data.toString() }); + }; + channel.on("data", log); + channel.stderr.on("data", log); + const exitCode = await new Promise((resolve, reject) => { + timer = setTimeout( + () => reject(Error("Agent setup timed out after 10 minutes")), + 600000, + ); + channel!.once("error", reject); + channel!.once("close", (code: number) => resolve(code ?? -1)); + }); + if (operation === "forwarding" && exitCode === 0) { + const verification = await ctx.ssh.connect(hostId, { + purpose: "ai-agent-forwarding-verify", + profile: "session", + timeoutMs: 15000, + }); + try { + await new Promise((resolve, reject) => { + const deadline = setTimeout( + () => reject(Error("SSH forwarding verification timed out")), + 15000, + ); + verification.client.forwardIn("127.0.0.1", 0, (error) => { + clearTimeout(deadline); + return error + ? reject( + Error( + "SSH configuration was checked, but a new connection still refused remote forwarding. Check authorized_keys restrictions and the active SSH server configuration.", + ), + ) + : resolve(); + }); + }); + write({ + log: "Verified loopback remote forwarding on a new SSH connection\n", + }); + } finally { + verification.dispose(); + } + } + await ctx.audit.record({ + action: `agent_${operation}`, + resourceId: String(hostId), + success: exitCode === 0, + details: JSON.stringify({ + agent: operation === "install" ? agent : undefined, + package: + operation === "install" + ? PACKAGES[agent as AgentKind] + : undefined, + exitCode, + }), + }); + write({ done: true, success: exitCode === 0 }); + res.end(); + } catch (error) { + const message = + error instanceof Error ? error.message : "Agent setup failed"; + if (res.headersSent) { + write({ done: true, success: false, error: message }); + res.end(); + } else res.status(400).json({ error: message }); + } finally { + clearTimeout(timer); + channel?.signal("TERM"); + dispose?.(); + if (acquired) active.delete(hostId); + } + }); +} diff --git a/plugins/ai/src/backend/agents/remote-runner.ts b/plugins/ai/src/backend/agents/remote-runner.ts new file mode 100644 index 000000000..0339e92c7 --- /dev/null +++ b/plugins/ai/src/backend/agents/remote-runner.ts @@ -0,0 +1,192 @@ +/** Runs on the selected SSH host. Only a session-scoped proxy token reaches it. */ +export const REMOTE_RUNNER = String.raw` +const fs = require('node:fs'), path = require('node:path'), os = require('node:os'); +const {spawn} = require('node:child_process'), readline = require('node:readline'); +let config, child, nativeId, turnId, serverUrl, serverAuth, ready = false, running = false, rpcId = 0; +const pending = new Map(), approvals = new Map(); +const emit = (kind, text, extra = {}) => process.stdout.write(JSON.stringify({kind,text,...extra})+'\n'); +const send = value => child.stdin.write(JSON.stringify(value)+'\n'); +const request = (method, params) => new Promise((resolve,reject) => { + const id=++rpcId, timer=setTimeout(()=>{pending.delete(id);reject(Error('Agent request timed out: '+method));},60000); + pending.set(id,{resolve,reject,timer}); send({id,method,params}); +}); +function fail(error) { emit('error',error.message || String(error)); if(!ready) { if(child?.pid) {try{process.kill(-child.pid,'SIGTERM');}catch{}} process.stdin.destroy(); } } +function done(initial=false) { if(!initial && !running) return; running=false; emit('status','ready'); } +async function launch(args, env) { + const managed=path.join(os.homedir(),'.local','share','termix-agent-runtime','agents',config.agent,'node_modules','.bin',config.agent); + const executable=(!config.executable || config.executable===config.agent) && fs.existsSync(managed)?managed:(config.executable || config.agent); + child=spawn(executable,args,{cwd:config.cwd,env:{...process.env,...env},stdio:['pipe','pipe','pipe'],detached:true}); + child.on('error',fail); + child.on('exit',(code)=>{emit('status','stopped');process.exitCode=code||0;process.stdin.destroy();}); + child.stderr.on('data',d=>emit('tool',d.toString().slice(0,8000))); + readline.createInterface({input:child.stdout}).on('line',line=>{ + try { receive(JSON.parse(line)); } catch { /* CLI startup chatter is not protocol. */ } + }); + await new Promise((resolve,reject)=>{child.once('spawn',resolve);child.once('error',reject);}); +} +function receive(m) { + if(config.agent==='pi') { + if(m.type==='response' && pending.has(m.id)) {const p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);m.success?p.resolve(m.data):p.reject(Error(m.error));return;} + if(m.type==='assistant_message_event' || m.type==='message_update') { + const e=m.assistantMessageEvent || m.event; + if(e?.type==='text_delta') emit('text',e.delta); + } + if(m.type==='tool_execution_start') emit('tool',m.toolName+' '+JSON.stringify(m.args)); + if(m.type==='tool_execution_end') emit('tool',JSON.stringify(m.result)); + if(m.type==='agent_end') done(); + if(m.type==='response' && m.success===false) {fail(Error(m.error || 'Pi request failed'));done();} + if(m.type==='extension_ui_request' && ['select','confirm','input','editor'].includes(m.method)) { + approvals.set(m.id,m); emit('permission',m.title || m.message || m.method,{requestId:m.id,choices:m.options}); + } + return; + } + if(config.agent==='claude') { + if(m.type==='system' && m.session_id) {nativeId=m.session_id;emit('native','',{nativeId});} + if(m.type==='stream_event' && m.event?.type==='content_block_delta' && m.event.delta?.type==='text_delta') emit('text',m.event.delta.text); + if(m.type==='stream_event' && m.event?.type==='content_block_start' && m.event.content_block?.type==='tool_use') emit('tool',m.event.content_block.name); + if(m.type==='control_request') { + if(m.request?.subtype==='can_use_tool') { + approvals.set(m.request_id,m);emit('permission',m.request.tool_name+' '+JSON.stringify(m.request.input),{requestId:m.request_id}); + } else send({type:'control_response',response:{subtype:'error',request_id:m.request_id,error:'Unsupported control request'}}); + } + if(m.type==='result') {if(m.is_error) fail(Error(m.result || m.errors?.join('\n') || 'Claude turn failed'));done();} + return; + } + if(config.agent==='codex') { + if(m.id!==undefined && !m.method && pending.has(m.id)) { + const p=pending.get(m.id);pending.delete(m.id);clearTimeout(p.timer);m.error?p.reject(Error(m.error.message)):p.resolve(m.result);return; + } + if(m.id!==undefined && m.method) { + approvals.set(String(m.id),m);emit('permission',m.method+' '+JSON.stringify(m.params),{requestId:String(m.id)});return; + } + if(m.method==='item/agentMessage/delta') emit('text',m.params.delta); + if(m.method==='item/started' && m.params.item?.type!=='agentMessage') emit('tool',JSON.stringify(m.params.item)); + if(m.method==='turn/started') turnId=m.params.turn.id; + if(m.method==='turn/completed') {if(m.params.turn.status==='failed') fail(Error(m.params.turn.error?.message || 'Codex turn failed'));done();} + return; + } +} +async function oc(method, route, body) { + const r=await fetch(serverUrl+route,{method,headers:{'Content-Type':'application/json','x-opencode-directory':config.cwd,authorization:serverAuth},body:body===undefined?undefined:JSON.stringify(body),signal:AbortSignal.timeout(5000)}); + if(!r.ok) throw Error('OpenCode '+route+' returned '+r.status); + return r.status===204?null:r.json(); +} +async function ocEvents() { + const response=await fetch(serverUrl+'/event',{headers:{'x-opencode-directory':config.cwd,authorization:serverAuth}}); + if(!response.ok || !response.body) throw Error('OpenCode event stream unavailable'); + let buffer='';const decoder=new TextDecoder(),partTypes=new Map(); + for await(const chunk of response.body) { + buffer+=decoder.decode(chunk,{stream:true}); + let end; + while((end=buffer.indexOf('\n\n'))>=0) { + const frame=buffer.slice(0,end);buffer=buffer.slice(end+2); + const data=frame.split('\n').filter(x=>x.startsWith('data:')).map(x=>x.slice(5).trim()).join('\n'); + if(!data) continue; + let e;try{e=JSON.parse(data);}catch{continue;}const p=e.properties||{}; + const sessionId=p.sessionID || p.part?.sessionID || p.info?.sessionID; + if(sessionId!==nativeId) continue; + if(e.type==='message.part.updated') partTypes.set(p.part.id,p.part.type); + if(e.type==='message.part.delta' && p.field==='text' && partTypes.get(p.partID)==='text') emit('text',p.delta); + if(e.type==='message.part.updated' && p.part?.type==='tool') emit('tool',JSON.stringify(p.part)); + if(e.type==='session.idle' || (e.type==='session.status' && p.status?.type==='idle')) done(); + if(e.type==='session.error') {fail(Error(JSON.stringify(p.error)));done();} + if(e.type==='permission.asked' || e.type==='question.asked') {approvals.set(p.id,{...p,event:e.type});emit('permission',JSON.stringify(p),{requestId:p.id});} + } + } + throw Error('OpenCode event stream closed'); +} +async function start(c) { + config=c; + emit('tool','Starting '+c.agent+' in '+c.cwd); + if(!fs.statSync(c.cwd).isDirectory()) throw Error('Working directory does not exist'); + const dir=path.join(os.homedir(),'.local','state','termix-agents',c.id); + fs.mkdirSync(dir,{recursive:true,mode:0o700}); + const base=c.proxyUrl, token=c.token; + if(c.agent==='pi') { + const agentDir=path.join(dir,'config'); + fs.mkdirSync(agentDir,{recursive:true,mode:0o700}); + fs.writeFileSync(path.join(agentDir,'models.json'),JSON.stringify({providers:{termix:{baseUrl:base+'/v1',api:c.providerType==='anthropic'?'anthropic-messages':'openai-completions',apiKey:token,models:[{id:c.model,name:c.model,reasoning:false,input:['text','image'],contextWindow:131072,maxTokens:8192,cost:{input:0,output:0,cacheRead:0,cacheWrite:0}}]}}}),{mode:0o600}); + await launch(['--mode','rpc','--provider','termix','--model',c.model,'--session',path.join(dir,'session.jsonl')],{PI_CODING_AGENT_DIR:agentDir}); + const state=await new Promise((resolve,reject)=>{const id=String(++rpcId),timer=setTimeout(()=>{pending.delete(id);reject(Error('Pi startup timed out'));},60000);pending.set(id,{resolve,reject,timer});send({id,type:'get_state'});}); + nativeId=state.sessionId;emit('native','',{nativeId}); + emit('tool','Loaded '+state.messageCount+' messages from the Pi session'); + } + if(c.agent==='claude') { + const args=['--print','--verbose','--input-format','stream-json','--output-format','stream-json','--include-partial-messages','--permission-prompt-tool','stdio','--model',c.model]; + if(c.nativeId) args.push('--resume',c.nativeId); + await launch(args,{ANTHROPIC_BASE_URL:base,ANTHROPIC_API_KEY:token,ANTHROPIC_AUTH_TOKEN:'',CLAUDE_CONFIG_DIR:dir,DISABLE_UPDATES:'1',DISABLE_AUTOUPDATER:'1'}); + } + if(c.agent==='codex') { + await launch(['app-server','-c','model_provider="termix"','-c','model_providers.termix.name="Termix"','-c','model_providers.termix.base_url='+JSON.stringify(base+'/v1'),'-c','model_providers.termix.env_key="TERMIX_AGENT_TOKEN"','-c','model_providers.termix.wire_api="responses"'],{CODEX_HOME:dir,TERMIX_AGENT_TOKEN:token}); + await request('initialize',{clientInfo:{name:'termix',version:'1.0.0'}});send({method:'initialized'}); + const result=await request(c.nativeId?'thread/resume':'thread/start',{...(c.nativeId?{threadId:c.nativeId}:{}),cwd:c.cwd,model:c.model,approvalPolicy:'untrusted',sandbox:'workspace-write'}); + nativeId=result.thread.id;emit('native','',{nativeId}); + } + if(c.agent==='opencode') { + const net=require('node:net');const port=await new Promise(resolve=>{const s=net.createServer();s.listen(0,'127.0.0.1',()=>{const p=s.address().port;s.close(()=>resolve(p));});}); + serverUrl='http://127.0.0.1:'+port; + // Loopback is shared with every local account; the password keeps them off the agent. + const password=require('node:crypto').randomBytes(32).toString('hex');serverAuth='Basic '+Buffer.from('opencode:'+password).toString('base64'); + const settings={autoupdate:false,model:'termix/'+c.model,provider:{termix:{npm:c.providerType==='anthropic'?'@ai-sdk/anthropic':'@ai-sdk/openai-compatible',name:'Termix',options:{baseURL:base+'/v1',apiKey:token},models:{[c.model]:{name:c.model}}}},permission:'ask'}; + emit('tool','Starting OpenCode server'); + await launch(['serve','--hostname','127.0.0.1','--port',String(port)],{OPENCODE_CONFIG_CONTENT:JSON.stringify(settings),OPENCODE_SERVER_USERNAME:'opencode',OPENCODE_SERVER_PASSWORD:password,XDG_DATA_HOME:dir,XDG_CONFIG_HOME:dir}); + emit('tool','Waiting for OpenCode health'); + let available=false;for(let i=0;i<100;i++){try{await oc('GET','/global/health');available=true;break;}catch{await new Promise(r=>setTimeout(r,200));}} + if(!available) throw Error('OpenCode startup timed out'); + emit('tool','Opening OpenCode session'); + nativeId=c.nativeId || (await oc('POST','/session',{})).id;emit('native','',{nativeId}); + void ocEvents().catch(fail); + } + ready=true;done(true); +} +async function handle(m) { + if(m.type==='start') return start(m.config); + if(m.type==='stop') {if(child?.pid) {try{process.kill(-child.pid,'SIGTERM');}catch{}};setTimeout(()=>process.exit(0),1500).unref();return;} + if(!ready) throw Error('Agent is not ready'); + if(m.type==='prompt') { + running=true; + emit('status','running'); + try { + const attachments=m.attachments||[],images=[]; + let text=m.text; + for(const a of attachments) { + if(a.mime.startsWith('image/')) { + const fd=fs.openSync(a.path,fs.constants.O_RDONLY|fs.constants.O_NOFOLLOW); + try { + const stat=fs.fstatSync(fd);if(!stat.isFile() || stat.size>1024*1024) throw Error('Image exceeds 1 MiB or is no longer a regular file'); + const data=Buffer.alloc(stat.size),size=fs.readSync(fd,data,0,data.length,0); + images.push({type:'image',data:data.subarray(0,size).toString('base64'),mimeType:a.mime}); + } finally {fs.closeSync(fd);} + } else text+='\nAttached file on this host: '+JSON.stringify(a.path); + } + if(config.agent==='pi') send({type:'prompt',message:text,images}); + if(config.agent==='claude') send({type:'user',message:{role:'user',content:[{type:'text',text:text||'Please inspect the attached images.'},...images.map(i=>({type:'image',source:{type:'base64',media_type:i.mimeType,data:i.data}}))]},parent_tool_use_id:null,session_id:nativeId||''}); + if(config.agent==='codex') await request('turn/start',{threadId:nativeId,input:[{type:'text',text},...images.map(i=>({type:'image',url:'data:'+i.mimeType+';base64,'+i.data}))]}); + if(config.agent==='opencode') await oc('POST','/session/'+nativeId+'/prompt_async',{model:{providerID:'termix',modelID:config.model},parts:[{type:'text',text},...images.map(i=>({type:'file',mime:i.mimeType,url:'data:'+i.mimeType+';base64,'+i.data}))]}); + } catch(error) {fail(error);done();} + } + if(m.type==='cancel') { + if(config.agent==='pi') send({type:'abort'}); + if(config.agent==='claude') send({type:'control_request',request_id:String(++rpcId),request:{subtype:'interrupt'}}); + if(config.agent==='codex' && turnId) await request('turn/interrupt',{threadId:nativeId,turnId}); + if(config.agent==='opencode') await oc('POST','/session/'+nativeId+'/abort',{}); + } + if(m.type==='answer') { + const a=approvals.get(m.requestId);if(!a) throw Error('Approval request expired'); + if(config.agent==='pi') send({type:'extension_ui_response',id:m.requestId,...(a.method==='confirm'?{confirmed:m.allow}:{value:m.value,cancelled:!m.allow})}); + if(config.agent==='claude') send({type:'control_response',response:{subtype:'success',request_id:m.requestId,response:m.allow?{behavior:'allow',updatedInput:a.request.input}:{behavior:'deny',message:'Denied in Termix'}}}); + if(config.agent==='codex') { + let result={decision:m.allow?'accept':'decline'}; + if(a.method==='item/tool/requestUserInput') {if(!m.value) throw Error('Provide JSON answers for this question');result={answers:JSON.parse(m.value)};} + else if(!a.method.endsWith('/requestApproval')) throw Error('Unsupported Codex request; stop the session'); + send({id:a.id,result}); + } + if(config.agent==='opencode') { + if(a.event==='question.asked') await oc('POST','/question/'+m.requestId+(m.allow?'/reply':'/reject'),m.allow?{answers:JSON.parse(m.value||'[]')}:{}); + else await oc('POST','/permission/'+m.requestId+'/reply',{reply:m.allow?'once':'reject'}); + } + approvals.delete(m.requestId); + } +} +readline.createInterface({input:process.stdin}).on('line',line=>{try{Promise.resolve(handle(JSON.parse(line))).catch(fail);}catch(e){fail(e);}}).on('close',()=>{if(child?.pid) {try{process.kill(-child.pid,'SIGTERM');}catch{}};}); +`; diff --git a/plugins/ai/src/backend/agents/routes.ts b/plugins/ai/src/backend/agents/routes.ts new file mode 100644 index 000000000..7e78bb446 --- /dev/null +++ b/plugins/ai/src/backend/agents/routes.ts @@ -0,0 +1,709 @@ +import { randomBytes, randomUUID } from "node:crypto"; +import { createServer } from "node:http"; +import { once } from "node:events"; +import type { Client, ClientChannel } from "ssh2"; +import type { Request, Response, Router } from "express"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { + createAiGate, + readPrivateAllowlist, + resolveAiAccess, +} from "../gating.js"; +import { createProviderFetch } from "../providers/http.js"; +import type { AiRepository } from "../repository.js"; +import { + AGENTS, + clipText, + compatible, + shellQuote, + validStart, + type AgentEvent, + type AgentSession, + type QueuedPrompt, + type AgentAttachment, +} from "./types.js"; +import { registerInstallRoute } from "./install.js"; +import { workspaceOperation } from "./workspace.js"; +import { promptInput, updateSession, updateQueue } from "./session-state.js"; +import { REMOTE_RUNNER } from "./remote-runner.js"; +import { EVENT_HISTORY, type AgentStore } from "./store.js"; + +interface Live { + session: AgentSession; + channel?: ClientChannel; + dispose: () => void; + subscribers: Set; + approvals: Set; + /** Events not yet written; flushed together at most once a second. */ + pending: AgentEvent[]; + saved: Promise; + stopping?: Promise; + draining?: boolean; + flush?: ReturnType; +} +const MAX_BODY = 16 * 1024 * 1024; +const PROVIDER_PATHS = new Set([ + "/chat/completions", + "/responses", + "/messages", + "/messages/count_tokens", + "/models", +]); +export function providerPath(raw: string): string | null { + const url = new URL(raw, "http://localhost"); + const path = url.pathname.replace(/^\/v1(?=\/)/, ""); + return PROVIDER_PATHS.has(path) ? path : null; +} + +export function registerAgentRoutes( + router: Router, + repository: AiRepository, + ctx: PluginContext, + store: AgentStore, +): void { + const live = new Map(); + const actor = () => ctx.currentActor() as string; + /** Writes are queued per session, so rows land in the order they happened. */ + const write = (entry: Live, full: boolean) => { + entry.saved = entry.saved + .catch(() => undefined) + .then(async () => { + const session = { ...entry.session }; + await store.append(session, entry.pending.splice(0)); + if (full) await store.save(session); + }); + return entry.saved; + }; + const save = (entry: Live) => write(entry, true); + const logFailure = (error: unknown) => + ctx.log.error( + "Agent session could not be saved", + error instanceof Error ? error : new Error(String(error)), + ); + const event = (entry: Live, input: Omit) => { + const s = entry.session; + const item = { + ...input, + seq: (s.events.at(-1)?.seq ?? 0) + 1, + text: clipText(input.text.slice(0, 32000)), + }; + s.events.push(item); + if (s.events.length > EVENT_HISTORY) s.events.shift(); + entry.pending.push(item); + s.updatedAt = new Date().toISOString(); + for (const res of entry.subscribers) { + if (!res.write(`id: ${item.seq}\ndata: ${JSON.stringify(item)}\n\n`)) + res.end(); + } + if (!entry.flush) + entry.flush = setTimeout(() => { + entry.flush = undefined; + write(entry, false).catch(logFailure); + }, 1000); + }; + async function allowed(s: AgentSession) { + return ( + (await resolveAiAccess(ctx.settings, s.userId)).enabled && + (await ctx.rbac.hasFor(s.userId, "agents")) && + (await ctx.hosts.checkAccess(s.hostId, "connect")).hasAccess + ); + } + async function find(id: string): Promise { + if (!/^[a-f0-9-]{36}$/.test(id)) throw Error("Invalid session ID"); + const s = live.get(id)?.session ?? (await store.get(actor(), id)); + if (!s || s.userId !== actor() || !(await allowed(s))) + throw Error("Agent session is not accessible"); + return live.has(id) ? s : { ...s, status: "stopped" }; + } + function stop(entry: Live): Promise { + if (entry.stopping) return entry.stopping; + if (live.get(entry.session.id) !== entry) return Promise.resolve(); + entry.stopping = (async () => { + if (entry.channel && !entry.channel.destroyed) { + await new Promise((resolve) => { + const timer = setTimeout(resolve, 2000); + entry.channel!.once("close", () => { + clearTimeout(timer); + resolve(); + }); + entry.channel!.write(JSON.stringify({ type: "stop" }) + "\n"); + }); + } + if (live.get(entry.session.id) === entry) live.delete(entry.session.id); + entry.dispose(); + entry.session.status = "stopped"; + event(entry, { kind: "status", text: "stopped" }); + // The save below writes everything pending. + clearTimeout(entry.flush); + for (const res of entry.subscribers) res.end(); + await save(entry); + })(); + return entry.stopping; + } + ctx.disposables.add(() => { + for (const entry of live.values()) stop(entry); + }); + ctx.events.on("user.deleted", (payload) => { + const id = (payload as { userId?: string }).userId; + for (const entry of live.values()) + if (entry.session.userId === id) stop(entry); + }); + + async function persist(s: AgentSession) { + const entry = live.get(s.id); + s.updatedAt = new Date().toISOString(); + if (entry) { + event(entry, { kind: "state", text: "" }); + await save(entry); + } else await store.save(s); + } + async function dispatch(entry: Live, prompt: QueuedPrompt) { + const s = entry.session; + if (s.status !== "ready" || entry.stopping || !entry.channel) + throw Error("Agent is not ready"); + s.status = "running"; + s.queue = s.queue?.filter((p) => p.id !== prompt.id); + event(entry, { + kind: "user", + text: + prompt.text + + (prompt.attachmentIds.length + ? "\n" + + prompt.attachmentIds + .map((id) => s.attachments!.find((a) => a.id === id)!.name) + .join("\n") + : ""), + }); + event(entry, { kind: "status", text: "running" }); + event(entry, { kind: "state", text: "" }); + await save(entry); + entry.channel.write( + JSON.stringify({ + type: "prompt", + text: prompt.text, + attachments: prompt.attachmentIds.map((id) => + s.attachments!.find((a) => a.id === id), + ), + }) + "\n", + ); + } + function drain(entry: Live) { + if (entry.draining || entry.stopping) return; + entry.draining = true; + void ctx + .asUser(entry.session.userId, async () => { + const s = entry.session; + if (!(await allowed(s))) return stop(entry); + if ( + s.status === "ready" && + !s.queuePaused && + !s.archived && + s.queue?.length && + !entry.stopping + ) + await dispatch(entry, s.queue[0]); + }) + .catch((error) => { + entry.session.queuePaused = true; + event(entry, { kind: "error", text: String(error.message || error) }); + }) + .finally(() => { + entry.draining = false; + }); + } + + async function start(s: AgentSession): Promise { + const existing = live.get(s.id); + if (existing?.stopping) await existing.stopping; + else if (existing) return existing; + if ( + [...live.values()].filter((e) => e.session.userId === s.userId).length >= + 4 + ) + throw Error( + "Stop an agent before opening another (maximum 4 active sessions)", + ); + const provider = await repository.findProviderWithSecret( + s.providerId, + s.userId, + ); + if (!provider?.enabled || !compatible(s.agent, provider.providerType)) + throw Error("Provider protocol is not compatible with this agent"); + const connection = await ctx.ssh.connect(s.hostId, { + purpose: "ai-agent", + profile: "session", + timeoutMs: 15000, + }); + const entry: Live = { + // History is loaded so a resumed session keeps numbering its events. + session: { ...s, status: "starting", events: await store.events(s.id) }, + dispose: connection.dispose, + subscribers: new Set(), + approvals: new Set(), + pending: [], + saved: Promise.resolve(), + }; + live.set(s.id, entry); + const token = randomBytes(32).toString("hex"); + const aborters = new Set(); + let port = 0; + const proxy = createServer((req, res) => { + const controller = new AbortController(); + aborters.add(controller); + res.on("close", () => { + controller.abort(); + aborters.delete(controller); + }); + void ctx + .asUser(s.userId, async () => { + const path = providerPath(req.url ?? "/"); + const auth = + req.headers.authorization === `Bearer ${token}` || + req.headers["x-api-key"] === token; + if (!auth || !path || !["GET", "POST"].includes(req.method ?? "")) { + res.writeHead(403).end(); + return; + } + if (!(await allowed(s))) { + res.writeHead(403).end(); + stop(entry); + return; + } + const current = await repository.findProviderWithSecret( + s.providerId, + s.userId, + ); + if (!current?.enabled) { + res.writeHead(403).end(); + return; + } + let body = ""; + for await (const chunk of req) { + body += chunk.toString(); + if (Buffer.byteLength(body) > MAX_BODY) { + res.writeHead(413).end(); + return; + } + } + if (body) { + const parsed = JSON.parse(body); + if (parsed.model && parsed.model !== s.model) { + res.writeHead(403).end(); + return; + } + } + const base = + current.baseUrl || + (current.providerType === "anthropic" + ? "https://api.anthropic.com/v1" + : "https://api.openai.com/v1"); + const headers: Record = { + "content-type": "application/json", + }; + if (current.providerType === "anthropic") { + headers["x-api-key"] = current.apiKey ?? ""; + headers["anthropic-version"] = "2023-06-01"; + } else if (current.apiKey) + headers.authorization = `Bearer ${current.apiKey}`; + const response = await createProviderFetch( + ctx.fetch, + await readPrivateAllowlist(ctx.settings), + )(base.replace(/\/$/, "") + path, { + method: req.method, + headers, + body: body || undefined, + signal: controller.signal, + }); + res.writeHead(response.status, { + "Content-Type": + response.headers.get("content-type") ?? "application/json", + "Cache-Control": "no-store", + }); + if (response.body) + for await (const chunk of response.body as unknown as AsyncIterable) { + if (!res.write(chunk)) + await once(res, "drain", { signal: controller.signal }); + } + res.end(); + }) + .catch(() => { + if (!res.headersSent) res.writeHead(502); + res.end('{"error":"Termix provider request failed"}'); + }); + }); + proxy.maxHeadersCount = 30; + connection.client.on("tcp connection", (info, accept, reject) => { + if (info.destPort !== port || info.destIP !== "127.0.0.1") { + reject(); + return; + } + proxy.emit("connection", accept()); + }); + const startup = setTimeout(() => { + if (entry.session.status === "starting") { + event(entry, { kind: "error", text: "Agent startup timed out" }); + void stop(entry); + } + }, 60000); + startup.unref(); + const expiry = setTimeout(() => stop(entry), 8 * 60 * 60 * 1000); + expiry.unref(); + const accessTimer = setInterval(() => { + void ctx + .asUser(s.userId, async () => { + if (!(await allowed(s))) stop(entry); + }) + .catch(() => stop(entry)); + }, 30000); + accessTimer.unref(); + entry.dispose = () => { + clearTimeout(startup); + clearTimeout(expiry); + clearInterval(accessTimer); + for (const c of aborters) c.abort(); + proxy.close(); + connection.dispose(); + }; + try { + port = await new Promise((resolve, reject) => + connection.client.forwardIn("127.0.0.1", 0, (err, p) => + err + ? reject( + Error( + "SSH remote forwarding was refused. Allow loopback remote forwarding for this SSH account (AllowTcpForwarding remote and PermitListen 127.0.0.1:*). Installing an agent does not change SSH policy.", + ), + ) + : resolve(p), + ), + ); + const source = Buffer.from(REMOTE_RUNNER).toString("base64"); + const command = + 'export PATH="$HOME/.local/share/termix-agent-runtime/node/bin:$PATH"; command -v node >/dev/null || { echo "Node.js is missing. Use Install runtime first." >&2; exit 1; }; exec node -e ' + + shellQuote(`eval(Buffer.from('${source}','base64').toString())`); + const channel = await new Promise((resolve, reject) => + connection.client.exec("sh -lc " + shellQuote(command), (err, ch) => + err ? reject(err) : resolve(ch), + ), + ); + entry.channel = channel; + let buffer = ""; + channel.on("data", (chunk: Buffer) => { + buffer += chunk.toString(); + if (buffer.length > MAX_BODY) { + event(entry, { + kind: "error", + text: "Agent output exceeded the protocol limit", + }); + stop(entry); + return; + } + let n: number; + while ((n = buffer.indexOf("\n")) >= 0) { + const line = buffer.slice(0, n); + buffer = buffer.slice(n + 1); + try { + const m = JSON.parse(line); + if (m.kind === "native" && typeof m.nativeId === "string") { + entry.session.nativeId = m.nativeId; + save(entry).catch(logFailure); + continue; + } + if ( + !["text", "tool", "permission", "status", "error"].includes( + m.kind, + ) + ) + continue; + if ( + m.kind === "status" && + ["ready", "running", "stopped"].includes(m.text) + ) { + if (entry.session.status === m.text) continue; + entry.session.status = m.text; + } + if (m.kind === "error") entry.session.queuePaused = true; + if (m.kind === "permission" && typeof m.requestId === "string") + entry.approvals.add(m.requestId); + event(entry, { + kind: m.kind, + text: String(m.text ?? "").replaceAll(token, "[session token]"), + requestId: m.requestId, + choices: m.choices, + }); + if (m.kind === "status" && m.text === "ready") drain(entry); + } catch { + /* Ignore non-protocol startup chatter. */ + } + } + }); + channel.stderr.on("data", (d: Buffer) => + event(entry, { + kind: "error", + text: d.toString().replaceAll(token, "[session token]"), + }), + ); + channel.on("close", () => stop(entry)); + channel.write( + JSON.stringify({ + type: "start", + config: { + ...s, + userId: undefined, + events: undefined, + proxyUrl: `http://127.0.0.1:${port}`, + token, + providerType: provider.providerType, + }, + }) + "\n", + ); + await save(entry); + await ctx.audit.record({ + action: "agent_start", + resourceId: String(s.hostId), + success: true, + details: JSON.stringify({ + agent: s.agent, + sessionId: s.id, + providerId: s.providerId, + }), + }); + return entry; + } catch (error) { + stop(entry); + throw error; + } + } + + const gate = createAiGate(ctx.settings, () => ctx.currentActor()); + router.use("/agents", ctx.rbac.require("agents") as never, gate as never); + const mutations = new Map>(); + const route = + (fn: (req: Request, res: Response) => Promise) => + (req: Request, res: Response) => { + const id = req.method !== "GET" ? String(req.params.id ?? "") : ""; + const previous = id ? mutations.get(id) : undefined; + const operation = (previous ?? Promise.resolve()) + .catch(() => undefined) + .then(() => fn(req, res)); + if (id) mutations.set(id, operation); + void operation + .catch((error) => { + if (!res.headersSent) + res.status(400).json({ + error: + error instanceof Error + ? error.message + : "Agent operation failed", + }); + }) + .finally(() => { + if (id && mutations.get(id) === operation) mutations.delete(id); + }); + }; + registerInstallRoute(router, ctx, (hostId) => + [...live.values()].some((entry) => entry.session.hostId === hostId), + ); + router.get( + "/agents", + route(async (_req, res) => { + const sessions: AgentSession[] = []; + for (const stored of await store.list(actor())) { + if (await allowed(stored)) + sessions.push( + live.get(stored.id)?.session ?? { ...stored, status: "stopped" }, + ); + } + res.json({ + agents: AGENTS, + sessions: sessions + .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) + .map((s) => ({ ...s, events: undefined, userId: undefined })), + }); + }), + ); + router.post( + "/agents", + route(async (req, res) => { + if (!validStart(req.body ?? {})) + throw Error( + "Choose an agent, host, provider, model and absolute working directory", + ); + const s: AgentSession = { + id: randomUUID(), + userId: actor(), + hostId: req.body.hostId, + providerId: req.body.providerId, + agent: req.body.agent, + model: req.body.model, + cwd: req.body.cwd, + executable: req.body.executable || req.body.agent, + status: "starting", + events: [], + updatedAt: new Date().toISOString(), + }; + if (!(await allowed(s))) throw Error("SSH access is required"); + await start(s); + res.status(201).json({ id: s.id }); + }), + ); + router.post( + "/agents/:id/workspace", + route(async (req, res) => { + const s = await find(String(req.params.id)); + const body = req.body ?? {}; + if ( + ["upload", "reference"].includes(body.operation) && + (s.attachments?.length ?? 0) >= 100 + ) + throw Error("Maximum 100 attachments per session"); + const result = await workspaceOperation(ctx, s, body); + if (["upload", "reference"].includes(body.operation)) { + s.attachments = [ + ...(s.attachments ?? []), + result as unknown as AgentAttachment, + ]; + await persist(s); + } + if (["upload", "worktree"].includes(body.operation)) + await ctx.audit.record({ + action: "agent_workspace_" + body.operation, + resourceId: String(s.hostId), + success: true, + details: JSON.stringify({ sessionId: s.id }), + }); + res.json(result); + }), + ); + router.patch( + "/agents/:id", + route(async (req, res) => { + const s = await find(String(req.params.id)); + updateSession(s, req.body ?? {}); + if (s.archived) { + s.queuePaused = true; + const entry = live.get(s.id); + if (entry) await stop(entry); + } + await persist(s); + res.json(s); + }), + ); + router.post( + "/agents/:id/queue", + route(async (req, res) => { + const s = await find(String(req.params.id)); + updateQueue(s, req.body ?? {}); + await persist(s); + const entry = live.get(s.id); + if (entry) drain(entry); + res.json({ queue: s.queue ?? [], queuePaused: !!s.queuePaused }); + }), + ); + router.get( + "/agents/:id", + route(async (req, res) => { + const s = await find(String(req.params.id)); + res.json(live.has(s.id) ? s : { ...s, events: await store.events(s.id) }); + }), + ); + router.get( + "/agents/:id/events", + route(async (req, res) => { + const s = await find(String(req.params.id)); + res.set({ + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + }); + res.flushHeaders(); + const after = Number( + req.headers["last-event-id"] || req.query.after || 0, + ); + const entry = live.get(s.id); + const history = entry + ? s.events.filter((e) => e.seq > after) + : await store.events(s.id, after); + for (const e of history) + res.write(`id: ${e.seq}\ndata: ${JSON.stringify(e)}\n\n`); + if (!entry) { + res.write( + `event: snapshot\ndata: ${JSON.stringify({ status: "stopped" })}\n\n`, + ); + res.end(); + return; + } + entry.subscribers.add(res); + const heartbeat = setInterval(() => res.write(": heartbeat\n\n"), 15000); + req.on("close", () => { + clearInterval(heartbeat); + entry.subscribers.delete(res); + }); + }), + ); + router.post( + "/agents/:id/resume", + route(async (req, res) => { + const s = await find(String(req.params.id)); + if (s.archived) throw Error("Restore the archived session first"); + await start(s); + res.json({ id: s.id }); + }), + ); + router.post( + "/agents/:id/input", + route(async (req, res) => { + const s = await find(String(req.params.id)); + const entry = live.get(s.id); + if (!entry?.channel) throw Error("Resume the agent first"); + const { type, text, requestId, allow, value } = req.body ?? {}; + if (type === "prompt") { + if (s.archived) throw Error("Restore the archived session first"); + await dispatch(entry, promptInput(s, req.body)); + res.json({ ok: true }); + return; + } else if (type === "answer") { + if ( + !entry.approvals.has(requestId) || + typeof allow !== "boolean" || + (value !== undefined && + (typeof value !== "string" || value.length > 64000)) + ) + throw Error("Invalid approval response"); + entry.approvals.delete(requestId); + await ctx.audit.record({ + action: "agent_approval", + resourceId: String(s.hostId), + success: true, + details: JSON.stringify({ sessionId: s.id, requestId, allow }), + }); + } else if (type === "cancel") { + s.queuePaused = true; + await persist(s); + } else throw Error("Unknown input type"); + entry.channel.write( + JSON.stringify({ type, text, requestId, allow, value }) + "\n", + ); + res.json({ ok: true }); + }), + ); + router.post( + "/agents/:id/stop", + route(async (req, res) => { + const s = await find(String(req.params.id)); + const entry = live.get(s.id); + if (entry) await stop(entry); + res.json({ ok: true }); + }), + ); + router.delete( + "/agents/:id", + route(async (req, res) => { + const s = await find(String(req.params.id)); + const entry = live.get(s.id); + if (entry) await stop(entry); + await entry?.saved.catch(() => undefined); + await store.delete(s.id); + res.json({ ok: true }); + }), + ); +} diff --git a/plugins/ai/src/backend/agents/session-state.ts b/plugins/ai/src/backend/agents/session-state.ts new file mode 100644 index 000000000..179692374 --- /dev/null +++ b/plugins/ai/src/backend/agents/session-state.ts @@ -0,0 +1,64 @@ +import { randomUUID } from "node:crypto"; +import { fitsText, type AgentSession, type QueuedPrompt } from "./types.js"; + +export function promptInput( + s: AgentSession, + body: Record, +): QueuedPrompt { + const text = body.text; + const attachmentIds = body.attachmentIds ?? []; + if ( + typeof text !== "string" || + !fitsText(text) || + !Array.isArray(attachmentIds) || + attachmentIds.length > 4 || + attachmentIds.some( + (id) => + typeof id !== "string" || !s.attachments?.some((a) => a.id === id), + ) || + (!text.trim() && !attachmentIds.length) + ) + throw Error("Provide a message and up to four session attachments"); + return { id: randomUUID(), text, attachmentIds: [...new Set(attachmentIds)] }; +} +export function updateSession( + s: AgentSession, + body: Record, +): void { + if (body.title !== undefined) { + if (typeof body.title !== "string" || body.title.length > 120) + throw Error("Session title must be at most 120 characters"); + } + if (body.archived !== undefined && typeof body.archived !== "boolean") + throw Error("Invalid archive state"); + if ( + body.draft !== undefined && + (typeof body.draft !== "string" || !fitsText(body.draft)) + ) + throw Error("Draft is too long"); + if (body.title !== undefined) s.title = (body.title as string).trim(); + if (body.archived !== undefined) s.archived = body.archived as boolean; + if (body.draft !== undefined) s.draft = body.draft as string; +} +export function updateQueue( + s: AgentSession, + body: Record, +): void { + const queue = s.queue ?? []; + if (body.operation === "add") { + if (s.archived || queue.length >= 20) + throw Error( + "Restore the session or remove a queued message (maximum 20)", + ); + s.queue = [...queue, promptInput(s, body)]; + } else if (body.operation === "update") { + const i = queue.findIndex((p) => p.id === body.id); + if (i < 0) throw Error("Queued message has already been sent or removed"); + const updated = promptInput(s, body); + s.queue = queue.map((p, n) => (n === i ? { ...updated, id: p.id } : p)); + } else if (body.operation === "remove") { + s.queue = queue.filter((p) => p.id !== body.id); + } else if (body.operation === "continue") s.queuePaused = false; + else if (body.operation === "pause") s.queuePaused = true; + else throw Error("Unknown queue operation"); +} diff --git a/plugins/ai/src/backend/agents/store.ts b/plugins/ai/src/backend/agents/store.ts new file mode 100644 index 000000000..50032923c --- /dev/null +++ b/plugins/ai/src/backend/agents/store.ts @@ -0,0 +1,221 @@ +import { and, asc, desc, eq, gt, lte } from "drizzle-orm"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { agentEvents, agentQueue, agentSessions } from "../tables.js"; +import type { AgentEvent, AgentSession } from "./types.js"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Tables come from ctx.db.define, which the SDK hands back untyped. +type Table = any; +type Drizzle = any; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +/** Events kept per session; older ones are pruned as new ones land. */ +export const EVENT_HISTORY = 2000; + +type SessionRow = { + sessionId: string; + userId: string; + hostId: number; + providerId: number; + agent: AgentSession["agent"]; + model: string; + cwd: string; + executable: string; + nativeId: string | null; + title: string | null; + archived: boolean; + queuePaused: boolean; + draft: string | null; + attachments: string; + updatedAt: string; +}; + +function toRow(s: AgentSession): SessionRow { + return { + sessionId: s.id, + userId: s.userId, + hostId: s.hostId, + providerId: s.providerId, + agent: s.agent, + model: s.model, + cwd: s.cwd, + executable: s.executable, + nativeId: s.nativeId ?? null, + title: s.title ?? null, + archived: !!s.archived, + queuePaused: !!s.queuePaused, + draft: s.draft ?? null, + attachments: JSON.stringify(s.attachments ?? []), + updatedAt: s.updatedAt, + }; +} + +function fromRow(row: SessionRow): AgentSession { + return { + id: row.sessionId, + userId: row.userId, + hostId: Number(row.hostId), + providerId: Number(row.providerId), + agent: row.agent, + model: row.model, + cwd: row.cwd, + executable: row.executable, + nativeId: row.nativeId ?? undefined, + title: row.title ?? undefined, + archived: !!row.archived, + queuePaused: !!row.queuePaused, + draft: row.draft ?? undefined, + attachments: JSON.parse(row.attachments), + status: "stopped", + events: [], + updatedAt: row.updatedAt, + }; +} + +/** + * Agent sessions in the plugin's own tables. A session row holds what the + * user configured; events and queued prompts are separate rows, so a long run + * appends a few rows a second instead of rewriting everything it said. + */ +export async function createAgentStore(ctx: Pick) { + const { db } = ctx; + const sessions: Table = await db.define(agentSessions); + const events: Table = await db.define(agentEvents); + const queue: Table = await db.define(agentQueue); + const client = () => db.client(); + + async function writeSession(d: Drizzle, s: AgentSession) { + const row = toRow(s); + const where = eq(sessions.sessionId, s.id); + const existing = await d.select().from(sessions).where(where).limit(1); + if (existing.length) await d.update(sessions).set(row).where(where); + else await d.insert(sessions).values(row); + } + + return { + async list(userId: string): Promise { + const rows = await ( + await client() + ) + .select() + .from(sessions) + .where(eq(sessions.userId, userId)); + return rows.map(fromRow); + }, + + /** The session without its events, which only the event stream needs. */ + async get(userId: string, id: string): Promise { + const d = await client(); + const [row] = await d + .select() + .from(sessions) + .where(and(eq(sessions.sessionId, id), eq(sessions.userId, userId))) + .limit(1); + if (!row) return null; + const s = fromRow(row); + const prompts = await d + .select() + .from(queue) + .where(eq(queue.sessionId, id)) + .orderBy(asc(queue.position)); + if (prompts.length) + s.queue = prompts.map( + (p: { promptId: string; text: string; attachmentIds: string }) => ({ + id: p.promptId, + text: p.text, + attachmentIds: JSON.parse(p.attachmentIds), + }), + ); + return s; + }, + + async events(id: string, after = 0): Promise { + const rows = await ( + await client() + ) + .select() + .from(events) + .where(and(eq(events.sessionId, id), gt(events.seq, after))) + .orderBy(desc(events.seq)) + .limit(EVENT_HISTORY); + return rows + .reverse() + .map( + (e: { + seq: number; + kind: AgentEvent["kind"]; + text: string; + requestId: string | null; + choices: string | null; + }) => ({ + seq: Number(e.seq), + kind: e.kind, + text: e.text, + ...(e.requestId ? { requestId: e.requestId } : {}), + ...(e.choices ? { choices: JSON.parse(e.choices) } : {}), + }), + ); + }, + + /** Session fields and queue. Durable before it resolves. */ + async save(s: AgentSession): Promise { + const d = await client(); + await writeSession(d, s); + await d.delete(queue).where(eq(queue.sessionId, s.id)); + const prompts = s.queue ?? []; + if (prompts.length) + await d.insert(queue).values( + prompts.map((p, position) => ({ + sessionId: s.id, + userId: s.userId, + promptId: p.id, + position, + text: p.text, + attachmentIds: JSON.stringify(p.attachmentIds), + })), + ); + await db.persist(); + }, + + /** + * Session fields plus new events. Informational, so it joins the next + * periodic save rather than forcing one per batch. + */ + async append(s: AgentSession, batch: AgentEvent[]): Promise { + if (!batch.length) return; + const d = await client(); + const lastSeq = batch[batch.length - 1].seq; + await d.insert(events).values( + batch.map((e) => ({ + sessionId: s.id, + userId: s.userId, + seq: e.seq, + kind: e.kind, + text: e.text, + requestId: e.requestId ?? null, + choices: e.choices ? JSON.stringify(e.choices) : null, + })), + ); + await d + .delete(events) + .where( + and( + eq(events.sessionId, s.id), + lte(events.seq, lastSeq - EVENT_HISTORY), + ), + ); + await writeSession(d, s); + await db.persist({ lazy: true }); + }, + + async delete(id: string): Promise { + const d = await client(); + await d.delete(events).where(eq(events.sessionId, id)); + await d.delete(queue).where(eq(queue.sessionId, id)); + await d.delete(sessions).where(eq(sessions.sessionId, id)); + await db.persist(); + }, + }; +} + +export type AgentStore = Awaited>; diff --git a/plugins/ai/src/backend/agents/types.ts b/plugins/ai/src/backend/agents/types.ts new file mode 100644 index 000000000..42660e316 --- /dev/null +++ b/plugins/ai/src/backend/agents/types.ts @@ -0,0 +1,84 @@ +export const AGENTS = ["pi", "opencode", "claude", "codex"] as const; +export type AgentKind = (typeof AGENTS)[number]; +export interface AgentEvent { + seq: number; + kind: "text" | "tool" | "permission" | "status" | "error" | "user" | "state"; + text: string; + requestId?: string; + choices?: string[]; +} +export interface AgentAttachment { + id: string; + name: string; + path: string; + mime: string; + size: number; +} +export interface QueuedPrompt { + id: string; + text: string; + attachmentIds: string[]; +} +export interface AgentSession { + title?: string; + archived?: boolean; + draft?: string; + queue?: QueuedPrompt[]; + queuePaused?: boolean; + attachments?: AgentAttachment[]; + + id: string; + userId: string; + hostId: number; + providerId: number; + agent: AgentKind; + model: string; + cwd: string; + executable: string; + nativeId?: string; + status: "starting" | "ready" | "running" | "stopped" | "error"; + events: AgentEvent[]; + updatedAt: string; +} +export function compatible(agent: AgentKind, providerType: string): boolean { + if (agent === "claude") return providerType === "anthropic"; + if (agent === "codex") + return ["openai", "openai_compatible"].includes(providerType); + return ["openai", "openai_compatible", "anthropic", "ollama"].includes( + providerType, + ); +} +/** MySQL stores plugin text as TEXT, which holds 65535 bytes. */ +export const MAX_TEXT_BYTES = 65535; +export function fitsText(value: string): boolean { + return Buffer.byteLength(value, "utf8") <= MAX_TEXT_BYTES; +} +/** Cut to fit; the slack covers a split character decoding to U+FFFD. */ +export function clipText(value: string): string { + if (fitsText(value)) return value; + return Buffer.from(value, "utf8") + .subarray(0, MAX_TEXT_BYTES - 3) + .toString("utf8"); +} +export function shellQuote(value: string): string { + return "'" + value.replaceAll("'", "'\\''") + "'"; +} +export function validStart(value: Record): boolean { + return ( + AGENTS.includes(value.agent as AgentKind) && + Number.isSafeInteger(value.hostId) && + Number(value.hostId) > 0 && + Number.isSafeInteger(value.providerId) && + Number(value.providerId) > 0 && + typeof value.model === "string" && + value.model.length > 0 && + value.model.length <= 200 && + typeof value.cwd === "string" && + value.cwd.startsWith("/") && + value.cwd.length <= 4096 && + !value.cwd.includes("\0") && + (value.executable === undefined || + (typeof value.executable === "string" && + /^[\w./-]{1,1024}$/.test(value.executable))) + ); +} diff --git a/plugins/ai/src/backend/agents/workspace.ts b/plugins/ai/src/backend/agents/workspace.ts new file mode 100644 index 000000000..c3596648b --- /dev/null +++ b/plugins/ai/src/backend/agents/workspace.ts @@ -0,0 +1,153 @@ +import type { Client, ClientChannel } from "ssh2"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { shellQuote, type AgentSession } from "./types.js"; + +/** Fixed operations; all paths and content arrive as JSON on stdin, never shell source. */ +export const REMOTE_WORKSPACE = String.raw` +const fs=require('node:fs'),path=require('node:path'),os=require('node:os'); +const {execFileSync}=require('node:child_process'); +const MAX=1024*1024; +function imageMime(data) { + if(data.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10]))) return 'image/png'; + if(data[0]===255 && data[1]===216 && data[2]===255) return 'image/jpeg'; + if(['GIF87a','GIF89a'].includes(data.subarray(0,6).toString())) return 'image/gif'; + if(data.subarray(0,4).toString()==='RIFF' && data.subarray(8,12).toString()==='WEBP') return 'image/webp'; + return 'application/octet-stream'; +} +function main(m) { + const cwd=fs.realpathSync(m.cwd), body=m.body; + const inside=(value,missing=false)=>{ + if(typeof value!=='string' || value.includes('\0')) throw Error('Invalid path'); + const requested=path.resolve(cwd,value); + let ancestor=requested; + if(missing) while(!fs.existsSync(ancestor) && path.dirname(ancestor)!==ancestor) ancestor=path.dirname(ancestor); + const p=path.resolve(fs.realpathSync(ancestor),path.relative(ancestor,requested)); + const relative=path.relative(cwd,p); + if(relative==='..' || relative.startsWith('../') || path.isAbsolute(relative)) throw Error('Choose a file inside the working directory'); + return p; + }; + function file(p) { + const fd=fs.openSync(p,fs.constants.O_RDONLY|fs.constants.O_NOFOLLOW); + try { + const stat=fs.fstatSync(fd); + if(!stat.isFile() || stat.size>MAX) throw Error('Choose a regular file no larger than 1 MiB'); + const data=Buffer.alloc(stat.size), size=fs.readSync(fd,data,0,data.length,0); + return data.subarray(0,size); + } finally {fs.closeSync(fd);} + } + function git(args) { + return execFileSync('git',['--no-pager','-c','core.hooksPath=/dev/null','-c','core.fsmonitor=false',...args],{cwd,encoding:'utf8',maxBuffer:2*MAX,timeout:20000,env:{...process.env,GIT_TERMINAL_PROMPT:'0',GIT_OPTIONAL_LOCKS:'0'}}); + } + if(body.operation==='upload' || body.operation==='reference') { + let p,data; + if(body.operation==='reference') {p=inside(body.path);data=file(p);} + else { + if(typeof body.name!=='string' || !body.name || body.name.length>255 || body.name.includes('\0') || typeof body.data!=='string' || body.data.length>Math.ceil(MAX/3)*4 || !/^[A-Za-z0-9+/]*={0,2}$/.test(body.data)) throw Error('Invalid upload (maximum 1 MiB)'); + data=Buffer.from(body.data,'base64'); + if(data.length>MAX) throw Error('Upload exceeds 1 MiB'); + const dir=path.join(os.homedir(),'.local','state','termix-agents',m.id,'attachments'); + fs.mkdirSync(dir,{recursive:true,mode:0o700}); + p=path.join(dir,require('node:crypto').randomUUID()+'-'+path.basename(body.name).replace(/[^\p{L}\p{N}._-]/gu,'_')); + fs.writeFileSync(p,data,{mode:0o600,flag:'wx'}); + } + return {id:require('node:crypto').randomUUID(),name:body.operation==='upload'?path.basename(body.name):path.basename(p),path:p,mime:imageMime(data),size:data.length}; + } + if(body.operation==='status') { + const root=git(['rev-parse','--show-toplevel']).trim(); + const records=git(['status','--porcelain=v1','-z','--untracked-files=all']).split('\0'),files=[]; + for(let i=0;i'+'+l).join('\n')}; + } + return {diff:'--- Unstaged ---\n'+git(['diff','--no-ext-diff','--no-textconv','--',rel])+'\n--- Staged ---\n'+git(['diff','--cached','--no-ext-diff','--no-textconv','--',rel])}; + } + if(body.operation==='worktree') { + if(typeof body.branch!=='string' || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,100}$/.test(body.branch)) throw Error('Invalid branch name'); + git(['check-ref-format','--branch',body.branch]); + const dir=path.join(os.homedir(),'.local','state','termix-agents','worktrees');fs.mkdirSync(dir,{recursive:true,mode:0o700}); + const target=path.join(dir,require('node:crypto').randomUUID()); + git(['worktree','add','-b',body.branch,'--',target,'HEAD']); + return {path:target,branch:body.branch}; + } + throw Error('Unknown workspace operation'); +} +let input='';process.stdin.on('data',d=>{input+=d;if(input.length>2*MAX) {process.stderr.write('Request too large');process.exit(1);}}); +process.stdin.on('end',()=>{try{process.stdout.write(JSON.stringify(main(JSON.parse(input))));}catch(e){process.stderr.write(e.message);process.exitCode=1;}}); +`; + +export async function workspaceOperation( + ctx: PluginContext, + s: AgentSession, + body: Record, +): Promise> { + if ( + !["upload", "reference", "status", "diff", "worktree"].includes( + String(body.operation), + ) + ) + throw Error("Unknown workspace operation"); + if (JSON.stringify(body).length > 1500000) + throw Error("Upload exceeds 1 MiB"); + const connection = await ctx.ssh.connect(s.hostId, { + purpose: "ai-agent-workspace", + profile: "session", + timeoutMs: 15000, + }); + let channel: ClientChannel | undefined; + try { + const command = + 'export PATH="$HOME/.local/share/termix-agent-runtime/node/bin:$PATH"; exec node -e ' + + shellQuote(REMOTE_WORKSPACE); + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + channel?.close(); + reject(Error("Workspace operation timed out")); + }, 30000); + connection.client.exec("sh -lc " + shellQuote(command), (error, ch) => { + if (error) { + clearTimeout(timer); + reject(error); + return; + } + channel = ch; + let output = "", + stderr = ""; + ch.on("data", (d: Buffer) => { + output += d.toString(); + if (output.length > 3 * 1024 * 1024) { + ch.close(); + reject(Error("Result exceeds 3 MiB; choose a smaller file")); + } + }); + ch.stderr.on("data", (d: Buffer) => { + stderr = (stderr + d.toString()).slice(-8000); + }); + ch.on("error", (error) => { + clearTimeout(timer); + reject(error); + }); + ch.on("close", (code: number) => { + clearTimeout(timer); + if (code !== 0) { + reject(Error(stderr || "Workspace operation failed")); + return; + } + try { + resolve(JSON.parse(output)); + } catch { + reject(Error("Invalid workspace response")); + } + }); + ch.end(JSON.stringify({ id: s.id, cwd: s.cwd, body })); + }); + }); + } finally { + connection.dispose(); + } +} diff --git a/plugins/ai/src/backend/index.ts b/plugins/ai/src/backend/index.ts index dd8b43661..7d90fe487 100644 --- a/plugins/ai/src/backend/index.ts +++ b/plugins/ai/src/backend/index.ts @@ -3,13 +3,17 @@ import type { PluginContext } from "@termix/plugin-sdk/backend"; import { hostImportNormalizer } from "./host-import.js"; import { createAiRepository } from "./repository.js"; import { registerProviderSync } from "./sync.js"; +import { registerAgentRoutes } from "./agents/routes.js"; +import { createAgentStore } from "./agents/store.js"; import { registerAiRoutes } from "./routes.js"; export async function activate(ctx: PluginContext) { const repository = await createAiRepository(ctx); await registerProviderSync(ctx, repository); - registerAiRoutes(ctx.http.router(), repository, ctx); + const router = ctx.http.router(); + registerAiRoutes(router, repository, ctx); + registerAgentRoutes(router, repository, ctx, await createAgentStore(ctx)); ctx.registry.provide("ai.hostImportNormalizer", hostImportNormalizer); ctx.disposables.add( diff --git a/plugins/ai/src/backend/tables.ts b/plugins/ai/src/backend/tables.ts index bef838e5e..6a7a79247 100644 --- a/plugins/ai/src/backend/tables.ts +++ b/plugins/ai/src/backend/tables.ts @@ -135,4 +135,88 @@ export const proposals = adoptLegacyTable( ), ); -export const tables = [providers, conversations, messages, proposals]; +/* + * Coding agent sessions. Events and queued prompts are rows of their own, so a + * running agent appends instead of rewriting its session and no single value + * grows with the conversation. Text is TEXT on MySQL (64 KB), which is why + * prompts, drafts and event text are capped in UTF-8 bytes. + */ +export const agentSessions = defineTable( + "agent_sessions", + { + id: id(), + sessionId: varchar(36).notNull().unique(), + userId: refUser(), + hostId: integer().notNull(), + providerId: integer().notNull(), + agent: varchar(32).notNull(), + model: text().notNull(), + cwd: text().notNull(), + executable: text().notNull(), + nativeId: text(), + title: text(), + archived: boolean().notNull().default(false), + queuePaused: boolean().notNull().default(false), + draft: text(), + // At most 100 small records. + attachments: text().notNull().default("[]"), + updatedAt: text().notNull(), + }, + { + indexes: [{ name: "idx_ai_agent_sessions_user", columns: ["userId"] }], + }, +); + +export const agentEvents = defineTable( + "agent_events", + { + id: id(), + sessionId: varchar(36).notNull(), + userId: refUser(), + seq: integer().notNull(), + // text | tool | permission | status | error | user | state + kind: varchar(16).notNull(), + text: text().notNull(), + requestId: text(), + choices: text(), + }, + { + uniques: [ + { + name: "idx_ai_agent_events_session_seq", + columns: ["sessionId", "seq"], + }, + ], + }, +); + +export const agentQueue = defineTable( + "agent_queue", + { + id: id(), + sessionId: varchar(36).notNull(), + userId: refUser(), + promptId: varchar(36).notNull(), + position: integer().notNull(), + text: text().notNull(), + attachmentIds: text().notNull().default("[]"), + }, + { + indexes: [ + { + name: "idx_ai_agent_queue_session", + columns: ["sessionId", "position"], + }, + ], + }, +); + +export const tables = [ + providers, + conversations, + messages, + proposals, + agentSessions, + agentEvents, + agentQueue, +]; diff --git a/plugins/ai/src/frontend/agents/AgentComposer.tsx b/plugins/ai/src/frontend/agents/AgentComposer.tsx new file mode 100644 index 000000000..0aadd5803 --- /dev/null +++ b/plugins/ai/src/frontend/agents/AgentComposer.tsx @@ -0,0 +1,236 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; +import { Button, Textarea } from "@termix/plugin-sdk/ui"; +import type { AgentSession } from "../../backend/agents/types"; +import { aiApp } from "../app-ref"; +import { Attachments } from "./Attachments"; + +export interface SessionActions { + session: AgentSession; + busy: boolean; + action: (fn: () => Promise) => Promise; + onUpdated: (s: AgentSession) => void; +} +export function AgentComposer({ + session: s, + busy, + action, + onUpdated, + onComposition, +}: SessionActions & { onComposition: (value: boolean) => void }) { + const { t } = useTranslation(); + const [text, setText] = useState(s.draft ?? ""); + const [attachmentIds, setAttachmentIds] = useState([]); + const [editing, setEditing] = useState(null); + const [editText, setEditText] = useState(""); + const composing = useRef(false); + const draft = useRef({ text, changed: false }); + const draftWrites = useRef(Promise.resolve()); + const persistDraft = useCallback(() => { + if (!draft.current.changed) return; + const value = draft.current.text; + draft.current.changed = false; + draftWrites.current = draftWrites.current + .catch(() => undefined) + .then(async () => { + await aiApp().api.patch(`/agents/${s.id}`, { draft: value }); + }); + return draftWrites.current; + }, [s.id]); + useEffect(() => { + const timer = setTimeout(() => { + void persistDraft()?.catch(() => { + draft.current.changed = true; + }); + }, 500); + return () => clearTimeout(timer); + }, [text, persistDraft]); + useEffect( + () => () => { + void persistDraft()?.catch(() => undefined); + }, + [persistDraft], + ); + function change(value: string) { + draft.current = { text: value, changed: true }; + setText(value); + } + async function refresh() { + onUpdated((await aiApp().api.get(`/agents/${s.id}`)).data); + } + async function queue(body: unknown) { + await aiApp().api.post(`/agents/${s.id}/queue`, body); + await refresh(); + } + const canSend = + !busy && + !s.archived && + ["ready", "running"].includes(s.status) && + (!!text.trim() || !!attachmentIds.length); + return ( +
+ {!!s.queue?.length && ( +
+
+ + {t("agents.queue")} ({s.queue.length}) + + +
+ {s.queue.map((p) => ( +
+ {editing === p.id ? ( +