feat(ssh): rework remote instance setup and fix remote installs

Adding an SSH connection now starts from the hosts in the SSH config
instead of a blank command field, ports and install options sit behind
Advanced settings, and each connection reports one of three states with
the failure text and an action that resolves it.

Remote installs no longer touch the root-owned global npm prefix: npm is
pinned to a prefix under $HOME and bun is resolved at its known location,
because an SSH login shell exposes neither on PATH. The opencode CLI is
resolved the same way and handed to the remote server through
OPENCODE_BINARY, and the server is started and stopped through the
resolved binary rather than PATH — the HTTP shutdown route sits behind UI
authentication and never stopped anything.

A managed remote server can also be published to the remote machine's own
network. That requires a UI password, enforced in the form and again in
the SSH manager.
This commit is contained in:
Bohdan Triapitsyn
2026-08-22 16:45:56 +03:00
parent eed317f18d
commit f7a0d1f0f6
16 changed files with 1234 additions and 302 deletions
+4
View File
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
- **Desktop/Remote instances:** adding an SSH connection now starts from the hosts in your SSH config instead of a blank command field. Ports, install method and passwords moved behind Advanced settings, and each connection shows Connected, Connecting, or Needs attention with the failure text and a button that resolves it.
- Desktop/Remote instances: connecting to a remote machine now works when bun, OpenChamber or the opencode CLI live in your home directory rather than on the system path. Installing no longer fails with a permission error, and a missing opencode CLI is now reported before the connection starts instead of as a stack trace.
- Desktop/Remote instances: a managed remote server can now also be published to the remote machine's own network, so other devices there reach it without the SSH tunnel. It requires a UI password, and stays private to the tunnel otherwise.
- Desktop/Remote instances: disconnecting from a connection set to not keep the server running now actually stops that remote server.
- **Chat sessions:** start chats without choosing a project. They live in their own Chats section, rather than inheriting a project's repository and worktree context.
- **Skills catalog:** browse curated GitHub skill collections in a card-based catalog with cross-source search, skill counts, stars, recent updates, and links back to each skill's repository.
- **Diff:** the context-panel diff can now show every change on the current branch against its base branch. OpenChamber detects the base when Git knows it, or lets you choose one once when it does not.
+127 -45
View File
@@ -10,6 +10,25 @@ import { replaceFileWithRetry } from './windows-file-replace.mjs';
const LOCAL_HOST_ID = 'local';
const DEFAULT_CONNECTION_TIMEOUT_SEC = 60;
const DEFAULT_LOCAL_BIND_HOST = '127.0.0.1';
// Global npm prefixes are root-owned on most distributions, so `npm install -g`
// fails with EACCES for a normal SSH user. Everything we install goes to a
// prefix inside the user's home instead.
const REMOTE_USER_PREFIX = '$HOME/.openchamber/npm-global';
const REMOTE_BUN_CANDIDATE = '"${BUN_INSTALL:-$HOME/.bun}/bin/bun"';
// The opencode CLI usually installs into the user's home, which an SSH login
// shell does not have on PATH. The remote server only looks at OPENCODE_BINARY
// and PATH, so resolve the CLI here and hand it over explicitly.
const REMOTE_OPENCODE_CANDIDATES = [
'"$HOME/.opencode/bin/opencode"',
'"${BUN_INSTALL:-$HOME/.bun}/bin/opencode"',
'"$HOME/.local/bin/opencode"',
'"$HOME/.openchamber/npm-global/bin/opencode"',
];
const REMOTE_PATH_PREFIX = '$HOME/.opencode/bin:${BUN_INSTALL:-$HOME/.bun}/bin:$HOME/.local/bin:$HOME/.openchamber/npm-global/bin';
const REMOTE_BIN_CANDIDATES = [
'"$HOME/.openchamber/npm-global/bin/openchamber"',
'"${BUN_INSTALL:-$HOME/.bun}/bin/openchamber"',
];
const DEFAULT_CONTROL_PERSIST_SEC = 300;
const DEFAULT_READY_TIMEOUT_SEC = 30;
const DEFAULT_RECONNECT_MAX_ATTEMPTS = 5;
@@ -585,6 +604,7 @@ export class ElectronSshManager {
localUrl: null,
localPort: null,
remotePort: null,
remoteBinPath: null,
startedByUs: false,
retryAttempt: 0,
requiresUserAction: false,
@@ -811,9 +831,10 @@ export class ElectronSshManager {
mode: instance?.remoteOpenchamber?.mode === 'external' ? 'external' : 'managed',
keepRunning: instance?.remoteOpenchamber?.keepRunning !== false,
...(Number.isFinite(instance?.remoteOpenchamber?.preferredPort) ? { preferredPort: Number(instance.remoteOpenchamber.preferredPort) } : {}),
installMethod: ['npm', 'bun', 'download_release', 'upload_bundle'].includes(instance?.remoteOpenchamber?.installMethod)
installMethod: ['auto', 'npm', 'bun'].includes(instance?.remoteOpenchamber?.installMethod)
? instance.remoteOpenchamber.installMethod
: 'bun',
: 'auto',
bindHost: instance?.remoteOpenchamber?.bindHost === '0.0.0.0' ? '0.0.0.0' : '127.0.0.1',
uploadBundleOverSsh: Boolean(instance?.remoteOpenchamber?.uploadBundleOverSsh),
},
localForward: {
@@ -988,38 +1009,77 @@ export class ElectronSshManager {
return secret?.enabled && typeof secret.value === 'string' && secret.value.trim() ? secret.value.trim() : null;
}
async remoteCommandExists(parsed, controlPath, commandName) {
try {
const output = await this.runRemoteCommand(parsed, controlPath, `command -v ${commandName} >/dev/null 2>&1 && echo yes || echo no`);
return output.trim() === 'yes';
} catch {
return false;
}
}
// A login shell over SSH does not source the user's interactive rc files, so
// tools installed into a home directory (bun above all) are missing from PATH
// even when they exist. Look at their known install locations too.
async resolveRemoteTool(parsed, controlPath, commandName, extraCandidates = []) {
const candidateList = [...extraCandidates, `"$(command -v ${commandName} 2>/dev/null)"`].join(' ');
const script = [
`for candidate in ${candidateList}; do`,
' [ -n "$candidate" ] || continue;',
' [ -x "$candidate" ] || continue;',
` printf '%s' "$candidate";`,
' exit 0;',
'done',
].join(' ');
async currentRemoteOpenChamberVersion(parsed, controlPath) {
try {
const output = await this.runRemoteCommand(parsed, controlPath, 'openchamber --version 2>/dev/null || true');
return parseVersionToken(output);
const output = await this.runRemoteCommand(parsed, controlPath, script);
return output.trim() || null;
} catch {
return null;
}
}
async installOpenChamberManaged(parsed, controlPath, version, preferred) {
const hasBun = await this.remoteCommandExists(parsed, controlPath, 'bun');
const hasNpm = await this.remoteCommandExists(parsed, controlPath, 'npm');
const commands = [];
// Every place OpenChamber may live on the remote host, with the version each
// one reports. Installs land in the user prefix while an older copy can still
// sit on PATH, so the caller picks by version instead of trusting PATH order.
async remoteOpenChamberCandidates(parsed, controlPath) {
const script = [
`for candidate in ${REMOTE_BIN_CANDIDATES.join(' ')} "$(command -v openchamber 2>/dev/null)"; do`,
' [ -n "$candidate" ] || continue;',
' [ -x "$candidate" ] || continue;',
` printf '%s\t%s\n' "$candidate" "$("$candidate" --version 2>/dev/null | head -n 1)";`,
'done',
].join(' ');
if (preferred === 'bun') {
if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`);
if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`);
} else if (preferred === 'npm') {
if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`);
if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`);
let output = '';
try {
output = await this.runRemoteCommand(parsed, controlPath, script);
} catch {
return [];
}
const candidates = [];
const seen = new Set();
for (const line of output.split(/\r?\n/)) {
const [binPath, versionRaw] = line.split('\t');
const trimmed = (binPath || '').trim();
if (!trimmed || seen.has(trimmed)) continue;
seen.add(trimmed);
candidates.push({ binPath: trimmed, version: parseVersionToken(versionRaw || '') });
}
return candidates;
}
async installOpenChamberManaged(parsed, controlPath, version, preferred) {
const bunPath = await this.resolveRemoteTool(parsed, controlPath, 'bun', [REMOTE_BUN_CANDIDATE]);
const npmPath = await this.resolveRemoteTool(parsed, controlPath, 'npm');
// bun's global install already targets ~/.bun; npm is pinned to a prefix in
// the user's home so it never touches the root-owned global directory.
const bunCommand = bunPath ? `${shellQuote(bunPath)} add -g @openchamber/web@${version}` : null;
const npmCommand = npmPath
? `mkdir -p "${REMOTE_USER_PREFIX}" && ${shellQuote(npmPath)} install -g --prefix "${REMOTE_USER_PREFIX}" @openchamber/web@${version}`
: null;
const commands = [];
if (preferred === 'npm') {
if (npmCommand) commands.push(npmCommand);
if (bunCommand) commands.push(bunCommand);
} else {
if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`);
if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`);
if (bunCommand) commands.push(bunCommand);
if (npmCommand) commands.push(npmCommand);
}
if (commands.length === 0) {
@@ -1079,24 +1139,35 @@ export class ElectronSshManager {
}
}
async startRemoteServerManaged(parsed, controlPath, instance, desiredPort) {
let envPrefix = 'OPENCHAMBER_RUNTIME=ssh-remote';
async startRemoteServerManaged(parsed, controlPath, instance, desiredPort, binPath) {
const opencodePath = await this.resolveRemoteTool(parsed, controlPath, 'opencode', REMOTE_OPENCODE_CANDIDATES);
if (!opencodePath) {
throw new Error('The opencode CLI is not installed on the remote machine. Install it there, then connect again');
}
const secret = this.configuredOpenChamberPassword(instance);
const remoteBindHost = instance.remoteOpenchamber?.bindHost === '0.0.0.0' ? '0.0.0.0' : '127.0.0.1';
// Binding the remote server to every interface publishes its UI to the
// remote machine's whole network, so it may not run without a password.
if (remoteBindHost === '0.0.0.0' && !secret) {
throw new Error('Exposing the remote server to its network requires a UI password');
}
let envPrefix = `PATH="${REMOTE_PATH_PREFIX}:$PATH" OPENCODE_BINARY=${shellQuote(opencodePath)} OPENCHAMBER_RUNTIME=ssh-remote`;
if (secret) {
envPrefix += ` OPENCHAMBER_UI_PASSWORD=${shellQuote(secret)}`;
}
const output = await this.runRemoteCommand(parsed, controlPath, `${envPrefix} openchamber serve --hostname 127.0.0.1 --port ${desiredPort}`);
const output = await this.runRemoteCommand(parsed, controlPath, `${envPrefix} ${shellQuote(binPath)} serve --hostname ${remoteBindHost} --port ${desiredPort}`);
const port = output.split(/\s+/).map((token) => Number.parseInt(token, 10)).find((value) => Number.isFinite(value));
return port || desiredPort;
}
async stopRemoteServerBestEffort(parsed, controlPath, remotePort) {
// `openchamber stop` owns the daemon lifecycle. The HTTP shutdown route sits
// behind UI authentication, so it cannot stop a password-protected server.
async stopRemoteServerBestEffort(parsed, controlPath, remotePort, remoteBinPath) {
if (!remoteBinPath) return;
try {
await this.runRemoteCommand(
parsed,
controlPath,
`if command -v curl >/dev/null 2>&1; then curl -fsS -X POST http://127.0.0.1:${remotePort}/api/system/shutdown >/dev/null 2>&1 || true; elif command -v wget >/dev/null 2>&1; then wget -qO- --method=POST http://127.0.0.1:${remotePort}/api/system/shutdown >/dev/null 2>&1 || true; fi`,
);
await this.runRemoteCommand(parsed, controlPath, `${shellQuote(remoteBinPath)} stop --port ${remotePort}`);
} catch {
}
}
@@ -1150,17 +1221,27 @@ export class ElectronSshManager {
const port = instance.remoteOpenchamber.preferredPort;
this.setStatus(instance.id, 'server_detecting', 'Probing external OpenChamber server', null, null, port, false, 0, false);
await this.probeRemoteSystemInfo(parsed, controlPath, port, this.configuredOpenChamberPassword(instance));
return { remotePort: port, startedByUs: false };
return { remotePort: port, startedByUs: false, remoteBinPath: null };
}
this.setStatus(instance.id, 'remote_probe', 'Checking remote OpenChamber installation');
const installedVersion = await this.currentRemoteOpenChamberVersion(parsed, controlPath);
if (!installedVersion) {
this.setStatus(instance.id, 'installing', 'Installing OpenChamber on remote host');
await this.installOpenChamberManaged(parsed, controlPath, this.appVersion, instance.remoteOpenchamber.installMethod);
} else if (installedVersion !== this.appVersion) {
this.setStatus(instance.id, 'updating', `Updating remote OpenChamber from ${installedVersion} to ${this.appVersion}`);
const installed = await this.remoteOpenChamberCandidates(parsed, controlPath);
let binary = installed.find((candidate) => candidate.version === this.appVersion) || null;
if (!binary) {
const existing = installed[0] || null;
if (existing) {
this.setStatus(instance.id, 'updating', `Updating remote OpenChamber from ${existing.version || 'unknown'} to ${this.appVersion}`);
} else {
this.setStatus(instance.id, 'installing', 'Installing OpenChamber on remote host');
}
await this.installOpenChamberManaged(parsed, controlPath, this.appVersion, instance.remoteOpenchamber.installMethod);
const afterInstall = await this.remoteOpenChamberCandidates(parsed, controlPath);
binary = afterInstall.find((candidate) => candidate.version === this.appVersion) || afterInstall[0] || existing;
if (!binary) {
throw new Error('OpenChamber was installed on the remote host but no openchamber binary could be found');
}
}
this.setStatus(instance.id, 'server_detecting', 'Detecting managed OpenChamber server');
@@ -1172,13 +1253,13 @@ export class ElectronSshManager {
if (!remotePort) {
this.setStatus(instance.id, 'server_starting', 'Starting managed OpenChamber server');
const desiredPort = instance.remoteOpenchamber.preferredPort || randomPortCandidate(instance.id);
remotePort = await this.startRemoteServerManaged(parsed, controlPath, instance, desiredPort);
remotePort = await this.startRemoteServerManaged(parsed, controlPath, instance, desiredPort, binary.binPath);
startedByUs = true;
}
if (!(await this.remoteServerRunning(parsed, controlPath, remotePort, this.configuredOpenChamberPassword(instance)))) {
throw new Error('Managed OpenChamber server failed to become reachable');
}
return { remotePort, startedByUs };
return { remotePort, startedByUs, remoteBinPath: binary.binPath };
}
async disconnectInternal(id, reportIdle) {
@@ -1193,7 +1274,7 @@ export class ElectronSshManager {
if (session) {
if (session.startedByUs && session.remotePort && session.instance.remoteOpenchamber.mode === 'managed' && !session.instance.remoteOpenchamber.keepRunning) {
await this.stopRemoteServerBestEffort(session.parsed, session.controlPath, session.remotePort);
await this.stopRemoteServerBestEffort(session.parsed, session.controlPath, session.remotePort, session.remoteBinPath);
}
await this.stopControlMasterBestEffort(session.parsed, session.controlPath);
const auth = this.sshAuth.get(session.parsed);
@@ -1269,9 +1350,10 @@ export class ElectronSshManager {
throw new Error(`Unsupported remote OS: ${remoteOs}`);
}
const { remotePort, startedByUs } = await this.ensureRemoteServer(instance, parsed, controlPath);
const { remotePort, startedByUs, remoteBinPath } = await this.ensureRemoteServer(instance, parsed, controlPath);
session.remotePort = remotePort;
session.startedByUs = startedByUs;
session.remoteBinPath = remoteBinPath;
this.setStatus(id, 'forwarding', 'Setting up port forwards', null, null, remotePort, startedByUs, 0, false);
const bindHost = sanitizeBindHost(instance.localForward?.bindHost);
+157
View File
@@ -289,4 +289,161 @@ describe('ElectronSshManager', () => {
});
expect(settings.desktopHosts).toEqual([{ id: 'ssh-1', label: 'SSH Host', url: localUrl, apiUrl: localUrl, clientToken: 'ssh-client-token' }]);
});
test('installs OpenChamber into a home-owned npm prefix instead of the root-owned global one', async () => {
const commands = [];
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.resolveRemoteTool = async (_parsed, _controlPath, name) => (name === 'npm' ? '/usr/bin/npm' : null);
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
commands.push(script);
return '';
};
await manager.installOpenChamberManaged({ destination: 'user@example.test', args: [] }, '/tmp/control.sock', '1.2.3', 'auto');
expect(commands).toHaveLength(1);
expect(commands[0]).toContain('--prefix "$HOME/.openchamber/npm-global"');
expect(commands[0]).not.toMatch(/npm install -g @openchamber/);
});
test('lists every remote OpenChamber binary with its reported version', async () => {
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.runRemoteCommand = async () => [
'/home/pi/.openchamber/npm-global/bin/openchamber\t1.2.3',
'/usr/bin/openchamber\t0.9.0',
'',
].join('\n');
const candidates = await manager.remoteOpenChamberCandidates({ destination: 'user@example.test', args: [] }, '/tmp/control.sock');
expect(candidates).toEqual([
{ binPath: '/home/pi/.openchamber/npm-global/bin/openchamber', version: '1.2.3' },
{ binPath: '/usr/bin/openchamber', version: '0.9.0' },
]);
});
test('starts the resolved OpenChamber binary rather than whatever PATH exposes', async () => {
let started = '';
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.resolveRemoteTool = async () => '/home/pi/.opencode/bin/opencode';
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
started = script;
return '4321\n';
};
const instance = { id: 'ssh-1', auth: {}, remoteOpenchamber: { mode: 'managed' } };
const port = await manager.startRemoteServerManaged(
{ destination: 'user@example.test', args: [] },
'/tmp/control.sock',
instance,
4321,
'/home/pi/.openchamber/npm-global/bin/openchamber',
);
expect(port).toBe(4321);
expect(started).toContain("'/home/pi/.openchamber/npm-global/bin/openchamber' serve");
expect(started).toContain("OPENCODE_BINARY='/home/pi/.opencode/bin/opencode'");
expect(started).toContain('$HOME/.opencode/bin:');
});
test('refuses to start when the remote machine has no opencode CLI', async () => {
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.resolveRemoteTool = async () => null;
manager.runRemoteCommand = async () => {
throw new Error('should not start the server without a CLI');
};
await expect(manager.startRemoteServerManaged(
{ destination: 'user@example.test', args: [] },
'/tmp/control.sock',
{ id: 'ssh-1', auth: {}, remoteOpenchamber: { mode: 'managed' } },
4321,
'/home/pi/.bun/bin/openchamber',
)).rejects.toThrow(/opencode CLI is not installed/);
});
test('prefers a bun that only exists in the home directory over npm', async () => {
const commands = [];
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
// A login shell over SSH does not put ~/.bun/bin on PATH.
manager.resolveRemoteTool = async (_parsed, _controlPath, name) =>
(name === 'bun' ? '/home/pi/.bun/bin/bun' : '/usr/bin/npm');
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
commands.push(script);
return '';
};
await manager.installOpenChamberManaged({ destination: 'user@example.test', args: [] }, '/tmp/control.sock', '1.2.3', 'auto');
expect(commands).toEqual(["'/home/pi/.bun/bin/bun' add -g @openchamber/web@1.2.3"]);
});
test('stops a remote server it started through the CLI, not the authenticated HTTP route', async () => {
const scripts = [];
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
scripts.push(script);
return '';
};
await manager.stopRemoteServerBestEffort(
{ destination: 'user@example.test', args: [] },
'/tmp/control.sock',
41777,
'/home/pi/.bun/bin/openchamber',
);
expect(scripts).toEqual(["'/home/pi/.bun/bin/openchamber' stop --port 41777"]);
});
test('publishes the remote server to its network only with a UI password', async () => {
const manager = new ElectronSshManager({
settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'),
appVersion: '1.2.3',
emit: () => undefined,
});
manager.resolveRemoteTool = async () => '/home/pi/.opencode/bin/opencode';
let started = '';
manager.runRemoteCommand = async (_parsed, _controlPath, script) => {
started = script;
return '4321\n';
};
const parsed = { destination: 'user@example.test', args: [] };
const exposed = {
id: 'ssh-1',
auth: {},
remoteOpenchamber: { mode: 'managed', bindHost: '0.0.0.0' },
};
await expect(manager.startRemoteServerManaged(parsed, '/tmp/control.sock', exposed, 4321, '/bin/openchamber'))
.rejects.toThrow(/requires a UI password/);
const secured = {
...exposed,
auth: { openchamberPassword: { enabled: true, value: 'remote-secret', store: 'settings' } },
};
await manager.startRemoteServerManaged(parsed, '/tmp/control.sock', secured, 4321, '/bin/openchamber');
expect(started).toContain('--hostname 0.0.0.0');
});
});
@@ -23,7 +23,9 @@ import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLay
import {
SettingsSection,
SettingsGroupTitle,
SettingsChipGroup,
SETTINGS_PAGE_TITLE_CLASS,
SETTINGS_SECTION_TITLE_CLASS,
SETTINGS_FIELD_LABEL_CLASS,
SETTINGS_SELECT_SIZE,
} from '@/components/sections/shared/SettingsSection';
@@ -150,6 +152,58 @@ const isConnectingPhase = (phase?: string): boolean => {
return Boolean(phase && CONNECTING_PHASES.has(phase));
};
// The backend reports 13 lifecycle phases. People only need to know which of
// three situations they are in; the phase stays as the secondary detail line.
type InstanceState = 'idle' | 'connecting' | 'ready' | 'error';
const instanceState = (phase?: string): InstanceState => {
if (phase === 'ready') return 'ready';
if (phase === 'error') return 'error';
if (phase === 'degraded' || isConnectingPhase(phase)) return 'connecting';
return 'idle';
};
const instanceStateLabelKey = (state: InstanceState): I18nKey => {
switch (state) {
case 'ready':
return 'settings.remoteInstances.page.state.ready';
case 'connecting':
return 'settings.remoteInstances.page.state.connecting';
case 'error':
return 'settings.remoteInstances.page.state.problem';
default:
return 'settings.remoteInstances.page.state.notConnected';
}
};
// Known backend failures that the user can act on from here. Everything else
// falls back to the raw detail plus the logs button.
type ErrorRemedy = 'uiPassword' | 'localPort' | 'noRuntime' | 'noOpencode' | 'externalPort' | null;
const errorRemedy = (detail?: string): ErrorRemedy => {
const text = (detail || '').toLowerCase();
if (!text) return null;
if (text.includes('ui authentication') || text.includes('ui password')) return 'uiPassword';
if (text.includes('already in use') || text.includes('eaddrinuse')) return 'localPort';
if (text.includes('neither bun nor npm')) return 'noRuntime';
if (text.includes('opencode cli is not installed')) return 'noOpencode';
if (text.includes('requires a ui password')) return 'uiPassword';
if (text.includes('preferred remote openchamber port')) return 'externalPort';
return null;
};
// Remedies the user resolves on the remote machine: explain, do not offer a button.
const REMEDY_HINT_KEYS = {
noRuntime: 'settings.remoteInstances.page.error.hint.noRuntime',
noOpencode: 'settings.remoteInstances.page.error.hint.noOpencode',
} satisfies Record<string, I18nKey>;
const remedyHintKey = (remedy: ErrorRemedy): I18nKey | null => {
if (remedy === 'noRuntime') return REMEDY_HINT_KEYS.noRuntime;
if (remedy === 'noOpencode') return REMEDY_HINT_KEYS.noOpencode;
return null;
};
const phaseDotClass = (phase?: string): string => {
if (phase === 'ready') {
return 'bg-[var(--status-success)] animate-pulse';
@@ -462,8 +516,11 @@ export const RemoteInstancesPage: React.FC = () => {
const [transportOptions, setTransportOptions] = React.useState<{ localUrl: string | null; lanUrl: string | null; relayAvailable: boolean } | null>(null);
const revokedClientCount = React.useMemo(() => remoteClients.filter((client) => Boolean(client.revokedAt)).length, [remoteClients]);
const [sshAddDialogOpen, setSshAddDialogOpen] = React.useState(false);
const [sshCommandDraft, setSshCommandDraft] = React.useState('ssh user@example.com');
const [sshAddMode, setSshAddMode] = React.useState<'saved' | 'manual'>('saved');
const [sshHostSearch, setSshHostSearch] = React.useState('');
const [sshCommandDraft, setSshCommandDraft] = React.useState('');
const [sshNameDraft, setSshNameDraft] = React.useState('');
const [advancedOpen, setAdvancedOpen] = React.useState(false);
React.useEffect(() => {
void load();
@@ -730,7 +787,7 @@ export const RemoteInstancesPage: React.FC = () => {
await createFromCommand(id, command, sshNameDraft.trim() || t('settings.remoteInstances.sidebar.newSshInstanceName'));
setSelectedId(id);
setSshAddDialogOpen(false);
setSshCommandDraft('ssh user@example.com');
setSshCommandDraft('');
setSshNameDraft('');
toast.success(t('settings.remoteInstances.page.toast.instanceCreated'));
} catch (error) {
@@ -740,6 +797,12 @@ export const RemoteInstancesPage: React.FC = () => {
}
}, [createFromCommand, setSelectedId, sshCommandDraft, sshNameDraft, t]);
const openSshAddDialog = React.useCallback(() => {
setSshHostSearch('');
setSshAddMode(importCandidates.length > 0 ? 'saved' : 'manual');
setSshAddDialogOpen(true);
}, [importCandidates.length]);
const setDefaultDirectHost = React.useCallback(async (id: string) => {
await persistDirectHosts(directHosts, id);
}, [directHosts, persistDirectHosts]);
@@ -1000,6 +1063,11 @@ export const RemoteInstancesPage: React.FC = () => {
setDraft(selectedInstance);
}, [selectedInstance]);
// Every instance opens on the simple view; advanced stays a deliberate choice.
React.useEffect(() => {
setAdvancedOpen(false);
}, [selectedId]);
React.useEffect(() => {
if (!selectedId) {
return;
@@ -1064,6 +1132,9 @@ export const RemoteInstancesPage: React.FC = () => {
const canDisconnect = isReady || isBusy;
const statusAgeMs = status ? Math.max(0, clockMs - status.updatedAtMs) : 0;
const reconnectAppearsStuck = isReconnecting && statusAgeMs > 12_000;
const currentState = instanceState(statusPhase);
const currentRemedy = currentState === 'error' ? errorRemedy(status?.detail) : null;
const currentRemedyHintKey = remedyHintKey(currentRemedy);
const hasChanges = React.useMemo(() => {
if (!draft || !selectedInstance) return false;
@@ -1083,6 +1154,25 @@ export const RemoteInstancesPage: React.FC = () => {
return;
}
// "Already running" cannot pick a port on its own; catching it here keeps
// the failure in the form instead of surfacing it mid-connect.
if (normalized.remoteOpenchamber.mode === 'external' && !normalized.remoteOpenchamber.preferredPort) {
toast.error(t('settings.remoteInstances.page.validation.externalPortRequired'));
setAdvancedOpen(true);
return;
}
if (
normalized.remoteOpenchamber.mode === 'managed' &&
normalized.remoteOpenchamber.bindHost === '0.0.0.0' &&
!normalized.auth.openchamberPassword?.value?.trim()
) {
toast.error(t('settings.remoteInstances.page.validation.remoteLanNeedsPassword'));
setAdvancedOpen(true);
window.setTimeout(() => uiPasswordRef.current?.focus(), 0);
return;
}
if (normalized.localForward.bindHost === '0.0.0.0') {
const allow = window.confirm(
t('settings.remoteInstances.page.confirm.bindAllInterfaces'),
@@ -1154,6 +1244,7 @@ export const RemoteInstancesPage: React.FC = () => {
const handleImportCandidate = React.useCallback(
(host: string, pattern: boolean) => {
setSshAddDialogOpen(false);
if (pattern) {
setPatternHost(host);
setPatternDestination(suggestConcreteHost(host));
@@ -1164,6 +1255,25 @@ export const RemoteInstancesPage: React.FC = () => {
[createImportedInstance],
);
const filteredImportCandidates = React.useMemo(() => {
const query = sshHostSearch.trim().toLowerCase();
if (!query) return importCandidates;
return importCandidates.filter((candidate) => {
return candidate.host.toLowerCase().includes(query) || candidate.sshCommand.toLowerCase().includes(query);
});
}, [importCandidates, sshHostSearch]);
// Opening a ready instance means pointing this window at the forwarded local
// URL — the same navigation the host switcher performs after its own connect.
const openInstanceUrl = React.useCallback((localUrl?: string) => {
const target = (localUrl || '').trim();
if (!target) {
toast.error(t('settings.remoteInstances.page.toast.instanceUrlUnavailable'));
return;
}
navigateToUrl(target);
}, [t]);
const handlePatternCreate = React.useCallback(async () => {
const host = patternHost;
const destination = patternDestination.trim();
@@ -1216,6 +1326,44 @@ export const RemoteInstancesPage: React.FC = () => {
}
}, [connect, selectedInstance, t, upsertInstance]);
const uiPasswordRef = React.useRef<HTMLInputElement | null>(null);
const remotePortRef = React.useRef<HTMLDivElement | null>(null);
// Turn a reported failure into the one action that resolves it, instead of
// leaving the raw backend sentence as the whole answer.
const applyErrorRemedy = React.useCallback(async (remedy: ErrorRemedy) => {
if (!selectedInstance) return;
if (remedy === 'localPort') {
const nextInstance: DesktopSshInstance = {
...selectedInstance,
localForward: {
...selectedInstance.localForward,
preferredLocalPort: randomPort(),
},
};
try {
await upsertInstance(nextInstance);
await connect(nextInstance.id);
toast.success(t('settings.remoteInstances.sidebar.toast.retriedWithRandomPort'));
} catch (error) {
toast.error(t('settings.remoteInstances.page.toast.connectFailed'), {
description: error instanceof Error ? error.message : String(error),
});
}
return;
}
setAdvancedOpen(true);
window.setTimeout(() => {
if (remedy === 'uiPassword') {
uiPasswordRef.current?.focus();
return;
}
remotePortRef.current?.scrollIntoView({ block: 'center' });
}, 0);
}, [connect, selectedInstance, t, upsertInstance]);
const readLogsForInstance = React.useCallback(async (id: string) => {
const lines = await desktopSshLogs(id, 600);
return lines.map((line) => formatLogLine(line));
@@ -1321,6 +1469,24 @@ export const RemoteInstancesPage: React.FC = () => {
return;
}
if (!canDisconnect && draft.remoteOpenchamber.mode === 'external' && !draft.remoteOpenchamber.preferredPort) {
toast.error(t('settings.remoteInstances.page.validation.externalPortRequired'));
setAdvancedOpen(true);
return;
}
if (
!canDisconnect &&
draft.remoteOpenchamber.mode === 'managed' &&
draft.remoteOpenchamber.bindHost === '0.0.0.0' &&
!draft.auth.openchamberPassword?.value?.trim()
) {
toast.error(t('settings.remoteInstances.page.validation.remoteLanNeedsPassword'));
setAdvancedOpen(true);
window.setTimeout(() => uiPasswordRef.current?.focus(), 0);
return;
}
setIsPrimaryActionPending(true);
const operation = canDisconnect ? disconnect(draft.id) : connectWithPortRecovery();
void operation
@@ -1774,7 +1940,7 @@ export const RemoteInstancesPage: React.FC = () => {
title={t('settings.remoteInstances.sidebar.title')}
description={t('settings.remoteInstances.sidebar.total', { count: instances.length })}
headerAction={(
<Button type="button" size="xs" className="!font-normal" onClick={() => setSshAddDialogOpen(true)}>
<Button type="button" size="xs" className="!font-normal" onClick={openSshAddDialog}>
<Icon name="add" className="h-3.5 w-3.5" />
{t('settings.remoteInstances.sidebar.actions.addSshInstance')}
</Button>
@@ -1782,50 +1948,71 @@ export const RemoteInstancesPage: React.FC = () => {
contentClassName="space-y-2.5"
>
{isLoading ? (
<p className="typography-meta text-muted-foreground">{t('settings.remoteInstances.page.import.loading')}</p>
<p className="typography-meta text-muted-foreground">{t('settings.remoteInstances.page.state.loadingInstances')}</p>
) : instances.length === 0 ? (
<p className="typography-meta text-muted-foreground">{t('settings.remoteInstances.page.import.noneFound')}</p>
<p className="typography-meta text-muted-foreground">
{importCandidates.length === 1
? t('settings.remoteInstances.page.empty.noInstancesWithOneImport')
: importCandidates.length > 1
? t('settings.remoteInstances.page.empty.noInstancesWithImports', { count: importCandidates.length })
: t('settings.remoteInstances.page.empty.noInstances')}
</p>
) : instances.map((instance) => {
const instanceStatus = statusesById[instance.id];
const title = instance.nickname?.trim() || instance.sshParsed?.destination || instance.id;
const phase = instanceStatus?.phase;
const ready = phase === 'ready';
const state = instanceState(phase);
const failureDetail = state === 'error' ? instanceStatus?.detail : undefined;
return (
<div key={instance.id} className="flex items-center justify-between gap-3 py-1.5">
<div className="min-w-0 space-y-0.5">
<div className="flex min-w-0 items-center gap-2">
<span className={`h-2 w-2 rounded-full ${phaseDotClass(phase)}`} />
<p className="typography-ui-label text-foreground truncate">{title}</p>
<div key={instance.id} className="space-y-1.5 py-1.5">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 space-y-0.5">
<div className="flex min-w-0 items-center gap-2">
<span className={`h-2 w-2 rounded-full ${phaseDotClass(phase)}`} />
<p className="typography-ui-label text-foreground truncate">{title}</p>
</div>
<p className="typography-micro text-muted-foreground truncate">
{t(instanceStateLabelKey(state))}
{state === 'connecting' ? ` · ${t(phaseLabelKey(phase))}` : ''}
{ready && instanceStatus?.localUrl ? ` · ${instanceStatus.localUrl}` : ''}
</p>
</div>
<div className="flex shrink-0 items-center gap-1">
{ready ? (
<Button type="button" variant="ghost" size="xs" className="!font-normal" onClick={() => openInstanceUrl(instanceStatus?.localUrl)}>
<Icon name="external-link" className="h-3.5 w-3.5" />
{t('settings.remoteInstances.page.actions.open')}
</Button>
) : null}
<Button type="button" variant="ghost" size="xs" className="!font-normal" onClick={() => {
const op = ready ? disconnect(instance.id) : connect(instance.id);
void op.catch((err) => toast.error(ready ? t('settings.remoteInstances.sidebar.toast.disconnectFailed') : t('settings.remoteInstances.sidebar.toast.connectFailed'), {
description: err instanceof Error ? err.message : String(err),
}));
}}>
{ready ? <Icon name="stop" className="h-3.5 w-3.5" /> : <Icon name="plug-2" className="h-3.5 w-3.5" />}
{ready ? t('settings.remoteInstances.sidebar.actions.disconnect') : t('settings.remoteInstances.sidebar.actions.connect')}
</Button>
<Button type="button" variant="ghost" size="xs" className="!font-normal" onClick={() => setSelectedId(instance.id)}>
<Icon name="pencil" className="h-3.5 w-3.5" />
{t('desktopHostSwitcher.actions.edit')}
</Button>
<Button type="button" variant="ghost" size="xs" className="!font-normal" onClick={() => {
const ok = window.confirm(t('settings.remoteInstances.page.confirm.removeInstance'));
if (!ok) return;
void removeInstance(instance.id).catch((err) => toast.error(t('settings.remoteInstances.page.toast.removeInstanceFailed'), {
description: err instanceof Error ? err.message : String(err),
}));
}}>
<Icon name="delete-bin" className="h-3.5 w-3.5" />
{t('settings.common.actions.delete')}
</Button>
</div>
<p className="typography-micro text-muted-foreground truncate">
{t(phaseLabelKey(phase))}{instanceStatus?.localUrl ? ` · ${instanceStatus.localUrl}` : ''}
</p>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button type="button" variant="ghost" size="xs" className="!font-normal" onClick={() => {
const op = ready ? disconnect(instance.id) : connect(instance.id);
void op.catch((err) => toast.error(ready ? t('settings.remoteInstances.sidebar.toast.disconnectFailed') : t('settings.remoteInstances.sidebar.toast.connectFailed'), {
description: err instanceof Error ? err.message : String(err),
}));
}}>
{ready ? <Icon name="stop" className="h-3.5 w-3.5" /> : <Icon name="plug-2" className="h-3.5 w-3.5" />}
{ready ? t('settings.remoteInstances.sidebar.actions.disconnect') : t('settings.remoteInstances.sidebar.actions.connect')}
</Button>
<Button type="button" variant="ghost" size="xs" className="!font-normal" onClick={() => setSelectedId(instance.id)}>
<Icon name="pencil" className="h-3.5 w-3.5" />
{t('desktopHostSwitcher.actions.edit')}
</Button>
<Button type="button" variant="ghost" size="xs" className="!font-normal" onClick={() => {
const ok = window.confirm(t('settings.remoteInstances.page.confirm.removeInstance'));
if (!ok) return;
void removeInstance(instance.id).catch((err) => toast.error(t('settings.remoteInstances.page.toast.removeInstanceFailed'), {
description: err instanceof Error ? err.message : String(err),
}));
}}>
<Icon name="delete-bin" className="h-3.5 w-3.5" />
{t('settings.common.actions.delete')}
</Button>
</div>
{failureDetail ? (
<p className="typography-micro text-[var(--status-error)] break-words">{failureDetail}</p>
) : null}
</div>
);
})}
@@ -1835,52 +2022,70 @@ export const RemoteInstancesPage: React.FC = () => {
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{t('settings.remoteInstances.sidebar.actions.addSshInstance')}</DialogTitle>
<DialogDescription>{t('settings.remoteInstances.page.section.instanceDescription')}</DialogDescription>
<DialogDescription>{t('settings.remoteInstances.page.addDialog.description')}</DialogDescription>
</DialogHeader>
<form className="space-y-3" onSubmit={(event) => { event.preventDefault(); void createSshInstanceFromDialog(); }}>
<Input className="h-8" value={sshNameDraft} onChange={(event) => setSshNameDraft(event.target.value)} placeholder={t('settings.remoteInstances.page.field.nicknamePlaceholder')} disabled={isSaving} />
<Input className="h-8" value={sshCommandDraft} onChange={(event) => setSshCommandDraft(event.target.value)} placeholder={t('settings.remoteInstances.page.field.sshCommandPlaceholder')} disabled={isSaving} autoFocus />
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" size="xs" className="!font-normal" onClick={() => setSshAddDialogOpen(false)} disabled={isSaving}>{t('settings.common.actions.cancel')}</Button>
<Button type="submit" size="xs" className="!font-normal" disabled={isSaving || !sshCommandDraft.trim()}>{t('settings.common.actions.create')}</Button>
<SettingsChipGroup
value={sshAddMode}
onChange={setSshAddMode}
aria-label={t('settings.remoteInstances.page.addDialog.sourceLabel')}
options={[
{ value: 'saved', label: t('settings.remoteInstances.page.addDialog.tab.saved') },
{ value: 'manual', label: t('settings.remoteInstances.page.addDialog.tab.manual') },
]}
/>
{sshAddMode === 'saved' ? (
<div className="space-y-2">
<Input
className="h-8"
value={sshHostSearch}
onChange={(event) => setSshHostSearch(event.target.value)}
placeholder={t('settings.remoteInstances.page.addDialog.searchPlaceholder')}
autoFocus
/>
{isImportsLoading ? (
<p className="typography-meta text-muted-foreground">{t('settings.remoteInstances.page.import.loading')}</p>
) : importCandidates.length === 0 ? (
<p className="typography-meta text-muted-foreground">{t('settings.remoteInstances.page.addDialog.emptySaved')}</p>
) : filteredImportCandidates.length === 0 ? (
<p className="typography-meta text-muted-foreground">{t('settings.remoteInstances.page.addDialog.searchEmpty')}</p>
) : (
<div className="max-h-[45vh] overflow-auto">
{filteredImportCandidates.map((candidate) => (
<div key={`${candidate.source}:${candidate.host}`} className="flex items-center justify-between gap-3 border-b border-[var(--surface-subtle)] py-2.5 last:border-b-0">
<div className="min-w-0">
<div className="typography-ui-label font-medium text-foreground truncate">
{candidate.host}
{candidate.pattern ? ` ${t('settings.remoteInstances.page.import.patternSuffix')}` : ''}
</div>
<div className="typography-meta text-muted-foreground truncate">{candidate.sshCommand}</div>
</div>
<Button
type="button"
variant="outline"
size="xs"
className="!font-normal"
onClick={() => void handleImportCandidate(candidate.host, candidate.pattern)}
>
{t('settings.remoteInstances.page.addDialog.use')}
</Button>
</div>
))}
</div>
)}
</div>
</form>
) : (
<form className="space-y-3" onSubmit={(event) => { event.preventDefault(); void createSshInstanceFromDialog(); }}>
<Input className="h-8" value={sshNameDraft} onChange={(event) => setSshNameDraft(event.target.value)} placeholder={t('settings.remoteInstances.page.field.nicknamePlaceholder')} disabled={isSaving} />
<Input className="h-8" value={sshCommandDraft} onChange={(event) => setSshCommandDraft(event.target.value)} placeholder={t('settings.remoteInstances.page.field.sshCommandPlaceholder')} disabled={isSaving} autoFocus />
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" size="xs" className="!font-normal" onClick={() => setSshAddDialogOpen(false)} disabled={isSaving}>{t('settings.common.actions.cancel')}</Button>
<Button type="submit" size="xs" className="!font-normal" disabled={isSaving || !sshCommandDraft.trim()}>{t('settings.common.actions.create')}</Button>
</div>
</form>
)}
</DialogContent>
</Dialog> : null}
{showInstanceManagement ? <SettingsSection
title={t('settings.remoteInstances.page.import.sectionTitle')}
>
{isImportsLoading ? (
<p className="typography-meta text-muted-foreground">{t('settings.remoteInstances.page.import.loading')}</p>
) : importCandidates.length === 0 ? (
<p className="typography-meta text-muted-foreground">{t('settings.remoteInstances.page.import.noneFound')}</p>
) : (
<div>
{importCandidates.map((candidate) => (
<div key={`${candidate.source}:${candidate.host}`} className="flex items-center justify-between gap-3 border-b border-[var(--surface-subtle)] py-3 last:border-b-0">
<div className="min-w-0">
<div className="typography-ui-label font-medium text-foreground truncate">
{candidate.host}
{candidate.pattern ? ` ${t('settings.remoteInstances.page.import.patternSuffix')}` : ''}
</div>
<div className="typography-meta text-muted-foreground truncate">{candidate.sshCommand}</div>
</div>
<Button
type="button"
variant="outline"
size="xs"
className="!font-normal"
onClick={() => void handleImportCandidate(candidate.host, candidate.pattern)}
>
{t('settings.common.actions.import')}
</Button>
</div>
))}
</div>
)}
</SettingsSection> : null}
<Dialog
open={Boolean(patternHost)}
onOpenChange={(open) => {
@@ -1925,6 +2130,10 @@ export const RemoteInstancesPage: React.FC = () => {
}
const isManagedMode = draft.remoteOpenchamber.mode === 'managed';
// Publishing the remote server to its network turns the UI password from an
// option into the only thing standing in front of it.
const remoteLanExposed = isManagedMode && draft.remoteOpenchamber.bindHost === '0.0.0.0';
const uiPasswordMissing = remoteLanExposed && !draft.auth.openchamberPassword?.value?.trim();
const instanceTitle = draft.nickname?.trim() || draft.sshParsed?.destination || draft.id;
return (
@@ -1934,7 +2143,8 @@ export const RemoteInstancesPage: React.FC = () => {
<h1 className={`${SETTINGS_PAGE_TITLE_CLASS} truncate`}>{instanceTitle}</h1>
<div className="mt-1 flex flex-wrap items-center gap-2 typography-meta text-muted-foreground">
<span className={`h-2.5 w-2.5 rounded-full ${phaseDotClass(statusPhase)}`} />
<span>{t(phaseLabelKey(statusPhase))}</span>
<span className="text-foreground">{t(instanceStateLabelKey(currentState))}</span>
{currentState === 'connecting' ? <span>{t(phaseLabelKey(statusPhase))}</span> : null}
{status?.localUrl ? <span className="font-mono text-foreground/80">{status.localUrl}</span> : null}
{reconnectAppearsStuck ? <span>{t('settings.remoteInstances.page.status.reconnectStale')}</span> : null}
</div>
@@ -2005,6 +2215,29 @@ export const RemoteInstancesPage: React.FC = () => {
{t('settings.remoteInstances.sidebar.actions.remove')}
</Button>
</div>
{currentState === 'error' && status?.detail ? (
<div className="space-y-2 rounded-md border border-[var(--status-error)]/30 bg-[var(--status-error-background)] p-3">
<p className="typography-meta text-[var(--status-error)] break-words">{status.detail}</p>
{currentRemedyHintKey ? (
<p className="typography-micro text-muted-foreground">{t(currentRemedyHintKey)}</p>
) : null}
{currentRemedy && !currentRemedyHintKey ? (
<Button
type="button"
variant="outline"
size="xs"
className="!font-normal"
onClick={() => void applyErrorRemedy(currentRemedy)}
>
{currentRemedy === 'uiPassword'
? t('settings.remoteInstances.page.error.action.setUiPassword')
: currentRemedy === 'localPort'
? t('settings.remoteInstances.page.error.action.pickRandomPort')
: t('settings.remoteInstances.page.error.action.setRemotePort')}
</Button>
) : null}
</div>
) : null}
{status?.localUrl ? (
<div className="flex flex-wrap items-center gap-2 typography-meta text-muted-foreground">
<span>{t('settings.remoteInstances.page.status.currentLocalUrl')}</span>
@@ -2046,30 +2279,6 @@ export const RemoteInstancesPage: React.FC = () => {
placeholder={t('settings.remoteInstances.page.field.nicknamePlaceholder')}
/>
</div>
<div className="flex flex-col gap-1.5 py-1.5 md:flex-row md:items-center md:gap-8">
<span className="typography-ui-label text-foreground w-56 shrink-0">{t('settings.remoteInstances.page.field.connectionTimeoutSeconds')}</span>
<NumberInput
containerClassName="w-fit"
min={5}
max={240}
step={1}
className="w-16 tabular-nums"
value={draft.connectionTimeoutSec}
onValueChange={(next) => {
updateDraft((current) => ({
...current,
connectionTimeoutSec: Number.isFinite(next) ? next : current.connectionTimeoutSec,
}));
}}
/>
</div>
</SettingsSection>
<SettingsSection
title={t('settings.remoteInstances.page.section.remoteServer')}
info={t('settings.remoteInstances.page.section.remoteServerDescription')}
contentClassName="space-y-3"
>
<div className="flex flex-col gap-1.5 py-1.5 md:flex-row md:items-center md:gap-8">
<div className="w-56 shrink-0">
<HintLabel
@@ -2099,8 +2308,40 @@ export const RemoteInstancesPage: React.FC = () => {
</Select>
</div>
</SettingsSection>
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
<CollapsibleTrigger className="mt-6 w-auto justify-start gap-1.5">
<span className={SETTINGS_SECTION_TITLE_CLASS}>{t('settings.remoteInstances.page.section.advanced')}</span>
<Icon name={advancedOpen ? 'arrow-up-s' : 'arrow-down-s'} className="h-4 w-4 text-muted-foreground" />
</CollapsibleTrigger>
<CollapsibleContent>
<p className="px-2 pb-2 typography-micro text-muted-foreground">{t('settings.remoteInstances.page.section.advancedHint')}</p>
<div className="flex flex-col gap-1.5 px-2 py-1.5 md:flex-row md:items-center md:gap-8">
<span className="typography-ui-label text-foreground w-56 shrink-0">{t('settings.remoteInstances.page.field.connectionTimeoutSeconds')}</span>
<NumberInput
containerClassName="w-fit"
min={5}
max={240}
step={1}
className="w-16 tabular-nums"
value={draft.connectionTimeoutSec}
onValueChange={(next) => {
updateDraft((current) => ({
...current,
connectionTimeoutSec: Number.isFinite(next) ? next : current.connectionTimeoutSec,
}));
}}
/>
</div>
<SettingsSection
title={t('settings.remoteInstances.page.section.remoteServer')}
info={t('settings.remoteInstances.page.section.remoteServerDescription')}
contentClassName="space-y-3"
>
<div className="flex flex-col gap-1.5 py-1.5 md:flex-row md:items-center md:gap-8">
<div className="w-56 shrink-0">
<div className="w-56 shrink-0" ref={remotePortRef}>
<HintLabel
label={t('settings.remoteInstances.page.field.preferredRemotePort')}
hint={t('settings.remoteInstances.page.field.preferredRemotePortHint')}
@@ -2150,10 +2391,7 @@ export const RemoteInstancesPage: React.FC = () => {
...current,
remoteOpenchamber: {
...current.remoteOpenchamber,
installMethod:
value === 'npm' || value === 'download_release' || value === 'upload_bundle'
? value
: 'bun',
installMethod: value === 'npm' || value === 'bun' ? value : 'auto',
},
}))
}
@@ -2162,15 +2400,45 @@ export const RemoteInstancesPage: React.FC = () => {
<SelectValue placeholder={t('settings.remoteInstances.page.field.selectInstallMethodPlaceholder')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">{t('settings.remoteInstances.page.field.installMethodAuto')}</SelectItem>
<SelectItem value="bun">bun</SelectItem>
<SelectItem value="npm">npm</SelectItem>
<SelectItem value="download_release">{t('settings.remoteInstances.page.field.installMethodDownloadRelease')}</SelectItem>
<SelectItem value="upload_bundle">{t('settings.remoteInstances.page.field.installMethodUploadBundle')}</SelectItem>
</SelectContent>
</Select>
</div>
) : null}
{isManagedMode ? (
<div className="py-1.5">
<div className="flex flex-col gap-1.5 md:flex-row md:items-center md:gap-8">
<div className="w-56 shrink-0">
<HintLabel
label={t('settings.remoteInstances.page.field.remoteLanAccess')}
hint={t('settings.remoteInstances.page.field.remoteLanAccessHint')}
/>
</div>
<Switch
checked={remoteLanExposed}
onCheckedChange={(checked) =>
updateDraft((current) => ({
...current,
remoteOpenchamber: {
...current.remoteOpenchamber,
bindHost: checked ? '0.0.0.0' : '127.0.0.1',
},
}))
}
aria-label={t('settings.remoteInstances.page.field.remoteLanAccess')}
/>
</div>
{remoteLanExposed ? (
<p className="mt-2 typography-micro text-[var(--status-warning)] md:pl-[16rem]">
{t('settings.remoteInstances.page.field.remoteLanAccessWarning')}
</p>
) : null}
</div>
) : null}
{isManagedMode ? (
<div className="flex flex-col gap-1.5 py-1.5 md:flex-row md:items-center md:gap-8">
<div className="w-56 shrink-0">
@@ -2227,13 +2495,13 @@ export const RemoteInstancesPage: React.FC = () => {
}));
}}
>
<SelectTrigger size={SETTINGS_SELECT_SIZE} className="w-fit min-w-[140px]">
<SelectTrigger size={SETTINGS_SELECT_SIZE} className="w-fit min-w-[240px]">
<SelectValue placeholder={t('settings.remoteInstances.page.field.selectBindHostPlaceholder')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="127.0.0.1">127.0.0.1</SelectItem>
<SelectItem value="localhost">localhost</SelectItem>
<SelectItem value="0.0.0.0">0.0.0.0</SelectItem>
<SelectItem value="127.0.0.1">{t('settings.remoteInstances.page.field.bindHostOption.loopback')}</SelectItem>
<SelectItem value="localhost">{t('settings.remoteInstances.page.field.bindHostOption.localhost')}</SelectItem>
<SelectItem value="0.0.0.0">{t('settings.remoteInstances.page.field.bindHostOption.lan')}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -2293,6 +2561,13 @@ export const RemoteInstancesPage: React.FC = () => {
</Button>
</div>
</div>
<div className="space-y-1 pt-1">
<p className="typography-micro text-muted-foreground">{t('settings.remoteInstances.page.tunnelPreview.caption')}</p>
<p className="typography-micro font-mono text-foreground/80 break-all">
{`${draft.localForward.bindHost}:${draft.localForward.preferredLocalPort || 'auto'}${draft.sshParsed?.destination || draft.nickname || 'remote'}:${draft.remoteOpenchamber.preferredPort || 'auto'}`}
</p>
</div>
</SettingsSection>
<SettingsSection
@@ -2301,7 +2576,12 @@ export const RemoteInstancesPage: React.FC = () => {
contentClassName="space-y-3"
>
<div className="flex flex-col gap-1.5 py-1.5 md:flex-row md:items-center md:gap-8">
<span className="typography-ui-label text-foreground w-56 shrink-0">{t('settings.remoteInstances.page.field.sshPasswordOptional')}</span>
<div className="w-56 shrink-0">
<HintLabel
label={t('settings.remoteInstances.page.field.sshPasswordOptional')}
hint={t('settings.remoteInstances.page.field.sshPasswordHint')}
/>
</div>
<Input
className="h-7 md:max-w-sm"
type="password"
@@ -2324,10 +2604,21 @@ export const RemoteInstancesPage: React.FC = () => {
</div>
<div className="flex flex-col gap-1.5 py-1.5 md:flex-row md:items-center md:gap-8">
<span className="typography-ui-label text-foreground w-56 shrink-0">{t('settings.remoteInstances.page.field.uiPasswordOptional')}</span>
<div className="w-56 shrink-0">
<HintLabel
label={remoteLanExposed
? t('settings.remoteInstances.page.field.uiPasswordRequired')
: t('settings.remoteInstances.page.field.uiPasswordOptional')}
hint={isManagedMode
? t('settings.remoteInstances.page.field.uiPasswordHintManaged')
: t('settings.remoteInstances.page.field.uiPasswordHintExternal')}
/>
</div>
<Input
className="h-7 md:max-w-sm"
className={cn('h-7 md:max-w-sm', uiPasswordMissing && 'border-[var(--status-error)]')}
type="password"
ref={uiPasswordRef}
aria-invalid={uiPasswordMissing}
value={draft.auth.openchamberPassword?.value || ''}
onChange={(event) =>
updateDraft((current) => ({
@@ -2345,6 +2636,11 @@ export const RemoteInstancesPage: React.FC = () => {
placeholder={t('settings.remoteInstances.page.field.uiPasswordPlaceholder')}
/>
</div>
{uiPasswordMissing ? (
<p className="typography-micro text-[var(--status-error)] md:pl-[16rem]">
{t('settings.remoteInstances.page.field.uiPasswordMissingForLan')}
</p>
) : null}
</SettingsSection>
<SettingsSection
@@ -2621,6 +2917,9 @@ export const RemoteInstancesPage: React.FC = () => {
</Button>
</SettingsSection>
</CollapsibleContent>
</Collapsible>
<div className="mt-8 border-t border-[var(--interactive-border)] pt-3">
<div className="flex items-center gap-2">
<Button type="button" size="xs" className="!font-normal" onClick={() => void handleSave()} disabled={!hasChanges || isSaving}>
+12 -7
View File
@@ -10,7 +10,7 @@ type DesktopBridgeGlobal = {
};
type DesktopSshRemoteMode = 'managed' | 'external';
type DesktopSshInstallMethod = 'npm' | 'bun' | 'download_release' | 'upload_bundle';
type DesktopSshInstallMethod = 'auto' | 'npm' | 'bun';
type DesktopSshSecretStore = 'never' | 'settings';
type DesktopSshStoredSecret = {
@@ -44,6 +44,8 @@ export type DesktopSshInstance = {
mode: DesktopSshRemoteMode;
keepRunning: boolean;
preferredPort?: number;
/** Interface the managed remote server listens on. '0.0.0.0' also exposes it to the remote machine's network. */
bindHost: '127.0.0.1' | '0.0.0.0';
installMethod: DesktopSshInstallMethod;
uploadBundleOverSsh: boolean;
};
@@ -197,12 +199,11 @@ const parseInstance = (value: unknown): DesktopSshInstance | null => {
const mode: DesktopSshRemoteMode = rawMode === 'external' ? 'external' : 'managed';
const rawInstallMethod = readString(remoteRaw, 'installMethod') || readString(remoteRaw, 'install_method');
// Legacy 'download_release'/'upload_bundle' never had their own remote path:
// they fell through to the same bun-then-npm attempt as 'auto'. Read them as
// 'auto' so the stored value matches what actually happens.
const installMethod: DesktopSshInstallMethod =
rawInstallMethod === 'npm' ||
rawInstallMethod === 'download_release' ||
rawInstallMethod === 'upload_bundle'
? rawInstallMethod
: 'bun';
rawInstallMethod === 'npm' || rawInstallMethod === 'bun' ? rawInstallMethod : 'auto';
const bindHostRaw =
readString(localRaw, 'bindHost') ||
@@ -222,6 +223,8 @@ const parseInstance = (value: unknown): DesktopSshInstance | null => {
.filter((item): item is DesktopSshPortForward => Boolean(item));
const preferredPort = readNumber(remoteRaw, 'preferredPort') ?? readNumber(remoteRaw, 'preferred_port');
const rawRemoteBindHost = readString(remoteRaw, 'bindHost') || readString(remoteRaw, 'bind_host');
const remoteBindHost: '127.0.0.1' | '0.0.0.0' = rawRemoteBindHost === '0.0.0.0' ? '0.0.0.0' : '127.0.0.1';
const preferredLocalPort =
readNumber(localRaw, 'preferredLocalPort') ?? readNumber(localRaw, 'preferred_local_port');
const sshPassword = parseStoredSecret(authRaw.sshPassword || authRaw.ssh_password);
@@ -239,6 +242,7 @@ const parseInstance = (value: unknown): DesktopSshInstance | null => {
remoteOpenchamber: {
mode,
keepRunning: readBoolean(remoteRaw, 'keepRunning') ?? readBoolean(remoteRaw, 'keep_running') ?? true,
bindHost: remoteBindHost,
...(preferredPort ? { preferredPort } : {}),
installMethod,
uploadBundleOverSsh:
@@ -327,7 +331,8 @@ export const createDesktopSshInstance = (id: string, sshCommand: string): Deskto
remoteOpenchamber: {
mode: 'managed',
keepRunning: true,
installMethod: 'bun',
bindHost: '127.0.0.1',
installMethod: 'auto',
uploadBundleOverSsh: false,
},
localForward: {
@@ -390,14 +390,14 @@ export const settingsDict = {
'settings.remoteInstances.page.field.modePlaceholder': 'Modus auswählen',
'settings.remoteInstances.page.field.modeManaged': 'Für mich starten',
'settings.remoteInstances.page.field.modeExternal': 'Läuft bereits',
'settings.remoteInstances.page.field.preferredRemotePort': 'Bevorzugter Remote-Port',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'Leer lassen für automatische Auswahl.',
'settings.remoteInstances.page.field.preferredRemotePort': 'Port auf dem entfernten Rechner',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'Port, den OpenChamber auf dem entfernten Rechner belegt. Leer lassen für eine automatische Wahl.',
'settings.remoteInstances.page.field.keepServerRunning': 'Server am Laufen halten',
'settings.remoteInstances.page.field.keepServerRunningHint': 'OpenChamber auf der Remote-Maschine weiterlaufen lassen nach Verbindungstrennung.',
'settings.remoteInstances.page.field.bindHost': 'Bind-Host',
'settings.remoteInstances.page.field.bindHostHint': 'Verwenden Sie 127.0.0.1 oder localhost, es sei denn, Sie benötigen LAN-Zugriff.',
'settings.remoteInstances.page.field.preferredLocalPort': 'Bevorzugter lokaler Port',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'Leer lassen für automatische Auswahl.',
'settings.remoteInstances.page.field.keepServerRunningHint': 'Den entfernten Server nach dem Trennen weiterlaufen lassen. Aus: Er wird beim Trennen gestoppt und beim nächsten Verbinden wieder gestartet.',
'settings.remoteInstances.page.field.bindHost': 'Erreichbar für',
'settings.remoteInstances.page.field.bindHostHint': 'Wer die weitergeleitete Adresse auf diesem Computer öffnen darf. Der entfernte Rechner selbst bleibt in beiden Fällen nur über den SSH-Tunnel erreichbar.',
'settings.remoteInstances.page.field.preferredLocalPort': 'Port auf diesem Computer',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'Port, der auf diesem Computer für den Tunnel geöffnet wird. Leer lassen für eine automatische Wahl.',
'settings.remoteInstances.page.field.forwardType': 'Weiterleitungstyp',
'settings.remoteInstances.page.field.localHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.remoteHostPlaceholder': '127.0.0.1',
@@ -1173,8 +1173,8 @@ export const settingsDict = {
'settings.remoteInstances.page.section.actionsDescription': 'Verbinden, erneut verbinden, Protokolle anzeigen oder diese Verbindung entfernen.',
'settings.remoteInstances.page.section.remoteServer': 'OpenChamber auf dem Remote-Rechner',
'settings.remoteInstances.page.section.remoteServerDescription': 'Wählen Sie aus, wie OpenChamber nach dem SSH-Verbindungsaufbau ausgeführt werden soll.',
'settings.remoteInstances.page.section.mainTunnel': 'Lokaler Zugriff',
'settings.remoteInstances.page.section.mainTunnelDescription': 'Wählen Sie die lokale Adresse, die zum Öffnen des Remote-OpenChamber-Servers verwendet wird.',
'settings.remoteInstances.page.section.mainTunnel': 'Zugriff von diesem Computer',
'settings.remoteInstances.page.section.mainTunnelDescription': 'OpenChamber läuft auf dem entfernten Rechner. Diese Einstellungen betreffen nur die Adresse auf diesem Computer, die per SSH-Tunnel dorthin führt.',
'settings.remoteInstances.page.section.authentication': 'Authentifizierung',
'settings.remoteInstances.page.section.authenticationDescription': 'Optionale Anmeldedaten für SSH und die Remote-OpenChamber-Benutzeroberfläche.',
'settings.remoteInstances.page.section.portForwards': 'Portweiterleitungen',
@@ -1187,8 +1187,6 @@ export const settingsDict = {
'settings.remoteInstances.page.field.installMethod': 'Installationsmethode',
'settings.remoteInstances.page.field.installMethodHint': 'Wie OpenChamber auf dem Remote-Rechner platziert werden soll, wenn diese Anwendung es für Sie startet.',
'settings.remoteInstances.page.field.selectInstallMethodPlaceholder': 'Installationsmethode auswählen',
'settings.remoteInstances.page.field.installMethodDownloadRelease': 'Release herunterladen',
'settings.remoteInstances.page.field.installMethodUploadBundle': 'Bundle hochladen',
'settings.remoteInstances.page.field.selectBindHostPlaceholder': 'Bind-Host auswählen',
'settings.remoteInstances.page.field.sshPasswordOptional': 'SSH-Passwort (optional)',
'settings.remoteInstances.page.field.sshPasswordPlaceholder': 'SSH-Passwort eingeben',
@@ -1212,7 +1210,44 @@ export const settingsDict = {
'settings.remoteInstances.page.actions.enableForwardAria': 'Weiterleitung aktivieren',
'settings.remoteInstances.page.actions.openLocal': 'Lokal öffnen',
'settings.remoteInstances.page.actions.addForward': 'Weiterleitung hinzufügen',
'settings.remoteInstances.page.import.sectionTitle': 'Gespeicherte SSH-Hosts',
'settings.remoteInstances.page.addDialog.description': 'Wähle einen Host aus deiner SSH-Konfiguration oder gib die Verbindung selbst ein.',
'settings.remoteInstances.page.addDialog.sourceLabel': 'Woher die Verbindung stammt',
'settings.remoteInstances.page.addDialog.tab.saved': 'Aus SSH-Konfiguration',
'settings.remoteInstances.page.addDialog.tab.manual': 'Selbst eingeben',
'settings.remoteInstances.page.addDialog.searchPlaceholder': 'Hosts suchen',
'settings.remoteInstances.page.addDialog.emptySaved': 'In deiner SSH-Konfiguration wurden keine Hosts gefunden. Gib die Verbindung stattdessen selbst ein.',
'settings.remoteInstances.page.addDialog.searchEmpty': 'Kein Host passt zu dieser Suche.',
'settings.remoteInstances.page.addDialog.use': 'Verwenden',
'settings.remoteInstances.page.state.notConnected': 'Nicht verbunden',
'settings.remoteInstances.page.state.connecting': 'Verbindung wird aufgebaut',
'settings.remoteInstances.page.state.ready': 'Verbunden',
'settings.remoteInstances.page.state.problem': 'Aktion erforderlich',
'settings.remoteInstances.page.section.advanced': 'Erweiterte Einstellungen',
'settings.remoteInstances.page.section.advancedHint': 'Ports, Installationsmethode, Passwörter und zusätzliche Weiterleitungen. Für die meisten Verbindungen genügen die Standardwerte.',
'settings.remoteInstances.page.field.installMethodAuto': 'Automatisch',
'settings.remoteInstances.page.error.hint.noRuntime': 'Auf dem entfernten Rechner gibt es weder bun noch npm. Installiere dort eines davon oder stelle diese Verbindung auf „Läuft bereits“ um.',
'settings.remoteInstances.page.error.hint.noOpencode': 'Auf dem entfernten Rechner ist die opencode-CLI nicht installiert. Installiere sie dort (siehe opencode.ai) und verbinde dich erneut.',
'settings.remoteInstances.page.error.action.setUiPassword': 'UI-Passwort festlegen',
'settings.remoteInstances.page.error.action.pickRandomPort': 'Anderen lokalen Port verwenden',
'settings.remoteInstances.page.error.action.setRemotePort': 'Entfernten Port festlegen',
'settings.remoteInstances.page.validation.externalPortRequired': 'Lege zuerst einen entfernten Port fest. Im Modus „Läuft bereits“ muss OpenChamber wissen, auf welchem Port der Server lauscht.',
'settings.remoteInstances.page.empty.noInstances': 'Noch keine SSH-Verbindungen.',
'settings.remoteInstances.page.field.uiPasswordRequired': 'UI-Passwort (erforderlich)',
'settings.remoteInstances.page.field.uiPasswordMissingForLan': 'Erforderlich, solange der entfernte Server in seinem Netzwerk erreichbar ist.',
'settings.remoteInstances.page.field.remoteLanAccess': 'Im Netzwerk des entfernten Rechners erreichbar',
'settings.remoteInstances.page.field.remoteLanAccessHint': 'Anderen Geräten im Netzwerk des entfernten Rechners erlauben, dieses OpenChamber direkt ohne SSH-Tunnel zu öffnen. Ein UI-Passwort ist erforderlich.',
'settings.remoteInstances.page.field.remoteLanAccessWarning': 'Jeder in diesem Netzwerk erreicht das entfernte OpenChamber. Es schützt nur das UI-Passwort unten.',
'settings.remoteInstances.page.validation.remoteLanNeedsPassword': 'Lege zuerst ein UI-Passwort fest. Ohne eines wäre das entfernte OpenChamber für jedes Gerät in diesem Netzwerk offen.',
'settings.remoteInstances.page.field.bindHostOption.loopback': 'Nur dieser Computer (127.0.0.1)',
'settings.remoteInstances.page.field.bindHostOption.localhost': 'Nur dieser Computer (localhost)',
'settings.remoteInstances.page.field.bindHostOption.lan': 'Jedes Gerät in meinem Netzwerk (0.0.0.0)',
'settings.remoteInstances.page.field.sshPasswordHint': 'Nur nötig, wenn dieser Host ein Passwort verlangt, statt einen SSH-Schlüssel zu akzeptieren.',
'settings.remoteInstances.page.field.uiPasswordHintManaged': 'Passwort, mit dem die entfernte OpenChamber-Oberfläche geschützt wird. OpenChamber setzt es auf dem Server, den es für dich startet.',
'settings.remoteInstances.page.field.uiPasswordHintExternal': 'Passwort des OpenChamber-Servers, der bereits auf dem entfernten Rechner läuft, für die Anmeldung.',
'settings.remoteInstances.page.tunnelPreview.caption': 'Diese Verbindung leitet weiter:',
'settings.remoteInstances.page.empty.noInstancesWithOneImport': 'Noch keine SSH-Verbindungen. Aus deiner SSH-Konfiguration lässt sich 1 Host importieren.',
'settings.remoteInstances.page.empty.noInstancesWithImports': 'Noch keine SSH-Verbindungen. Aus deiner SSH-Konfiguration lassen sich {count} Hosts importieren.',
'settings.remoteInstances.page.state.loadingInstances': 'Verbindungen werden geladen...',
'settings.remoteInstances.page.import.loading': 'SSH-Hosts werden geladen...',
'settings.remoteInstances.page.import.noneFound': 'Keine SSH-Hosts gefunden.',
'settings.remoteInstances.page.import.noneAvailable': 'Keine SSH-Hosts zum Importieren verfügbar.',
@@ -406,14 +406,14 @@ export const settingsDict = {
'settings.remoteInstances.page.field.modePlaceholder': 'Select mode',
'settings.remoteInstances.page.field.modeManaged': 'Start it for me',
'settings.remoteInstances.page.field.modeExternal': 'Already running',
'settings.remoteInstances.page.field.preferredRemotePort': 'Preferred remote port',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'Port to use on the remote machine. Leave empty to choose one automatically.',
'settings.remoteInstances.page.field.preferredRemotePort': 'Port on the remote machine',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'Port OpenChamber uses on the remote machine. Leave empty to choose one automatically.',
'settings.remoteInstances.page.field.keepServerRunning': 'Keep server running',
'settings.remoteInstances.page.field.keepServerRunningHint': 'Keep OpenChamber running on the remote machine after you disconnect.',
'settings.remoteInstances.page.field.bindHost': 'Bind host',
'settings.remoteInstances.page.field.bindHostHint': 'Where the local connection should listen. Use 127.0.0.1 or localhost unless you need LAN access.',
'settings.remoteInstances.page.field.preferredLocalPort': 'Preferred local port',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'Local port to open for this connection. Leave empty to choose one automatically.',
'settings.remoteInstances.page.field.keepServerRunningHint': 'Leave the remote server running after you disconnect. When off, it is stopped on disconnect and started again the next time you connect.',
'settings.remoteInstances.page.field.bindHost': 'Reachable from',
'settings.remoteInstances.page.field.bindHostHint': 'Who can open the forwarded address on this computer. The remote machine itself stays reachable only through the SSH tunnel either way.',
'settings.remoteInstances.page.field.preferredLocalPort': 'Port on this computer',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'Port opened on this computer for the tunnel. Leave empty to choose one automatically.',
'settings.remoteInstances.page.field.forwardType': 'Forward type',
'settings.remoteInstances.page.field.localHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.remoteHostPlaceholder': '127.0.0.1',
@@ -1235,8 +1235,8 @@ export const settingsDict = {
'settings.remoteInstances.page.section.actionsDescription': 'Connect, reconnect, view logs, or remove this connection.',
'settings.remoteInstances.page.section.remoteServer': 'OpenChamber on the remote machine',
'settings.remoteInstances.page.section.remoteServerDescription': 'Choose how OpenChamber should run after SSH connects.',
'settings.remoteInstances.page.section.mainTunnel': 'Local access',
'settings.remoteInstances.page.section.mainTunnelDescription': 'Choose the local address used to open this remote OpenChamber server.',
'settings.remoteInstances.page.section.mainTunnel': 'Access from this computer',
'settings.remoteInstances.page.section.mainTunnelDescription': 'OpenChamber runs on the remote machine. These settings only control the address on this computer that forwards to it through the SSH tunnel.',
'settings.remoteInstances.page.section.authentication': 'Authentication',
'settings.remoteInstances.page.section.authenticationDescription': 'Optional credentials for SSH and the remote OpenChamber UI.',
'settings.remoteInstances.page.section.portForwards': 'Port Forwards',
@@ -1249,8 +1249,6 @@ export const settingsDict = {
'settings.remoteInstances.page.field.installMethod': 'Install method',
'settings.remoteInstances.page.field.installMethodHint': 'How OpenChamber should be placed on the remote machine when this app starts it for you.',
'settings.remoteInstances.page.field.selectInstallMethodPlaceholder': 'Select install method',
'settings.remoteInstances.page.field.installMethodDownloadRelease': 'Download release',
'settings.remoteInstances.page.field.installMethodUploadBundle': 'Upload bundle',
'settings.remoteInstances.page.field.selectBindHostPlaceholder': 'Select bind host',
'settings.remoteInstances.page.field.sshPasswordOptional': 'SSH password (optional)',
'settings.remoteInstances.page.field.sshPasswordPlaceholder': 'Enter SSH password',
@@ -1274,7 +1272,44 @@ export const settingsDict = {
'settings.remoteInstances.page.actions.enableForwardAria': 'Enable forward',
'settings.remoteInstances.page.actions.openLocal': 'Open local',
'settings.remoteInstances.page.actions.addForward': 'Add forward',
'settings.remoteInstances.page.import.sectionTitle': 'Saved SSH hosts',
'settings.remoteInstances.page.addDialog.description': 'Pick a host from your SSH config, or type the connection yourself.',
'settings.remoteInstances.page.addDialog.sourceLabel': 'Where the connection comes from',
'settings.remoteInstances.page.addDialog.tab.saved': 'From SSH config',
'settings.remoteInstances.page.addDialog.tab.manual': 'Type it myself',
'settings.remoteInstances.page.addDialog.searchPlaceholder': 'Search hosts',
'settings.remoteInstances.page.addDialog.emptySaved': 'No hosts found in your SSH config. Type the connection yourself instead.',
'settings.remoteInstances.page.addDialog.searchEmpty': 'No host matches this search.',
'settings.remoteInstances.page.addDialog.use': 'Use',
'settings.remoteInstances.page.state.notConnected': 'Not connected',
'settings.remoteInstances.page.state.connecting': 'Connecting',
'settings.remoteInstances.page.state.ready': 'Connected',
'settings.remoteInstances.page.state.problem': 'Needs attention',
'settings.remoteInstances.page.section.advanced': 'Advanced settings',
'settings.remoteInstances.page.section.advancedHint': 'Ports, install method, passwords and extra forwards. The defaults work for most connections.',
'settings.remoteInstances.page.field.installMethodAuto': 'Automatic',
'settings.remoteInstances.page.error.hint.noRuntime': 'The remote machine has neither bun nor npm. Install one of them there, or switch this connection to "Already running".',
'settings.remoteInstances.page.error.hint.noOpencode': 'The opencode CLI is not installed on the remote machine. Install it there (see opencode.ai), then connect again.',
'settings.remoteInstances.page.error.action.setUiPassword': 'Set UI password',
'settings.remoteInstances.page.error.action.pickRandomPort': 'Use another local port',
'settings.remoteInstances.page.error.action.setRemotePort': 'Set the remote port',
'settings.remoteInstances.page.validation.externalPortRequired': 'Set a remote port first. In "Already running" mode OpenChamber needs to know which port the server listens on.',
'settings.remoteInstances.page.empty.noInstances': 'No SSH connections yet.',
'settings.remoteInstances.page.field.uiPasswordRequired': 'UI password (required)',
'settings.remoteInstances.page.field.uiPasswordMissingForLan': 'Required while the remote server is reachable on its network.',
'settings.remoteInstances.page.field.remoteLanAccess': 'Reachable on the remote network',
'settings.remoteInstances.page.field.remoteLanAccessHint': 'Also let other devices on the remote machines network open this OpenChamber directly, without the SSH tunnel. A UI password is required.',
'settings.remoteInstances.page.field.remoteLanAccessWarning': 'Anyone on that network can reach the remote OpenChamber. It is protected only by the UI password below.',
'settings.remoteInstances.page.validation.remoteLanNeedsPassword': 'Set a UI password first. Publishing the remote OpenChamber to its network without one would leave it open to every device there.',
'settings.remoteInstances.page.field.bindHostOption.loopback': 'Only this computer (127.0.0.1)',
'settings.remoteInstances.page.field.bindHostOption.localhost': 'Only this computer (localhost)',
'settings.remoteInstances.page.field.bindHostOption.lan': 'Any device on my network (0.0.0.0)',
'settings.remoteInstances.page.field.sshPasswordHint': 'Only needed when this host asks for a password instead of accepting an SSH key.',
'settings.remoteInstances.page.field.uiPasswordHintManaged': 'Password to protect the remote OpenChamber UI. OpenChamber sets it on the server it starts for you.',
'settings.remoteInstances.page.field.uiPasswordHintExternal': 'Password of the OpenChamber server already running on the remote machine, used to sign in to it.',
'settings.remoteInstances.page.tunnelPreview.caption': 'This connection forwards:',
'settings.remoteInstances.page.empty.noInstancesWithOneImport': 'No SSH connections yet. 1 host is available to import from your SSH config.',
'settings.remoteInstances.page.empty.noInstancesWithImports': 'No SSH connections yet. {count} hosts are available to import from your SSH config.',
'settings.remoteInstances.page.state.loadingInstances': 'Loading connections...',
'settings.remoteInstances.page.import.loading': 'Loading SSH hosts...',
'settings.remoteInstances.page.import.noneFound': 'No SSH hosts found.',
'settings.remoteInstances.page.import.noneAvailable': 'No SSH hosts available to import.',
@@ -374,14 +374,14 @@ export const settingsDict = {
"settings.remoteInstances.page.field.modePlaceholder": "Seleccionar modo",
"settings.remoteInstances.page.field.modeManaged": "Iniciarlo por mí",
"settings.remoteInstances.page.field.modeExternal": "Ya está en marcha",
"settings.remoteInstances.page.field.preferredRemotePort": "Puerto remoto preferido",
"settings.remoteInstances.page.field.preferredRemotePortHint": "Port to use on the remote machine. Leave empty to choose one automatically.",
"settings.remoteInstances.page.field.preferredRemotePort": "Puerto en la máquina remota",
"settings.remoteInstances.page.field.preferredRemotePortHint": "Puerto que OpenChamber usa en la máquina remota. Déjalo vacío para elegir uno automáticamente.",
"settings.remoteInstances.page.field.keepServerRunning": "Mantener servidor en ejecución",
"settings.remoteInstances.page.field.keepServerRunningHint": "Keep OpenChamber running on the remote machine after you disconnect.",
"settings.remoteInstances.page.field.bindHost": "Host de enlace",
"settings.remoteInstances.page.field.bindHostHint": "Where the local connection should listen. Use 127.0.0.1 or localhost unless you need LAN access.",
"settings.remoteInstances.page.field.preferredLocalPort": "Puerto local preferido",
"settings.remoteInstances.page.field.preferredLocalPortHint": "Local port to open for this connection. Leave empty to choose one automatically.",
"settings.remoteInstances.page.field.keepServerRunningHint": "Dejar el servidor remoto en marcha tras desconectar. Si está desactivado, se detiene al desconectar y se inicia de nuevo la próxima vez que conectes.",
"settings.remoteInstances.page.field.bindHost": "Quién puede acceder",
"settings.remoteInstances.page.field.bindHostHint": "Quién puede abrir la dirección reenviada en este equipo. La máquina remota sigue siendo accesible únicamente por el túnel SSH en cualquier caso.",
"settings.remoteInstances.page.field.preferredLocalPort": "Puerto en este equipo",
"settings.remoteInstances.page.field.preferredLocalPortHint": "Puerto que se abre en este equipo para el túnel. Déjalo vacío para elegir uno automáticamente.",
"settings.remoteInstances.page.field.forwardType": "Tipo de redirección",
"settings.remoteInstances.page.field.localHostPlaceholder": "127.0.0.1",
"settings.remoteInstances.page.field.remoteHostPlaceholder": "127.0.0.1",
@@ -1203,8 +1203,8 @@ export const settingsDict = {
"settings.remoteInstances.page.section.actionsDescription": "Conecta, reconecta, revisa registros o elimina esta conexión.",
"settings.remoteInstances.page.section.remoteServer": "OpenChamber en la máquina remota",
"settings.remoteInstances.page.section.remoteServerDescription": "Elige cómo debe ejecutarse OpenChamber después de conectar por SSH.",
"settings.remoteInstances.page.section.mainTunnel": "Acceso local",
"settings.remoteInstances.page.section.mainTunnelDescription": "Elige la dirección local que se usará para abrir este servidor remoto de OpenChamber.",
"settings.remoteInstances.page.section.mainTunnel": "Acceso desde este equipo",
"settings.remoteInstances.page.section.mainTunnelDescription": "OpenChamber se ejecuta en la máquina remota. Estos ajustes solo controlan la dirección de este equipo que lleva hasta ella por el túnel SSH.",
"settings.remoteInstances.page.section.authentication": "Autenticación",
"settings.remoteInstances.page.section.authenticationDescription": "Credenciales opcionales para SSH y la interfaz de usuario de OpenChamber remoto.",
"settings.remoteInstances.page.section.portForwards": "Redirecciones de puerto",
@@ -1217,8 +1217,6 @@ export const settingsDict = {
"settings.remoteInstances.page.field.installMethod": "Método de instalación",
"settings.remoteInstances.page.field.installMethodHint": "Cómo debe colocarse OpenChamber en la máquina remota cuando esta app lo inicia por ti.",
"settings.remoteInstances.page.field.selectInstallMethodPlaceholder": "Seleccionar método de instalación",
"settings.remoteInstances.page.field.installMethodDownloadRelease": "Descargar versión",
"settings.remoteInstances.page.field.installMethodUploadBundle": "Subir paquete",
"settings.remoteInstances.page.field.selectBindHostPlaceholder": "Seleccionar host de enlace",
"settings.remoteInstances.page.field.sshPasswordOptional": "Contraseña SSH (opcional)",
"settings.remoteInstances.page.field.sshPasswordPlaceholder": "Introducir contraseña SSH",
@@ -1242,7 +1240,44 @@ export const settingsDict = {
"settings.remoteInstances.page.actions.enableForwardAria": "Habilitar redirección",
"settings.remoteInstances.page.actions.openLocal": "Abrir local",
"settings.remoteInstances.page.actions.addForward": "Añadir redirección",
"settings.remoteInstances.page.import.sectionTitle": "Hosts SSH guardados",
"settings.remoteInstances.page.addDialog.description": "Elige un host de tu configuración SSH o escribe la conexión tú mismo.",
"settings.remoteInstances.page.addDialog.sourceLabel": "De dónde viene la conexión",
"settings.remoteInstances.page.addDialog.tab.saved": "Desde la configuración SSH",
"settings.remoteInstances.page.addDialog.tab.manual": "Escribirla yo",
"settings.remoteInstances.page.addDialog.searchPlaceholder": "Buscar hosts",
"settings.remoteInstances.page.addDialog.emptySaved": "No se encontraron hosts en tu configuración SSH. Escribe la conexión tú mismo.",
"settings.remoteInstances.page.addDialog.searchEmpty": "Ningún host coincide con esta búsqueda.",
"settings.remoteInstances.page.addDialog.use": "Usar",
"settings.remoteInstances.page.state.notConnected": "Sin conexión",
"settings.remoteInstances.page.state.connecting": "Conectando",
"settings.remoteInstances.page.state.ready": "Conectado",
"settings.remoteInstances.page.state.problem": "Requiere atención",
"settings.remoteInstances.page.section.advanced": "Ajustes avanzados",
"settings.remoteInstances.page.section.advancedHint": "Puertos, método de instalación, contraseñas y reenvíos adicionales. Los valores predeterminados sirven para casi todas las conexiones.",
"settings.remoteInstances.page.field.installMethodAuto": "Automático",
"settings.remoteInstances.page.error.hint.noRuntime": "La máquina remota no tiene ni bun ni npm. Instala uno de ellos allí o cambia esta conexión a «Ya está en ejecución».",
"settings.remoteInstances.page.error.hint.noOpencode": "La CLI de opencode no está instalada en la máquina remota. Instálala allí (consulta opencode.ai) y vuelve a conectar.",
"settings.remoteInstances.page.error.action.setUiPassword": "Definir contraseña de la interfaz",
"settings.remoteInstances.page.error.action.pickRandomPort": "Usar otro puerto local",
"settings.remoteInstances.page.error.action.setRemotePort": "Definir el puerto remoto",
"settings.remoteInstances.page.validation.externalPortRequired": "Primero indica un puerto remoto. En el modo «Ya está en ejecución», OpenChamber necesita saber en qué puerto escucha el servidor.",
"settings.remoteInstances.page.empty.noInstances": "Aún no hay conexiones SSH.",
"settings.remoteInstances.page.field.uiPasswordRequired": "Contraseña de la interfaz (obligatoria)",
"settings.remoteInstances.page.field.uiPasswordMissingForLan": "Obligatoria mientras el servidor remoto sea accesible en su red.",
"settings.remoteInstances.page.field.remoteLanAccess": "Accesible en la red remota",
"settings.remoteInstances.page.field.remoteLanAccessHint": "Permitir también que otros dispositivos de la red de la máquina remota abran este OpenChamber directamente, sin el túnel SSH. Requiere contraseña de la interfaz.",
"settings.remoteInstances.page.field.remoteLanAccessWarning": "Cualquiera en esa red puede llegar al OpenChamber remoto. Solo lo protege la contraseña de la interfaz de abajo.",
"settings.remoteInstances.page.validation.remoteLanNeedsPassword": "Define primero una contraseña de la interfaz. Sin ella, el OpenChamber remoto quedaría abierto a todos los dispositivos de esa red.",
"settings.remoteInstances.page.field.bindHostOption.loopback": "Solo este equipo (127.0.0.1)",
"settings.remoteInstances.page.field.bindHostOption.localhost": "Solo este equipo (localhost)",
"settings.remoteInstances.page.field.bindHostOption.lan": "Cualquier dispositivo de mi red (0.0.0.0)",
"settings.remoteInstances.page.field.sshPasswordHint": "Solo hace falta cuando este host pide contraseña en lugar de aceptar una clave SSH.",
"settings.remoteInstances.page.field.uiPasswordHintManaged": "Contraseña con la que se protegerá la interfaz remota de OpenChamber. OpenChamber la aplica al servidor que inicia por ti.",
"settings.remoteInstances.page.field.uiPasswordHintExternal": "Contraseña del servidor OpenChamber que ya se ejecuta en la máquina remota, usada para iniciar sesión.",
"settings.remoteInstances.page.tunnelPreview.caption": "Esta conexión reenvía:",
"settings.remoteInstances.page.empty.noInstancesWithOneImport": "Aún no hay conexiones SSH. Hay 1 host disponible para importar desde tu configuración SSH.",
"settings.remoteInstances.page.empty.noInstancesWithImports": "Aún no hay conexiones SSH. Hay {count} hosts disponibles para importar desde tu configuración SSH.",
"settings.remoteInstances.page.state.loadingInstances": "Cargando conexiones...",
"settings.remoteInstances.page.import.loading": "Cargando hosts SSH...",
"settings.remoteInstances.page.import.noneFound": "No se encontraron hosts SSH.",
"settings.remoteInstances.page.import.noneAvailable": "No hay hosts SSH disponibles para importar.",
@@ -297,14 +297,14 @@ export const settingsDict = {
'settings.remoteInstances.page.field.modePlaceholder': 'Sélectionnez le mode',
'settings.remoteInstances.page.field.modeManaged': 'Géré (démarrage automatique)',
'settings.remoteInstances.page.field.modeExternal': 'Externe (déjà en cours d\'exécution)',
'settings.remoteInstances.page.field.preferredRemotePort': 'Port distant préféré',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'Le port OpenChamber doit être utilisé sur l\'hôte distant. Laissez vide pour laisser le runtime choisir.',
'settings.remoteInstances.page.field.preferredRemotePort': 'Port sur la machine distante',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'Port utilisé par OpenChamber sur la machine distante. Laissez vide pour en choisir un automatiquement.',
'settings.remoteInstances.page.field.keepServerRunning': 'Maintenir le serveur en marche',
'settings.remoteInstances.page.field.keepServerRunningHint': 'S\'il est activé, le démon OpenChamber continue de s\'exécuter à distance lorsque vous vous déconnectez.',
'settings.remoteInstances.page.field.bindHost': 'Lier l\'hôte',
'settings.remoteInstances.page.field.bindHostHint': 'Interface réseau pour lURL locale principale. Utilisez 127.0.0.1/localhost pour un accès uniquement local.',
'settings.remoteInstances.page.field.preferredLocalPort': 'Port local préféré',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'Port local préféré pour le tunnel principal OpenChamber. Laissez vide pour la sélection automatique.',
'settings.remoteInstances.page.field.keepServerRunningHint': 'Laisser le serveur distant tourner après la déconnexion. Désactivé, il est arrêté à la déconnexion puis redémarré à la connexion suivante.',
'settings.remoteInstances.page.field.bindHost': 'Accessible depuis',
'settings.remoteInstances.page.field.bindHostHint': 'Qui peut ouvrir ladresse redirigée sur cet ordinateur. La machine distante reste de toute façon accessible uniquement par le tunnel SSH.',
'settings.remoteInstances.page.field.preferredLocalPort': 'Port sur cet ordinateur',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'Port ouvert sur cet ordinateur pour le tunnel. Laissez vide pour en choisir un automatiquement.',
'settings.remoteInstances.page.field.forwardType': 'Type de transfert',
'settings.remoteInstances.page.field.localHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.remoteHostPlaceholder': '127.0.0.1',
@@ -1121,8 +1121,8 @@ export const settingsDict = {
'settings.remoteInstances.page.section.actionsDescription': 'Connectez-vous, reconnectez-vous, inspectez les journaux ou supprimez cette instance.',
'settings.remoteInstances.page.section.remoteServer': 'Serveur distant',
'settings.remoteInstances.page.section.remoteServerDescription': 'Comment OpenChamber est géré et démarré sur l\'hôte distant.',
'settings.remoteInstances.page.section.mainTunnel': 'Tunnel principal',
'settings.remoteInstances.page.section.mainTunnelDescription': 'Point de terminaison local principal pour cette instance distante.',
'settings.remoteInstances.page.section.mainTunnel': 'Accès depuis cet ordinateur',
'settings.remoteInstances.page.section.mainTunnelDescription': 'OpenChamber tourne sur la machine distante. Ces réglages ne concernent que ladresse, sur cet ordinateur, qui y mène via le tunnel SSH.',
'settings.remoteInstances.page.section.authentication': 'Authentification',
'settings.remoteInstances.page.section.authenticationDescription': 'Informations d\'identification facultatives pour SSH et l\'interface utilisateur distante OpenChamber.',
'settings.remoteInstances.page.section.portForwards': 'Transferts de ports',
@@ -1135,8 +1135,6 @@ export const settingsDict = {
'settings.remoteInstances.page.field.installMethod': 'Méthode d\'installation',
'settings.remoteInstances.page.field.installMethodHint': 'Comment OpenChamber est installé lors de lexécution en mode géré.',
'settings.remoteInstances.page.field.selectInstallMethodPlaceholder': 'Sélectionnez la méthode d\'installation',
'settings.remoteInstances.page.field.installMethodDownloadRelease': 'Télécharger la version',
'settings.remoteInstances.page.field.installMethodUploadBundle': 'Télécharger le lot',
'settings.remoteInstances.page.field.selectBindHostPlaceholder': 'Sélectionnez l\'hôte de liaison',
'settings.remoteInstances.page.field.sshPasswordOptional': 'Mot de passe SSH (facultatif)',
'settings.remoteInstances.page.field.sshPasswordPlaceholder': 'Entrez le mot de passe SSH',
@@ -1160,7 +1158,44 @@ export const settingsDict = {
'settings.remoteInstances.page.actions.enableForwardAria': 'Activer le transfert',
'settings.remoteInstances.page.actions.openLocal': 'Ouvrir localement',
'settings.remoteInstances.page.actions.addForward': 'Ajouter en avant',
'settings.remoteInstances.page.import.sectionTitle': 'Importer depuis la configuration SSH',
'settings.remoteInstances.page.addDialog.description': 'Choisissez un hôte dans votre configuration SSH ou saisissez la connexion vous-même.',
'settings.remoteInstances.page.addDialog.sourceLabel': 'D\'où vient la connexion',
'settings.remoteInstances.page.addDialog.tab.saved': 'Depuis la configuration SSH',
'settings.remoteInstances.page.addDialog.tab.manual': 'Saisir moi-même',
'settings.remoteInstances.page.addDialog.searchPlaceholder': 'Rechercher des hôtes',
'settings.remoteInstances.page.addDialog.emptySaved': 'Aucun hôte trouvé dans votre configuration SSH. Saisissez plutôt la connexion vous-même.',
'settings.remoteInstances.page.addDialog.searchEmpty': 'Aucun hôte ne correspond à cette recherche.',
'settings.remoteInstances.page.addDialog.use': 'Utiliser',
'settings.remoteInstances.page.state.notConnected': 'Non connecté',
'settings.remoteInstances.page.state.connecting': 'Connexion en cours',
'settings.remoteInstances.page.state.ready': 'Connecté',
'settings.remoteInstances.page.state.problem': 'Action requise',
'settings.remoteInstances.page.section.advanced': 'Paramètres avancés',
'settings.remoteInstances.page.section.advancedHint': 'Ports, méthode dinstallation, mots de passe et redirections supplémentaires. Les valeurs par défaut conviennent à la plupart des connexions.',
'settings.remoteInstances.page.field.installMethodAuto': 'Automatique',
'settings.remoteInstances.page.error.hint.noRuntime': 'La machine distante na ni bun ni npm. Installez-en un là-bas, ou basculez cette connexion sur « Déjà en cours dexécution ».',
'settings.remoteInstances.page.error.hint.noOpencode': 'La CLI opencode nest pas installée sur la machine distante. Installez-la là-bas (voir opencode.ai), puis reconnectez-vous.',
'settings.remoteInstances.page.error.action.setUiPassword': 'Définir le mot de passe de linterface',
'settings.remoteInstances.page.error.action.pickRandomPort': 'Utiliser un autre port local',
'settings.remoteInstances.page.error.action.setRemotePort': 'Définir le port distant',
'settings.remoteInstances.page.validation.externalPortRequired': 'Indiquez dabord un port distant. En mode « Déjà en cours dexécution », OpenChamber doit savoir sur quel port le serveur écoute.',
'settings.remoteInstances.page.empty.noInstances': 'Aucune connexion SSH pour le moment.',
'settings.remoteInstances.page.field.uiPasswordRequired': 'Mot de passe dinterface (obligatoire)',
'settings.remoteInstances.page.field.uiPasswordMissingForLan': 'Obligatoire tant que le serveur distant est accessible sur son réseau.',
'settings.remoteInstances.page.field.remoteLanAccess': 'Accessible sur le réseau distant',
'settings.remoteInstances.page.field.remoteLanAccessHint': 'Autoriser aussi les autres appareils du réseau de la machine distante à ouvrir cet OpenChamber directement, sans le tunnel SSH. Un mot de passe dinterface est obligatoire.',
'settings.remoteInstances.page.field.remoteLanAccessWarning': 'Nimporte qui sur ce réseau peut atteindre lOpenChamber distant. Seul le mot de passe dinterface ci-dessous le protège.',
'settings.remoteInstances.page.validation.remoteLanNeedsPassword': 'Définissez dabord un mot de passe dinterface. Sans lui, lOpenChamber distant serait ouvert à tous les appareils de ce réseau.',
'settings.remoteInstances.page.field.bindHostOption.loopback': 'Cet ordinateur seulement (127.0.0.1)',
'settings.remoteInstances.page.field.bindHostOption.localhost': 'Cet ordinateur seulement (localhost)',
'settings.remoteInstances.page.field.bindHostOption.lan': 'Tout appareil de mon réseau (0.0.0.0)',
'settings.remoteInstances.page.field.sshPasswordHint': 'Nécessaire uniquement si cet hôte demande un mot de passe au lieu daccepter une clé SSH.',
'settings.remoteInstances.page.field.uiPasswordHintManaged': 'Mot de passe qui protégera linterface OpenChamber distante. OpenChamber lapplique au serveur quil démarre pour vous.',
'settings.remoteInstances.page.field.uiPasswordHintExternal': 'Mot de passe du serveur OpenChamber déjà en cours dexécution sur la machine distante, utilisé pour sy connecter.',
'settings.remoteInstances.page.tunnelPreview.caption': 'Cette connexion redirige :',
'settings.remoteInstances.page.empty.noInstancesWithOneImport': 'Aucune connexion SSH pour le moment. 1 hôte peut être importé depuis votre configuration SSH.',
'settings.remoteInstances.page.empty.noInstancesWithImports': 'Aucune connexion SSH pour le moment. {count} hôtes peuvent être importés depuis votre configuration SSH.',
'settings.remoteInstances.page.state.loadingInstances': 'Chargement des connexions...',
'settings.remoteInstances.page.import.loading': 'Chargement des hôtes SSH...',
'settings.remoteInstances.page.import.noneFound': 'Aucun hôte SSH trouvé.',
'settings.remoteInstances.page.import.noneAvailable': 'Aucun hôte SSH disponible pour l\'importation.',
@@ -407,14 +407,14 @@ export const settingsDict = {
'settings.remoteInstances.page.field.modePlaceholder': 'モードを選択',
'settings.remoteInstances.page.field.modeManaged': '自動起動',
'settings.remoteInstances.page.field.modeExternal': '既に実行中',
'settings.remoteInstances.page.field.preferredRemotePort': '優先リモートポート',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'リモートマシンで使用するポート。空の場合は自動的に選択されます。',
'settings.remoteInstances.page.field.preferredRemotePort': 'リモートマシンのポート',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'OpenChamber がリモートマシンで使ポート。空のままにすると自動で選ばれます。',
'settings.remoteInstances.page.field.keepServerRunning': 'サーバーを実行したままにする',
'settings.remoteInstances.page.field.keepServerRunningHint': '切断後もリモートマシンで OpenChamber を実行し続けます。',
'settings.remoteInstances.page.field.bindHost': 'バインドホスト',
'settings.remoteInstances.page.field.bindHostHint': 'ローカル接続の待受先。LAN アクセスが必要でない限り、127.0.0.1 または localhost を使用してください。',
'settings.remoteInstances.page.field.preferredLocalPort': '優先ローカルポート',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'この接続に使用するローカルポート。空の場合は自動的に選択されます。',
'settings.remoteInstances.page.field.keepServerRunningHint': '切断後もリモートサーバーを動かしたままにします。オフの場合は切断時に停止し、次の接続時に再び起動します。',
'settings.remoteInstances.page.field.bindHost': 'アクセスできる範囲',
'settings.remoteInstances.page.field.bindHostHint': 'このコンピュータの転送アドレスを誰が開けるか。リモートマシン自体は、どちらの場合も SSH トンネル経由でのみ到達できます。',
'settings.remoteInstances.page.field.preferredLocalPort': 'このコンピュータのポート',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'トンネル用にこのコンピュータで開くポート。空のままにすると自動で選ばれます。',
'settings.remoteInstances.page.field.forwardType': '転送タイプ',
'settings.remoteInstances.page.field.localHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.remoteHostPlaceholder': '127.0.0.1',
@@ -1236,8 +1236,8 @@ export const settingsDict = {
'settings.remoteInstances.page.section.actionsDescription': '接続、再接続、ログ表示、またはこの接続の削除。',
'settings.remoteInstances.page.section.remoteServer': 'リモートマシン上の OpenChamber',
'settings.remoteInstances.page.section.remoteServerDescription': 'SSH 接続後に OpenChamber をどのように実行するか選択します。',
'settings.remoteInstances.page.section.mainTunnel': 'ローカルアクセス',
'settings.remoteInstances.page.section.mainTunnelDescription': 'このリモート OpenChamber サーバーを開くために使用するローカルアドレスを選択します。',
'settings.remoteInstances.page.section.mainTunnel': 'このコンピュータからのアクセス',
'settings.remoteInstances.page.section.mainTunnelDescription': 'OpenChamber はリモートマシン上で動きます。ここで設定するのは、SSH トンネル経由でそこへつながる、このコンピュータ側のアドレスだけです。',
'settings.remoteInstances.page.section.authentication': '認証',
'settings.remoteInstances.page.section.authenticationDescription': 'SSH およびリモート OpenChamber UI のオプションの認証情報。',
'settings.remoteInstances.page.section.portForwards': 'ポート転送',
@@ -1250,8 +1250,6 @@ export const settingsDict = {
'settings.remoteInstances.page.field.installMethod': 'インストール方法',
'settings.remoteInstances.page.field.installMethodHint': 'このアプリがリモートマシンで OpenChamber を起動する際の配置方法。',
'settings.remoteInstances.page.field.selectInstallMethodPlaceholder': 'インストール方法を選択',
'settings.remoteInstances.page.field.installMethodDownloadRelease': 'リリースをダウンロード',
'settings.remoteInstances.page.field.installMethodUploadBundle': 'バンドルをアップロード',
'settings.remoteInstances.page.field.selectBindHostPlaceholder': 'バインドホストを選択',
'settings.remoteInstances.page.field.sshPasswordOptional': 'SSH パスワード(任意)',
'settings.remoteInstances.page.field.sshPasswordPlaceholder': 'SSH パスワードを入力',
@@ -1275,7 +1273,44 @@ export const settingsDict = {
'settings.remoteInstances.page.actions.enableForwardAria': '転送を有効化',
'settings.remoteInstances.page.actions.openLocal': 'ローカルを開く',
'settings.remoteInstances.page.actions.addForward': '転送を追加',
'settings.remoteInstances.page.import.sectionTitle': '保存された SSH ホスト',
'settings.remoteInstances.page.addDialog.description': 'SSH 設定からホストを選ぶか、接続を自分で入力します。',
'settings.remoteInstances.page.addDialog.sourceLabel': '接続の取得元',
'settings.remoteInstances.page.addDialog.tab.saved': 'SSH 設定から',
'settings.remoteInstances.page.addDialog.tab.manual': '自分で入力',
'settings.remoteInstances.page.addDialog.searchPlaceholder': 'ホストを検索',
'settings.remoteInstances.page.addDialog.emptySaved': 'SSH 設定にホストが見つかりません。接続を自分で入力してください。',
'settings.remoteInstances.page.addDialog.searchEmpty': '検索に一致するホストはありません。',
'settings.remoteInstances.page.addDialog.use': '使用',
'settings.remoteInstances.page.state.notConnected': '未接続',
'settings.remoteInstances.page.state.connecting': '接続中',
'settings.remoteInstances.page.state.ready': '接続済み',
'settings.remoteInstances.page.state.problem': '対応が必要',
'settings.remoteInstances.page.section.advanced': '詳細設定',
'settings.remoteInstances.page.section.advancedHint': 'ポート、インストール方法、パスワード、追加の転送。ほとんどの接続は初期値のままで動作します。',
'settings.remoteInstances.page.field.installMethodAuto': '自動',
'settings.remoteInstances.page.error.hint.noRuntime': 'リモートマシンに bun も npm もありません。どちらかをそこにインストールするか、この接続を「すでに実行中」に切り替えてください。',
'settings.remoteInstances.page.error.hint.noOpencode': 'リモートマシンに opencode CLI がインストールされていません。そこにインストールしてから(opencode.ai を参照)、もう一度接続してください。',
'settings.remoteInstances.page.error.action.setUiPassword': 'UI パスワードを設定',
'settings.remoteInstances.page.error.action.pickRandomPort': '別のローカルポートを使う',
'settings.remoteInstances.page.error.action.setRemotePort': 'リモートポートを設定',
'settings.remoteInstances.page.validation.externalPortRequired': '先にリモートポートを指定してください。「すでに実行中」モードでは、サーバーが待ち受けるポートを OpenChamber が知る必要があります。',
'settings.remoteInstances.page.empty.noInstances': 'SSH 接続はまだありません。',
'settings.remoteInstances.page.field.uiPasswordRequired': 'UI パスワード(必須)',
'settings.remoteInstances.page.field.uiPasswordMissingForLan': 'リモートサーバーがそのネットワークから到達可能な間は必須です。',
'settings.remoteInstances.page.field.remoteLanAccess': 'リモート側ネットワークから到達可能',
'settings.remoteInstances.page.field.remoteLanAccessHint': 'リモートマシンのネットワーク上の他の端末が、SSH トンネルなしでこの OpenChamber を直接開けるようにします。UI パスワードが必要です。',
'settings.remoteInstances.page.field.remoteLanAccessWarning': 'そのネットワーク上の誰もがリモートの OpenChamber に到達できます。守るのは下の UI パスワードだけです。',
'settings.remoteInstances.page.validation.remoteLanNeedsPassword': '先に UI パスワードを設定してください。設定しないと、リモートの OpenChamber はそのネットワークの全端末に開かれます。',
'settings.remoteInstances.page.field.bindHostOption.loopback': 'このコンピュータのみ (127.0.0.1)',
'settings.remoteInstances.page.field.bindHostOption.localhost': 'このコンピュータのみ (localhost)',
'settings.remoteInstances.page.field.bindHostOption.lan': 'ネットワーク上のすべての端末 (0.0.0.0)',
'settings.remoteInstances.page.field.sshPasswordHint': 'SSH 鍵ではなくパスワードを求めるホストの場合だけ必要です。',
'settings.remoteInstances.page.field.uiPasswordHintManaged': 'リモートの OpenChamber 画面を保護するパスワード。OpenChamber が起動するサーバーにこれを設定します。',
'settings.remoteInstances.page.field.uiPasswordHintExternal': 'リモートマシンですでに動いている OpenChamber サーバーにサインインするためのパスワード。',
'settings.remoteInstances.page.tunnelPreview.caption': 'この接続の転送:',
'settings.remoteInstances.page.empty.noInstancesWithOneImport': 'SSH 接続はまだありません。SSH 設定から 1 件のホストをインポートできます。',
'settings.remoteInstances.page.empty.noInstancesWithImports': 'SSH 接続はまだありません。SSH 設定から {count} 件のホストをインポートできます。',
'settings.remoteInstances.page.state.loadingInstances': '接続を読み込み中...',
'settings.remoteInstances.page.import.loading': 'SSH ホストを読み込み中...',
'settings.remoteInstances.page.import.noneFound': 'SSH ホストが見つかりません。',
'settings.remoteInstances.page.import.noneAvailable': 'インポート可能な SSH ホストがありません。',
@@ -374,14 +374,14 @@ export const settingsDict = {
'settings.remoteInstances.page.field.modePlaceholder': '모드 선택',
'settings.remoteInstances.page.field.modeManaged': '대신 시작하기',
'settings.remoteInstances.page.field.modeExternal': '이미 실행 중',
'settings.remoteInstances.page.field.preferredRemotePort': '기본 원격 포트',
'settings.remoteInstances.page.field.preferredRemotePortHint': '원격 컴퓨터에서 사용할 포트입니다. 비워 두면 자동으로 선택니다.',
'settings.remoteInstances.page.field.preferredRemotePort': '원격 머신의 포트',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'OpenChamber가 원격 머신에서 사용할 포트. 비워 두면 자동으로 선택니다.',
'settings.remoteInstances.page.field.keepServerRunning': '서버 유지',
'settings.remoteInstances.page.field.keepServerRunningHint': '연결을 끊은 뒤에도 원격 컴퓨터에서 OpenChamber를 계속 실행합니다.',
'settings.remoteInstances.page.field.bindHost': 'Bind host',
'settings.remoteInstances.page.field.bindHostHint': '로컬 연결이 대기할 주소입니다. LAN 접근이 필요하지 않으면 127.0.0.1 또는 localhost를 사용하세요.',
'settings.remoteInstances.page.field.preferredLocalPort': '기본 로컬 포트',
'settings.remoteInstances.page.field.preferredLocalPortHint': '이 연결에 열 로컬 포트입니다. 비워 두면 자동으로 선택니다.',
'settings.remoteInstances.page.field.keepServerRunningHint': '연결을 끊은 뒤에도 원격 서버를 계속 실행합니다. 끄면 연결 해제 시 중지되고 다음 연결 때 다시 시작됩니다.',
'settings.remoteInstances.page.field.bindHost': '접근 가능 범위',
'settings.remoteInstances.page.field.bindHostHint': '이 컴퓨터의 전달된 주소를 누가 열 수 있는지. 원격 머신 자체는 어느 경우든 SSH 터널로만 접근할 수 있습니다.',
'settings.remoteInstances.page.field.preferredLocalPort': '이 컴퓨터의 포트',
'settings.remoteInstances.page.field.preferredLocalPortHint': '터널을 위해 이 컴퓨터에서 여는 포트. 비워 두면 자동으로 선택니다.',
'settings.remoteInstances.page.field.forwardType': '포워딩 유형',
'settings.remoteInstances.page.field.localHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.remoteHostPlaceholder': '127.0.0.1',
@@ -1203,8 +1203,8 @@ export const settingsDict = {
'settings.remoteInstances.page.section.actionsDescription': '연결, 재연결, 로그 보기 또는 이 연결 삭제를 할 수 있습니다.',
'settings.remoteInstances.page.section.remoteServer': '원격 컴퓨터의 OpenChamber',
'settings.remoteInstances.page.section.remoteServerDescription': 'SSH 연결 후 OpenChamber를 어떻게 실행할지 선택하세요.',
'settings.remoteInstances.page.section.mainTunnel': '로컬 접근',
'settings.remoteInstances.page.section.mainTunnelDescription': '이 원격 OpenChamber 서버를 열 때 사용할 로컬 주소를 선택하세요.',
'settings.remoteInstances.page.section.mainTunnel': '이 컴퓨터에서의 접근',
'settings.remoteInstances.page.section.mainTunnelDescription': 'OpenChamber는 원격 머신에서 실행됩니다. 이 설정은 SSH 터널을 통해 그곳으로 연결되는 이 컴퓨터의 주소만 제어합니다.',
'settings.remoteInstances.page.section.authentication': '인증',
'settings.remoteInstances.page.section.authenticationDescription': 'SSH와 원격 OpenChamber UI를 위한 인증 정보입니다.',
'settings.remoteInstances.page.section.portForwards': '포트 포워딩',
@@ -1217,8 +1217,6 @@ export const settingsDict = {
'settings.remoteInstances.page.field.installMethod': '설치 방식',
'settings.remoteInstances.page.field.installMethodHint': '이 앱이 대신 시작할 때 OpenChamber를 원격 컴퓨터에 배치하는 방법입니다.',
'settings.remoteInstances.page.field.selectInstallMethodPlaceholder': '설치 방식 선택',
'settings.remoteInstances.page.field.installMethodDownloadRelease': '릴리스 다운로드',
'settings.remoteInstances.page.field.installMethodUploadBundle': '번들 업로드',
'settings.remoteInstances.page.field.selectBindHostPlaceholder': 'bind host 선택',
'settings.remoteInstances.page.field.sshPasswordOptional': 'SSH 비밀번호(선택 사항)',
'settings.remoteInstances.page.field.sshPasswordPlaceholder': 'SSH 비밀번호 입력',
@@ -1242,7 +1240,44 @@ export const settingsDict = {
'settings.remoteInstances.page.actions.enableForwardAria': '포워딩 활성화',
'settings.remoteInstances.page.actions.openLocal': '로컬 열기',
'settings.remoteInstances.page.actions.addForward': '포워딩 추가',
'settings.remoteInstances.page.import.sectionTitle': '저장된 SSH 호스트',
'settings.remoteInstances.page.addDialog.description': 'SSH 설정에서 호스트를 고르거나 연결을 직접 입력하세요.',
'settings.remoteInstances.page.addDialog.sourceLabel': '연결을 가져오는 위치',
'settings.remoteInstances.page.addDialog.tab.saved': 'SSH 설정에서',
'settings.remoteInstances.page.addDialog.tab.manual': '직접 입력',
'settings.remoteInstances.page.addDialog.searchPlaceholder': '호스트 검색',
'settings.remoteInstances.page.addDialog.emptySaved': 'SSH 설정에서 호스트를 찾지 못했습니다. 연결을 직접 입력하세요.',
'settings.remoteInstances.page.addDialog.searchEmpty': '검색과 일치하는 호스트가 없습니다.',
'settings.remoteInstances.page.addDialog.use': '사용',
'settings.remoteInstances.page.state.notConnected': '연결 안 됨',
'settings.remoteInstances.page.state.connecting': '연결 중',
'settings.remoteInstances.page.state.ready': '연결됨',
'settings.remoteInstances.page.state.problem': '조치 필요',
'settings.remoteInstances.page.section.advanced': '고급 설정',
'settings.remoteInstances.page.section.advancedHint': '포트, 설치 방법, 비밀번호, 추가 포워딩. 대부분의 연결은 기본값으로 충분합니다.',
'settings.remoteInstances.page.field.installMethodAuto': '자동',
'settings.remoteInstances.page.error.hint.noRuntime': '원격 머신에 bun도 npm도 없습니다. 그곳에 하나를 설치하거나 이 연결을 "이미 실행 중"으로 바꾸세요.',
'settings.remoteInstances.page.error.hint.noOpencode': '원격 머신에 opencode CLI가 설치되어 있지 않습니다. 그곳에 설치한 뒤(opencode.ai 참고) 다시 연결하세요.',
'settings.remoteInstances.page.error.action.setUiPassword': 'UI 비밀번호 설정',
'settings.remoteInstances.page.error.action.pickRandomPort': '다른 로컬 포트 사용',
'settings.remoteInstances.page.error.action.setRemotePort': '원격 포트 설정',
'settings.remoteInstances.page.validation.externalPortRequired': '먼저 원격 포트를 지정하세요. "이미 실행 중" 모드에서는 서버가 어떤 포트에서 대기하는지 OpenChamber가 알아야 합니다.',
'settings.remoteInstances.page.empty.noInstances': '아직 SSH 연결이 없습니다.',
'settings.remoteInstances.page.field.uiPasswordRequired': 'UI 비밀번호(필수)',
'settings.remoteInstances.page.field.uiPasswordMissingForLan': '원격 서버가 자기 네트워크에서 접근 가능한 동안에는 필수입니다.',
'settings.remoteInstances.page.field.remoteLanAccess': '원격 네트워크에서 접근 가능',
'settings.remoteInstances.page.field.remoteLanAccessHint': '원격 머신 네트워크의 다른 기기가 SSH 터널 없이 이 OpenChamber를 직접 열 수 있게 합니다. UI 비밀번호가 필요합니다.',
'settings.remoteInstances.page.field.remoteLanAccessWarning': '그 네트워크의 누구나 원격 OpenChamber에 접근할 수 있습니다. 아래 UI 비밀번호만이 이를 보호합니다.',
'settings.remoteInstances.page.validation.remoteLanNeedsPassword': '먼저 UI 비밀번호를 설정하세요. 없으면 원격 OpenChamber가 그 네트워크의 모든 기기에 열리게 됩니다.',
'settings.remoteInstances.page.field.bindHostOption.loopback': '이 컴퓨터만 (127.0.0.1)',
'settings.remoteInstances.page.field.bindHostOption.localhost': '이 컴퓨터만 (localhost)',
'settings.remoteInstances.page.field.bindHostOption.lan': '내 네트워크의 모든 기기 (0.0.0.0)',
'settings.remoteInstances.page.field.sshPasswordHint': 'SSH 키 대신 비밀번호를 요구하는 호스트에서만 필요합니다.',
'settings.remoteInstances.page.field.uiPasswordHintManaged': '원격 OpenChamber 화면을 보호할 비밀번호. OpenChamber가 대신 시작하는 서버에 이 값을 설정합니다.',
'settings.remoteInstances.page.field.uiPasswordHintExternal': '원격 머신에서 이미 실행 중인 OpenChamber 서버에 로그인할 때 쓰는 비밀번호.',
'settings.remoteInstances.page.tunnelPreview.caption': '이 연결의 전달 경로:',
'settings.remoteInstances.page.empty.noInstancesWithOneImport': '아직 SSH 연결이 없습니다. SSH 설정에서 호스트 1개를 가져올 수 있습니다.',
'settings.remoteInstances.page.empty.noInstancesWithImports': '아직 SSH 연결이 없습니다. SSH 설정에서 호스트 {count}개를 가져올 수 있습니다.',
'settings.remoteInstances.page.state.loadingInstances': '연결을 불러오는 중...',
'settings.remoteInstances.page.import.loading': 'SSH host 로딩 중...',
'settings.remoteInstances.page.import.noneFound': 'SSH host를 찾을 수 없습니다.',
'settings.remoteInstances.page.import.noneAvailable': '가져올 수 있는 SSH host가 없습니다.',
@@ -1548,17 +1548,15 @@ export const settingsDict = {
'settings.remoteInstances.page.empty.noExtraForwards': 'Nie skonfigurowano dodatkowych przekierowań portów.',
'settings.remoteInstances.page.empty.selectInstance': 'Wybierz instancję, aby wyświetlić i edytować jej ustawienia.',
'settings.remoteInstances.page.field.auto': 'Auto',
'settings.remoteInstances.page.field.bindHost': 'Host powiązania',
'settings.remoteInstances.page.field.bindHostHint': 'Miejsce nasłuchiwania lokalnego połączenia. Użyj 127.0.0.1 lub localhost, chyba że potrzebujesz dostępu z sieci lokalnej.',
'settings.remoteInstances.page.field.bindHost': 'Dostępne dla',
'settings.remoteInstances.page.field.bindHostHint': 'Kto może otworzyć przekierowany adres na tym komputerze. Sama zdalna maszyna i tak pozostaje dostępna tylko przez tunel SSH.',
'settings.remoteInstances.page.field.connectionTimeoutSeconds': 'Limit czasu połączenia (sekundy)',
'settings.remoteInstances.page.field.forwardType': 'Typ przekierowania',
'settings.remoteInstances.page.field.forwardTypeHint': 'Wybierz, jaki dostęp do portów ma zapewniać to połączenie SSH.',
'settings.remoteInstances.page.field.installMethod': 'Metoda instalacji',
'settings.remoteInstances.page.field.installMethodDownloadRelease': 'Pobierz wydanie',
'settings.remoteInstances.page.field.installMethodHint': 'Jak OpenChamber ma zostać umieszczony na zdalnej maszynie, gdy aplikacja uruchamia go za Ciebie.',
'settings.remoteInstances.page.field.installMethodUploadBundle': 'Prześlij paczkę',
'settings.remoteInstances.page.field.keepServerRunning': 'Pozostaw serwer uruchomiony',
'settings.remoteInstances.page.field.keepServerRunningHint': 'Pozostaw OpenChamber uruchomiony na zdalnej maszynie po rozłączeniu.',
'settings.remoteInstances.page.field.keepServerRunningHint': 'Pozostaw zdalny serwer uruchomiony po rozłączeniu. Wyłączone: zatrzymuje się przy rozłączeniu i startuje ponownie przy kolejnym połączeniu.',
'settings.remoteInstances.page.field.localHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.mode': 'Tryb',
'settings.remoteInstances.page.field.modeExternal': 'Już działa',
@@ -1567,10 +1565,10 @@ export const settingsDict = {
'settings.remoteInstances.page.field.modePlaceholder': 'Wybierz tryb',
'settings.remoteInstances.page.field.nickname': 'Pseudonim',
'settings.remoteInstances.page.field.nicknamePlaceholder': 'Laptop służbowy',
'settings.remoteInstances.page.field.preferredLocalPort': 'Preferowany port lokalny',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'Lokalny port dla tego połączenia. Zostaw puste, aby wybrać automatycznie.',
'settings.remoteInstances.page.field.preferredRemotePort': 'Preferowany port zdalny',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'Port używany na zdalnej maszynie. Zostaw puste, aby wybrać automatycznie.',
'settings.remoteInstances.page.field.preferredLocalPort': 'Port na tym komputerze',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'Port otwierany na tym komputerze dla tunelu. Zostaw puste, aby wybrać automatycznie.',
'settings.remoteInstances.page.field.preferredRemotePort': 'Port na zdalnej maszynie',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'Port, którego OpenChamber używa na zdalnej maszynie. Zostaw puste, aby wybrać automatycznie.',
'settings.remoteInstances.page.field.remoteHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.selectBindHostPlaceholder': 'Wybierz host powiązania',
'settings.remoteInstances.page.field.selectInstallMethodPlaceholder': 'Wybierz metodę instalacji',
@@ -1587,11 +1585,48 @@ export const settingsDict = {
'settings.remoteInstances.page.forwardTypeDescription.dynamic': 'Otwórz lokalny proxy SOCKS przez połączenie SSH.',
'settings.remoteInstances.page.forwardTypeDescription.local': 'Otwórz lokalny port łączący się z usługą na zdalnej maszynie.',
'settings.remoteInstances.page.forwardTypeDescription.remote': 'Otwórz port na zdalnej maszynie, który połączy się z powrotem z Twoim komputerem.',
'settings.remoteInstances.page.addDialog.description': 'Wybierz host z konfiguracji SSH albo wpisz połączenie samodzielnie.',
'settings.remoteInstances.page.addDialog.sourceLabel': 'Skąd pochodzi połączenie',
'settings.remoteInstances.page.addDialog.tab.saved': 'Z konfiguracji SSH',
'settings.remoteInstances.page.addDialog.tab.manual': 'Wpiszę sam',
'settings.remoteInstances.page.addDialog.searchPlaceholder': 'Szukaj hostów',
'settings.remoteInstances.page.addDialog.emptySaved': 'Nie znaleziono hostów w konfiguracji SSH. Wpisz połączenie samodzielnie.',
'settings.remoteInstances.page.addDialog.searchEmpty': 'Żaden host nie pasuje do tego wyszukiwania.',
'settings.remoteInstances.page.addDialog.use': 'Użyj',
'settings.remoteInstances.page.state.notConnected': 'Brak połączenia',
'settings.remoteInstances.page.state.connecting': 'Łączenie',
'settings.remoteInstances.page.state.ready': 'Połączono',
'settings.remoteInstances.page.state.problem': 'Wymaga uwagi',
'settings.remoteInstances.page.section.advanced': 'Ustawienia zaawansowane',
'settings.remoteInstances.page.section.advancedHint': 'Porty, metoda instalacji, hasła i dodatkowe przekierowania. Domyślne wartości wystarczą dla większości połączeń.',
'settings.remoteInstances.page.field.installMethodAuto': 'Automatycznie',
'settings.remoteInstances.page.error.hint.noRuntime': 'Na zdalnej maszynie nie ma ani bun, ani npm. Zainstaluj tam jedno z nich albo przełącz to połączenie na „Już uruchomiony”.',
'settings.remoteInstances.page.error.hint.noOpencode': 'Na zdalnej maszynie nie ma zainstalowanego opencode CLI. Zainstaluj je tam (zobacz opencode.ai) i połącz się ponownie.',
'settings.remoteInstances.page.error.action.setUiPassword': 'Ustaw hasło interfejsu',
'settings.remoteInstances.page.error.action.pickRandomPort': 'Użyj innego portu lokalnego',
'settings.remoteInstances.page.error.action.setRemotePort': 'Ustaw port zdalny',
'settings.remoteInstances.page.validation.externalPortRequired': 'Najpierw podaj port zdalny. W trybie „Już uruchomiony” OpenChamber musi wiedzieć, na którym porcie nasłuchuje serwer.',
'settings.remoteInstances.page.empty.noInstances': 'Brak połączeń SSH.',
'settings.remoteInstances.page.field.uiPasswordRequired': 'Hasło interfejsu (wymagane)',
'settings.remoteInstances.page.field.uiPasswordMissingForLan': 'Wymagane, dopóki zdalny serwer jest dostępny w swojej sieci.',
'settings.remoteInstances.page.field.remoteLanAccess': 'Dostępne w sieci zdalnej maszyny',
'settings.remoteInstances.page.field.remoteLanAccessHint': 'Pozwól innym urządzeniom w sieci zdalnej maszyny otwierać ten OpenChamber bezpośrednio, bez tunelu SSH. Wymagane jest hasło interfejsu.',
'settings.remoteInstances.page.field.remoteLanAccessWarning': 'Każdy w tej sieci dotrze do zdalnego OpenChamber. Chroni go tylko hasło interfejsu poniżej.',
'settings.remoteInstances.page.validation.remoteLanNeedsPassword': 'Najpierw ustaw hasło interfejsu. Bez niego zdalny OpenChamber byłby otwarty dla każdego urządzenia w tej sieci.',
'settings.remoteInstances.page.field.bindHostOption.loopback': 'Tylko ten komputer (127.0.0.1)',
'settings.remoteInstances.page.field.bindHostOption.localhost': 'Tylko ten komputer (localhost)',
'settings.remoteInstances.page.field.bindHostOption.lan': 'Dowolne urządzenie w mojej sieci (0.0.0.0)',
'settings.remoteInstances.page.field.sshPasswordHint': 'Potrzebne tylko wtedy, gdy host prosi o hasło zamiast przyjąć klucz SSH.',
'settings.remoteInstances.page.field.uiPasswordHintManaged': 'Hasło chroniące zdalny interfejs OpenChamber. OpenChamber ustawia je na serwerze, który uruchamia za Ciebie.',
'settings.remoteInstances.page.field.uiPasswordHintExternal': 'Hasło serwera OpenChamber już działającego na zdalnej maszynie, używane do zalogowania się.',
'settings.remoteInstances.page.tunnelPreview.caption': 'To połączenie przekierowuje:',
'settings.remoteInstances.page.empty.noInstancesWithOneImport': 'Brak połączeń SSH. Z konfiguracji SSH można zaimportować 1 host.',
'settings.remoteInstances.page.empty.noInstancesWithImports': 'Brak połączeń SSH. Z konfiguracji SSH można zaimportować {count} hostów.',
'settings.remoteInstances.page.state.loadingInstances': 'Wczytywanie połączeń...',
'settings.remoteInstances.page.import.loading': 'Ładowanie hostów SSH...',
'settings.remoteInstances.page.import.noneAvailable': 'Brak hostów SSH dostępnych do importu.',
'settings.remoteInstances.page.import.noneFound': 'Nie znaleziono hostów SSH.',
'settings.remoteInstances.page.import.patternSuffix': '(wzorzec)',
'settings.remoteInstances.page.import.sectionTitle': 'Zapisane hosty SSH',
'settings.remoteInstances.page.logsDialog.empty': 'Brak logów SSH.',
'settings.remoteInstances.page.logsDialog.loading': 'Ładowanie logów...',
'settings.remoteInstances.page.logsDialog.selectedInstanceFallback': 'Wybrana instancja',
@@ -1619,8 +1654,8 @@ export const settingsDict = {
'settings.remoteInstances.page.section.authenticationDescription': 'Opcjonalne dane logowania dla SSH i zdalnego interfejsu OpenChamber.',
'settings.remoteInstances.page.section.instance': 'Instancja',
'settings.remoteInstances.page.section.instanceDescription': 'Wybierz polecenie SSH i nazwę wyświetlaną dla tego połączenia.',
'settings.remoteInstances.page.section.mainTunnel': 'Dostęp lokalny',
'settings.remoteInstances.page.section.mainTunnelDescription': 'Wybierz lokalny adres używany do otwierania tego zdalnego serwera OpenChamber.',
'settings.remoteInstances.page.section.mainTunnel': 'Dostęp z tego komputera',
'settings.remoteInstances.page.section.mainTunnelDescription': 'OpenChamber działa na zdalnej maszynie. Te ustawienia dotyczą wyłącznie adresu na tym komputerze, który prowadzi do niej przez tunel SSH.',
'settings.remoteInstances.page.section.portForwards': 'Przekierowania portów',
'settings.remoteInstances.page.section.portForwardsDescription': 'Opcjonalne dodatkowe porty udostępniane przez to połączenie SSH.',
'settings.remoteInstances.page.section.remoteServer': 'OpenChamber na zdalnej maszynie',
@@ -374,14 +374,14 @@ export const settingsDict = {
"settings.remoteInstances.page.field.modePlaceholder": "Selecionar modo",
"settings.remoteInstances.page.field.modeManaged": "Iniciar para mim",
"settings.remoteInstances.page.field.modeExternal": "Já está em execução",
"settings.remoteInstances.page.field.preferredRemotePort": "Porta remoto preferido",
"settings.remoteInstances.page.field.preferredRemotePortHint": "Port to use on the remote machine. Leave empty to choose one automatically.",
"settings.remoteInstances.page.field.preferredRemotePort": "Porta na máquina remota",
"settings.remoteInstances.page.field.preferredRemotePortHint": "Porta que o OpenChamber usa na máquina remota. Deixe vazio para escolher automaticamente.",
"settings.remoteInstances.page.field.keepServerRunning": "Manter servidor em execução",
"settings.remoteInstances.page.field.keepServerRunningHint": "Keep OpenChamber running on the remote machine after you disconnect.",
"settings.remoteInstances.page.field.bindHost": "Host de link",
"settings.remoteInstances.page.field.bindHostHint": "Where the local connection should listen. Use 127.0.0.1 or localhost unless you need LAN access.",
"settings.remoteInstances.page.field.preferredLocalPort": "Porta local preferido",
"settings.remoteInstances.page.field.preferredLocalPortHint": "Local port to open for this connection. Leave empty to choose one automatically.",
"settings.remoteInstances.page.field.keepServerRunningHint": "Manter o servidor remoto rodando depois de desconectar. Desligado, ele para ao desconectar e sobe de novo na próxima conexão.",
"settings.remoteInstances.page.field.bindHost": "Quem pode acessar",
"settings.remoteInstances.page.field.bindHostHint": "Quem pode abrir o endereço encaminhado neste computador. A máquina remota continua acessível somente pelo túnel SSH nos dois casos.",
"settings.remoteInstances.page.field.preferredLocalPort": "Porta neste computador",
"settings.remoteInstances.page.field.preferredLocalPortHint": "Porta aberta neste computador para o túnel. Deixe vazio para escolher automaticamente.",
"settings.remoteInstances.page.field.forwardType": "Tipo de encaminhamento",
"settings.remoteInstances.page.field.localHostPlaceholder": "127.0.0.1",
"settings.remoteInstances.page.field.remoteHostPlaceholder": "127.0.0.1",
@@ -1203,8 +1203,8 @@ export const settingsDict = {
"settings.remoteInstances.page.section.actionsDescription": "Conecte, reconecte, veja logs ou remova esta conexão.",
"settings.remoteInstances.page.section.remoteServer": "OpenChamber na máquina remota",
"settings.remoteInstances.page.section.remoteServerDescription": "Escolha como o OpenChamber deve rodar depois que o SSH conectar.",
"settings.remoteInstances.page.section.mainTunnel": "Acesso local",
"settings.remoteInstances.page.section.mainTunnelDescription": "Escolha o endereço local usado para abrir este servidor OpenChamber remoto.",
"settings.remoteInstances.page.section.mainTunnel": "Acesso a partir deste computador",
"settings.remoteInstances.page.section.mainTunnelDescription": "O OpenChamber roda na máquina remota. Estas opções controlam apenas o endereço neste computador que leva até ela pelo túnel SSH.",
"settings.remoteInstances.page.section.authentication": "Autenticação",
"settings.remoteInstances.page.section.authenticationDescription": "Credenciais opcionais para SSH e para a interface de usuário do OpenChamber remoto.",
"settings.remoteInstances.page.section.portForwards": "Redeirecciones de porta",
@@ -1217,8 +1217,6 @@ export const settingsDict = {
"settings.remoteInstances.page.field.installMethod": "Método de instalação",
"settings.remoteInstances.page.field.installMethodHint": "Como o OpenChamber deve ser colocado na máquina remota quando este app o inicia para você.",
"settings.remoteInstances.page.field.selectInstallMethodPlaceholder": "Selecionar método de instalação",
"settings.remoteInstances.page.field.installMethodDownloadRelease": "Baixar versão",
"settings.remoteInstances.page.field.installMethodUploadBundle": "Enviar paquete",
"settings.remoteInstances.page.field.selectBindHostPlaceholder": "Selecionar host de link",
"settings.remoteInstances.page.field.sshPasswordOptional": "Senha SSH (opcional)",
"settings.remoteInstances.page.field.sshPasswordPlaceholder": "Introducir senha SSH",
@@ -1242,7 +1240,44 @@ export const settingsDict = {
"settings.remoteInstances.page.actions.enableForwardAria": "Ativar redeirección",
"settings.remoteInstances.page.actions.openLocal": "Abrir local",
"settings.remoteInstances.page.actions.addForward": "Adicionar redeirección",
"settings.remoteInstances.page.import.sectionTitle": "Hosts SSH salvos",
"settings.remoteInstances.page.addDialog.description": "Escolha um host da sua configuração SSH ou digite a conexão você mesmo.",
"settings.remoteInstances.page.addDialog.sourceLabel": "De onde vem a conexão",
"settings.remoteInstances.page.addDialog.tab.saved": "Da configuração SSH",
"settings.remoteInstances.page.addDialog.tab.manual": "Digitar eu mesmo",
"settings.remoteInstances.page.addDialog.searchPlaceholder": "Buscar hosts",
"settings.remoteInstances.page.addDialog.emptySaved": "Nenhum host encontrado na sua configuração SSH. Digite a conexão você mesmo.",
"settings.remoteInstances.page.addDialog.searchEmpty": "Nenhum host corresponde a esta busca.",
"settings.remoteInstances.page.addDialog.use": "Usar",
"settings.remoteInstances.page.state.notConnected": "Sem conexão",
"settings.remoteInstances.page.state.connecting": "Conectando",
"settings.remoteInstances.page.state.ready": "Conectado",
"settings.remoteInstances.page.state.problem": "Precisa de atenção",
"settings.remoteInstances.page.section.advanced": "Configurações avançadas",
"settings.remoteInstances.page.section.advancedHint": "Portas, método de instalação, senhas e encaminhamentos extras. Os padrões servem para quase todas as conexões.",
"settings.remoteInstances.page.field.installMethodAuto": "Automático",
"settings.remoteInstances.page.error.hint.noRuntime": "A máquina remota não tem bun nem npm. Instale um deles lá ou mude esta conexão para “Já em execução”.",
"settings.remoteInstances.page.error.hint.noOpencode": "A CLI do opencode não está instalada na máquina remota. Instale-a lá (veja opencode.ai) e conecte novamente.",
"settings.remoteInstances.page.error.action.setUiPassword": "Definir senha da interface",
"settings.remoteInstances.page.error.action.pickRandomPort": "Usar outra porta local",
"settings.remoteInstances.page.error.action.setRemotePort": "Definir a porta remota",
"settings.remoteInstances.page.validation.externalPortRequired": "Defina primeiro uma porta remota. No modo “Já em execução”, o OpenChamber precisa saber em qual porta o servidor escuta.",
"settings.remoteInstances.page.empty.noInstances": "Ainda não há conexões SSH.",
"settings.remoteInstances.page.field.uiPasswordRequired": "Senha da interface (obrigatória)",
"settings.remoteInstances.page.field.uiPasswordMissingForLan": "Obrigatória enquanto o servidor remoto estiver acessível na rede dele.",
"settings.remoteInstances.page.field.remoteLanAccess": "Acessível na rede remota",
"settings.remoteInstances.page.field.remoteLanAccessHint": "Permitir que outros dispositivos da rede da máquina remota abram este OpenChamber diretamente, sem o túnel SSH. É obrigatória uma senha da interface.",
"settings.remoteInstances.page.field.remoteLanAccessWarning": "Qualquer pessoa nessa rede alcança o OpenChamber remoto. Só a senha da interface abaixo o protege.",
"settings.remoteInstances.page.validation.remoteLanNeedsPassword": "Defina primeiro uma senha da interface. Sem ela, o OpenChamber remoto ficaria aberto a todos os dispositivos daquela rede.",
"settings.remoteInstances.page.field.bindHostOption.loopback": "Somente este computador (127.0.0.1)",
"settings.remoteInstances.page.field.bindHostOption.localhost": "Somente este computador (localhost)",
"settings.remoteInstances.page.field.bindHostOption.lan": "Qualquer dispositivo da minha rede (0.0.0.0)",
"settings.remoteInstances.page.field.sshPasswordHint": "Só é necessária quando este host pede senha em vez de aceitar uma chave SSH.",
"settings.remoteInstances.page.field.uiPasswordHintManaged": "Senha que protegerá a interface remota do OpenChamber. O OpenChamber a define no servidor que inicia para você.",
"settings.remoteInstances.page.field.uiPasswordHintExternal": "Senha do servidor OpenChamber que já está em execução na máquina remota, usada para entrar nele.",
"settings.remoteInstances.page.tunnelPreview.caption": "Esta conexão encaminha:",
"settings.remoteInstances.page.empty.noInstancesWithOneImport": "Ainda não há conexões SSH. Há 1 host disponível para importar da sua configuração SSH.",
"settings.remoteInstances.page.empty.noInstancesWithImports": "Ainda não há conexões SSH. Há {count} hosts disponíveis para importar da sua configuração SSH.",
"settings.remoteInstances.page.state.loadingInstances": "Carregando conexões...",
"settings.remoteInstances.page.import.loading": "Carregando hosts SSH...",
"settings.remoteInstances.page.import.noneFound": "Nenhum host SSH encontrado.",
"settings.remoteInstances.page.import.noneAvailable": "Não há hosts SSH disponíveis para importar.",
@@ -374,14 +374,14 @@ export const settingsDict = {
"settings.remoteInstances.page.field.modePlaceholder": "Виберіть режим",
"settings.remoteInstances.page.field.modeManaged": "Запустити для мене",
"settings.remoteInstances.page.field.modeExternal": "Уже запущено",
"settings.remoteInstances.page.field.preferredRemotePort": "Бажаний віддалений порт",
"settings.remoteInstances.page.field.preferredRemotePortHint": "Порт на віддаленій машині. Залиште порожнім, щоб вибрати автоматично.",
"settings.remoteInstances.page.field.preferredRemotePort": "Порт на віддаленій машині",
"settings.remoteInstances.page.field.preferredRemotePortHint": "Порт, який OpenChamber займе на віддаленій машині. Лишіть порожнім, щоб вибрався автоматично.",
"settings.remoteInstances.page.field.keepServerRunning": "Залишати сервер запущеним",
"settings.remoteInstances.page.field.keepServerRunningHint": "Залишати OpenChamber запущеним на віддаленій машині після відключення.",
"settings.remoteInstances.page.field.bindHost": "Прив’язати хост",
"settings.remoteInstances.page.field.bindHostHint": "Де має слухати локальне підключення. Використовуйте 127.0.0.1 або localhost, якщо вам не потрібен доступ з локальної мережі.",
"settings.remoteInstances.page.field.preferredLocalPort": "Бажаний локальний порт",
"settings.remoteInstances.page.field.preferredLocalPortHint": "Локальний порт для цього підключення. Залиште порожнім, щоб вибрати автоматично.",
"settings.remoteInstances.page.field.keepServerRunningHint": "Лишати віддалений сервер запущеним після відключення. Якщо вимкнено, він зупиняється при відключенні і запускається знову при наступному підключенні.",
"settings.remoteInstances.page.field.bindHost": "Хто має доступ",
"settings.remoteInstances.page.field.bindHostHint": "Хто може відкрити прокинуту адресу на цьому комп’ютері. Сама віддалена машина в будь-якому разі лишається доступною тільки через SSH-тунель.",
"settings.remoteInstances.page.field.preferredLocalPort": "Порт на цьому комп’ютері",
"settings.remoteInstances.page.field.preferredLocalPortHint": "Порт, який відкриється на цьому комп’ютері для тунелю. Лишіть порожнім, щоб вибрався автоматично.",
"settings.remoteInstances.page.field.forwardType": "Тип переадресації",
"settings.remoteInstances.page.field.localHostPlaceholder": "127.0.0.1",
"settings.remoteInstances.page.field.remoteHostPlaceholder": "127.0.0.1",
@@ -1203,8 +1203,8 @@ export const settingsDict = {
"settings.remoteInstances.page.section.actionsDescription": "Підключіться, перепідключіться, перегляньте журнали або видаліть це підключення.",
"settings.remoteInstances.page.section.remoteServer": "OpenChamber на віддаленій машині",
"settings.remoteInstances.page.section.remoteServerDescription": "Виберіть, як OpenChamber має працювати після SSH-підключення.",
"settings.remoteInstances.page.section.mainTunnel": "Локальний доступ",
"settings.remoteInstances.page.section.mainTunnelDescription": "Виберіть локальну адресу, через яку відкриватиметься цей віддалений сервер OpenChamber.",
"settings.remoteInstances.page.section.mainTunnel": "Доступ із цього комп’ютера",
"settings.remoteInstances.page.section.mainTunnelDescription": "OpenChamber працює на віддаленій машині. Ці налаштування керують лише адресою на цьому комп’ютері, яка веде до неї через SSH-тунель.",
"settings.remoteInstances.page.section.authentication": "Аутентифікація",
"settings.remoteInstances.page.section.authenticationDescription": "Додаткові облікові дані для SSH та віддаленого інтерфейсу користувача OpenChamber.",
"settings.remoteInstances.page.section.portForwards": "Перенаправлення портів",
@@ -1217,8 +1217,6 @@ export const settingsDict = {
"settings.remoteInstances.page.field.installMethod": "Спосіб встановлення",
"settings.remoteInstances.page.field.installMethodHint": "Як розмістити OpenChamber на віддаленій машині, коли цей застосунок запускає його для вас.",
"settings.remoteInstances.page.field.selectInstallMethodPlaceholder": "Вибрати метод встановлення",
"settings.remoteInstances.page.field.installMethodDownloadRelease": "Завантажити випуск",
"settings.remoteInstances.page.field.installMethodUploadBundle": "Завантажити пакет",
"settings.remoteInstances.page.field.selectBindHostPlaceholder": "Вибрати bind host",
"settings.remoteInstances.page.field.sshPasswordOptional": "Пароль SSH (необов'язково)",
"settings.remoteInstances.page.field.sshPasswordPlaceholder": "Введіть пароль SSH",
@@ -1242,7 +1240,44 @@ export const settingsDict = {
"settings.remoteInstances.page.actions.enableForwardAria": "Увімкнути пересилання",
"settings.remoteInstances.page.actions.openLocal": "Відкрити локально",
"settings.remoteInstances.page.actions.addForward": "Додати переадресацію",
"settings.remoteInstances.page.import.sectionTitle": "Збережені SSH-хости",
"settings.remoteInstances.page.addDialog.description": "Виберіть хост зі свого SSH-конфігу або впишіть підключення вручну.",
"settings.remoteInstances.page.addDialog.sourceLabel": "Звідки береться підключення",
"settings.remoteInstances.page.addDialog.tab.saved": "З SSH-конфігу",
"settings.remoteInstances.page.addDialog.tab.manual": "Ввести вручну",
"settings.remoteInstances.page.addDialog.searchPlaceholder": "Пошук хостів",
"settings.remoteInstances.page.addDialog.emptySaved": "У вашому SSH-конфізі немає хостів. Впишіть підключення вручну.",
"settings.remoteInstances.page.addDialog.searchEmpty": "Жоден хост не збігається з пошуком.",
"settings.remoteInstances.page.addDialog.use": "Обрати",
"settings.remoteInstances.page.state.notConnected": "Не підключено",
"settings.remoteInstances.page.state.connecting": "Підключення",
"settings.remoteInstances.page.state.ready": "Підключено",
"settings.remoteInstances.page.state.problem": "Потрібна дія",
"settings.remoteInstances.page.section.advanced": "Додаткові налаштування",
"settings.remoteInstances.page.section.advancedHint": "Порти, спосіб встановлення, паролі та додаткові прокидання. Для більшості підключень достатньо значень за замовчуванням.",
"settings.remoteInstances.page.field.installMethodAuto": "Автоматично",
"settings.remoteInstances.page.error.hint.noRuntime": "На віддаленій машині немає ні bun, ні npm. Встановіть щось із них там або переведіть це підключення в режим «Вже запущено».",
"settings.remoteInstances.page.error.hint.noOpencode": "На віддаленій машині не встановлено opencode CLI. Встановіть його там (див. opencode.ai) і підключіться знову.",
"settings.remoteInstances.page.error.action.setUiPassword": "Задати пароль UI",
"settings.remoteInstances.page.error.action.pickRandomPort": "Взяти інший локальний порт",
"settings.remoteInstances.page.error.action.setRemotePort": "Задати віддалений порт",
"settings.remoteInstances.page.validation.externalPortRequired": "Спершу вкажіть віддалений порт. У режимі «Вже запущено» OpenChamber має знати, на якому порту слухає сервер.",
"settings.remoteInstances.page.empty.noInstances": "SSH-підключень ще немає.",
"settings.remoteInstances.page.field.uiPasswordRequired": "Пароль UI (обов’язковий)",
"settings.remoteInstances.page.field.uiPasswordMissingForLan": "Обов’язковий, поки віддалений сервер доступний у своїй мережі.",
"settings.remoteInstances.page.field.remoteLanAccess": "Доступ у мережі віддаленої машини",
"settings.remoteInstances.page.field.remoteLanAccessHint": "Дозволити іншим пристроям у мережі віддаленої машини відкривати цей OpenChamber напряму, без SSH-тунелю. Потрібен пароль UI.",
"settings.remoteInstances.page.field.remoteLanAccessWarning": "Будь-хто в тій мережі зможе дістатись віддаленого OpenChamber. Його захищає лише пароль UI нижче.",
"settings.remoteInstances.page.validation.remoteLanNeedsPassword": "Спершу задайте пароль UI. Без нього віддалений OpenChamber буде відкритий для всіх пристроїв у тій мережі.",
"settings.remoteInstances.page.field.bindHostOption.loopback": "Лише цей комп’ютер (127.0.0.1)",
"settings.remoteInstances.page.field.bindHostOption.localhost": "Лише цей комп’ютер (localhost)",
"settings.remoteInstances.page.field.bindHostOption.lan": "Будь-який пристрій у моїй мережі (0.0.0.0)",
"settings.remoteInstances.page.field.sshPasswordHint": "Потрібен лише тоді, коли цей хост питає пароль замість того, щоб приймати SSH-ключ.",
"settings.remoteInstances.page.field.uiPasswordHintManaged": "Пароль, яким буде захищено віддалений інтерфейс OpenChamber. OpenChamber задасть його серверу, який запускає для вас.",
"settings.remoteInstances.page.field.uiPasswordHintExternal": "Пароль сервера OpenChamber, який уже працює на віддаленій машині, для входу в нього.",
"settings.remoteInstances.page.tunnelPreview.caption": "Це підключення прокидає:",
"settings.remoteInstances.page.empty.noInstancesWithOneImport": "SSH-підключень ще немає. З вашого SSH-конфігу можна імпортувати 1 хост.",
"settings.remoteInstances.page.empty.noInstancesWithImports": "SSH-підключень ще немає. З вашого SSH-конфігу можна імпортувати {count} хостів.",
"settings.remoteInstances.page.state.loadingInstances": "Завантаження підключень...",
"settings.remoteInstances.page.import.loading": "Завантаження хостів SSH...",
"settings.remoteInstances.page.import.noneFound": "Не знайдено хостів SSH.",
"settings.remoteInstances.page.import.noneAvailable": "Немає доступних для імпорту хостів SSH.",
@@ -374,14 +374,14 @@ export const settingsDict = {
'settings.remoteInstances.page.field.modePlaceholder': '选择模式',
'settings.remoteInstances.page.field.modeManaged': '帮我启动',
'settings.remoteInstances.page.field.modeExternal': '已在运行',
'settings.remoteInstances.page.field.preferredRemotePort': '首选远程端口',
'settings.remoteInstances.page.field.preferredRemotePortHint': '远程机器上使用的端口。留空则自动选择。',
'settings.remoteInstances.page.field.preferredRemotePort': '远程机器上的端口',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'OpenChamber 在远程机器上使用的端口。留空则自动选择。',
'settings.remoteInstances.page.field.keepServerRunning': '保持服务运行',
'settings.remoteInstances.page.field.keepServerRunningHint': '断开连接后仍让 OpenChamber 在远程机器上运行。',
'settings.remoteInstances.page.field.bindHost': '绑定主机',
'settings.remoteInstances.page.field.bindHostHint': '本地连接监听的地址。除非需要局域网访问,否则请使用 127.0.0.1 或 localhost。',
'settings.remoteInstances.page.field.preferredLocalPort': '首选本地端口',
'settings.remoteInstances.page.field.preferredLocalPortHint': '为此连接打开的本地端口。留空则自动选择。',
'settings.remoteInstances.page.field.keepServerRunningHint': '断开后让远程服务器继续运行。关闭时会在断开时停止,并在下次连接时重新启动。',
'settings.remoteInstances.page.field.bindHost': '谁可以访问',
'settings.remoteInstances.page.field.bindHostHint': '谁可以打开这台电脑上的转发地址。无论哪种选择,远程机器本身都只能通过 SSH 隧道访问。',
'settings.remoteInstances.page.field.preferredLocalPort': '这台电脑上的端口',
'settings.remoteInstances.page.field.preferredLocalPortHint': '为隧道在这台电脑上打开的端口。留空则自动选择。',
'settings.remoteInstances.page.field.forwardType': '转发类型',
'settings.remoteInstances.page.field.localHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.remoteHostPlaceholder': '127.0.0.1',
@@ -1203,8 +1203,8 @@ export const settingsDict = {
'settings.remoteInstances.page.section.actionsDescription': '连接、重新连接、查看日志或移除此连接。',
'settings.remoteInstances.page.section.remoteServer': '远程机器上的 OpenChamber',
'settings.remoteInstances.page.section.remoteServerDescription': '选择 SSH 连接后 OpenChamber 的运行方式。',
'settings.remoteInstances.page.section.mainTunnel': '本地访问',
'settings.remoteInstances.page.section.mainTunnelDescription': '选择用于打开此远程 OpenChamber 服务器的本地地址。',
'settings.remoteInstances.page.section.mainTunnel': '从这台电脑访问',
'settings.remoteInstances.page.section.mainTunnelDescription': 'OpenChamber 运行在远程机器上。这里的设置只决定这台电脑上通过 SSH 隧道通向它的地址。',
'settings.remoteInstances.page.section.authentication': '认证',
'settings.remoteInstances.page.section.authenticationDescription': 'SSH 和远程 OpenChamber UI 的可选凭据。',
'settings.remoteInstances.page.section.portForwards': '端口转发',
@@ -1217,8 +1217,6 @@ export const settingsDict = {
'settings.remoteInstances.page.field.installMethod': '安装方式',
'settings.remoteInstances.page.field.installMethodHint': '当此应用为你启动 OpenChamber 时,如何将它放到远程机器上。',
'settings.remoteInstances.page.field.selectInstallMethodPlaceholder': '选择安装方式',
'settings.remoteInstances.page.field.installMethodDownloadRelease': '下载发布版本',
'settings.remoteInstances.page.field.installMethodUploadBundle': '上传安装包',
'settings.remoteInstances.page.field.selectBindHostPlaceholder': '选择绑定主机',
'settings.remoteInstances.page.field.sshPasswordOptional': 'SSH 密码(可选)',
'settings.remoteInstances.page.field.sshPasswordPlaceholder': '输入 SSH 密码',
@@ -1242,7 +1240,44 @@ export const settingsDict = {
'settings.remoteInstances.page.actions.enableForwardAria': '启用转发',
'settings.remoteInstances.page.actions.openLocal': '打开本地',
'settings.remoteInstances.page.actions.addForward': '添加转发',
'settings.remoteInstances.page.import.sectionTitle': '已保存的 SSH 主机',
'settings.remoteInstances.page.addDialog.description': ' SSH 配置中选择一台主机,或者自己输入连接。',
'settings.remoteInstances.page.addDialog.sourceLabel': '连接的来源',
'settings.remoteInstances.page.addDialog.tab.saved': '来自 SSH 配置',
'settings.remoteInstances.page.addDialog.tab.manual': '自己输入',
'settings.remoteInstances.page.addDialog.searchPlaceholder': '搜索主机',
'settings.remoteInstances.page.addDialog.emptySaved': '在你的 SSH 配置中没有找到主机。请自己输入连接。',
'settings.remoteInstances.page.addDialog.searchEmpty': '没有主机匹配此搜索。',
'settings.remoteInstances.page.addDialog.use': '使用',
'settings.remoteInstances.page.state.notConnected': '未连接',
'settings.remoteInstances.page.state.connecting': '连接中',
'settings.remoteInstances.page.state.ready': '已连接',
'settings.remoteInstances.page.state.problem': '需要处理',
'settings.remoteInstances.page.section.advanced': '高级设置',
'settings.remoteInstances.page.section.advancedHint': '端口、安装方式、密码和额外转发。大多数连接使用默认值即可。',
'settings.remoteInstances.page.field.installMethodAuto': '自动',
'settings.remoteInstances.page.error.hint.noRuntime': '远程机器上既没有 bun 也没有 npm。请在那里安装其中之一,或把此连接切换为“已在运行”。',
'settings.remoteInstances.page.error.hint.noOpencode': '远程机器上没有安装 opencode CLI。请先在那里安装(见 opencode.ai),然后重新连接。',
'settings.remoteInstances.page.error.action.setUiPassword': '设置界面密码',
'settings.remoteInstances.page.error.action.pickRandomPort': '使用另一个本地端口',
'settings.remoteInstances.page.error.action.setRemotePort': '设置远程端口',
'settings.remoteInstances.page.validation.externalPortRequired': '请先指定远程端口。在“已在运行”模式下,OpenChamber 需要知道服务器监听哪个端口。',
'settings.remoteInstances.page.empty.noInstances': '还没有 SSH 连接。',
'settings.remoteInstances.page.field.uiPasswordRequired': '界面密码(必填)',
'settings.remoteInstances.page.field.uiPasswordMissingForLan': '只要远程服务器可在其网络中访问,就必须填写。',
'settings.remoteInstances.page.field.remoteLanAccess': '可在远程网络中访问',
'settings.remoteInstances.page.field.remoteLanAccessHint': '也允许远程机器所在网络中的其他设备不经 SSH 隧道直接打开这个 OpenChamber。必须设置界面密码。',
'settings.remoteInstances.page.field.remoteLanAccessWarning': '该网络中的任何人都能访问远程 OpenChamber,保护它的只有下面的界面密码。',
'settings.remoteInstances.page.validation.remoteLanNeedsPassword': '请先设置界面密码。没有密码时,远程 OpenChamber 会对该网络中的所有设备开放。',
'settings.remoteInstances.page.field.bindHostOption.loopback': '仅这台电脑 (127.0.0.1)',
'settings.remoteInstances.page.field.bindHostOption.localhost': '仅这台电脑 (localhost)',
'settings.remoteInstances.page.field.bindHostOption.lan': '我网络中的任意设备 (0.0.0.0)',
'settings.remoteInstances.page.field.sshPasswordHint': '只有当该主机要求密码而不是接受 SSH 密钥时才需要。',
'settings.remoteInstances.page.field.uiPasswordHintManaged': '用于保护远程 OpenChamber 界面的密码。OpenChamber 会把它设置到为你启动的服务器上。',
'settings.remoteInstances.page.field.uiPasswordHintExternal': '远程机器上已在运行的 OpenChamber 服务器的密码,用于登录。',
'settings.remoteInstances.page.tunnelPreview.caption': '此连接的转发:',
'settings.remoteInstances.page.empty.noInstancesWithOneImport': '还没有 SSH 连接。可从你的 SSH 配置导入 1 台主机。',
'settings.remoteInstances.page.empty.noInstancesWithImports': '还没有 SSH 连接。可从你的 SSH 配置导入 {count} 台主机。',
'settings.remoteInstances.page.state.loadingInstances': '正在加载连接...',
'settings.remoteInstances.page.import.loading': '正在加载 SSH 主机...',
'settings.remoteInstances.page.import.noneFound': '未找到 SSH 主机。',
'settings.remoteInstances.page.import.noneAvailable': '没有可导入的 SSH 主机。',
@@ -371,14 +371,14 @@ export const settingsDict = {
'settings.remoteInstances.page.field.modePlaceholder': '選擇模式',
'settings.remoteInstances.page.field.modeManaged': 'Managed(自動啟動)',
'settings.remoteInstances.page.field.modeExternal': 'External(已在執行)',
'settings.remoteInstances.page.field.preferredRemotePort': '偏好遠端連接埠',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'OpenChamber 在遠端機使用的連接埠。留空則由執行時自動選擇。',
'settings.remoteInstances.page.field.preferredRemotePort': '遠端機器上的連接埠',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'OpenChamber 在遠端機器上使用的連接埠。留空則自動選擇。',
'settings.remoteInstances.page.field.keepServerRunning': '保持服務執行',
'settings.remoteInstances.page.field.keepServerRunningHint': '啟用後,中斷連線時會保留遠端 OpenChamber 背景程式。',
'settings.remoteInstances.page.field.bindHost': '綁定主機',
'settings.remoteInstances.page.field.bindHostHint': '主本機存取位址使用的網路介面。使用 127.0.0.1/localhost 可僅限本機存取。',
'settings.remoteInstances.page.field.preferredLocalPort': '偏好本機連接埠',
'settings.remoteInstances.page.field.preferredLocalPortHint': '主 OpenChamber tunnel 的偏好本機連接埠。留空自動選擇。',
'settings.remoteInstances.page.field.keepServerRunningHint': '中斷後讓遠端伺服器繼續執行。關閉時會在中斷時停止,並在下次連線時重新啟動。',
'settings.remoteInstances.page.field.bindHost': '誰可以存取',
'settings.remoteInstances.page.field.bindHostHint': '誰可以開啟這台電腦上的轉發位址。無論哪種選擇,遠端機器本身都只能透過 SSH 隧道存取。',
'settings.remoteInstances.page.field.preferredLocalPort': '這台電腦上的連接埠',
'settings.remoteInstances.page.field.preferredLocalPortHint': '為隧道在這台電腦上開啟的連接埠。留空自動選擇。',
'settings.remoteInstances.page.field.forwardType': '轉送類型',
'settings.remoteInstances.page.field.localHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.remoteHostPlaceholder': '127.0.0.1',
@@ -1110,8 +1110,8 @@ export const settingsDict = {
'settings.remoteInstances.page.section.actionsDescription': '連線、重新連線、查看紀錄或移除此執行個體。',
'settings.remoteInstances.page.section.remoteServer': '遠端服務',
'settings.remoteInstances.page.section.remoteServerDescription': 'OpenChamber 在遠端主機上的管理與啟動方式。',
'settings.remoteInstances.page.section.mainTunnel': '主 tunnel',
'settings.remoteInstances.page.section.mainTunnelDescription': '該遠端執行個體的主本機存取端點。',
'settings.remoteInstances.page.section.mainTunnel': '從這台電腦存取',
'settings.remoteInstances.page.section.mainTunnelDescription': 'OpenChamber 執行在遠端機器上。這裡的設定只決定這台電腦上通往它的 SSH 隧道位址。',
'settings.remoteInstances.page.section.authentication': '驗證',
'settings.remoteInstances.page.section.authenticationDescription': 'SSH 和遠端 OpenChamber UI 的可選憑證。',
'settings.remoteInstances.page.section.portForwards': '連接埠轉送',
@@ -1124,8 +1124,6 @@ export const settingsDict = {
'settings.remoteInstances.page.field.installMethod': '安裝方式',
'settings.remoteInstances.page.field.installMethodHint': '在 managed 模式下 OpenChamber 的安裝方式。',
'settings.remoteInstances.page.field.selectInstallMethodPlaceholder': '選擇安裝方式',
'settings.remoteInstances.page.field.installMethodDownloadRelease': '下載發行版本',
'settings.remoteInstances.page.field.installMethodUploadBundle': '上傳安裝套件',
'settings.remoteInstances.page.field.selectBindHostPlaceholder': '選擇綁定主機',
'settings.remoteInstances.page.field.sshPasswordOptional': 'SSH 密碼(可選)',
'settings.remoteInstances.page.field.sshPasswordPlaceholder': '輸入 SSH 密碼',
@@ -1149,7 +1147,44 @@ export const settingsDict = {
'settings.remoteInstances.page.actions.enableForwardAria': '啟用轉送',
'settings.remoteInstances.page.actions.openLocal': '開啟本機',
'settings.remoteInstances.page.actions.addForward': '新增轉送',
'settings.remoteInstances.page.import.sectionTitle': '從 SSH 設定匯入',
'settings.remoteInstances.page.addDialog.description': '從 SSH 設定中選一台主機,或自己輸入連線。',
'settings.remoteInstances.page.addDialog.sourceLabel': '連線的來源',
'settings.remoteInstances.page.addDialog.tab.saved': '來自 SSH 設定',
'settings.remoteInstances.page.addDialog.tab.manual': '自己輸入',
'settings.remoteInstances.page.addDialog.searchPlaceholder': '搜尋主機',
'settings.remoteInstances.page.addDialog.emptySaved': '在你的 SSH 設定中找不到主機。請自己輸入連線。',
'settings.remoteInstances.page.addDialog.searchEmpty': '沒有主機符合此搜尋。',
'settings.remoteInstances.page.addDialog.use': '使用',
'settings.remoteInstances.page.state.notConnected': '未連線',
'settings.remoteInstances.page.state.connecting': '連線中',
'settings.remoteInstances.page.state.ready': '已連線',
'settings.remoteInstances.page.state.problem': '需要處理',
'settings.remoteInstances.page.section.advanced': '進階設定',
'settings.remoteInstances.page.section.advancedHint': '連接埠、安裝方式、密碼與額外轉發。大多數連線使用預設值即可。',
'settings.remoteInstances.page.field.installMethodAuto': '自動',
'settings.remoteInstances.page.error.hint.noRuntime': '遠端機器上既沒有 bun 也沒有 npm。請在那裡安裝其中之一,或把此連線切換為「已在執行」。',
'settings.remoteInstances.page.error.hint.noOpencode': '遠端機器上沒有安裝 opencode CLI。請先在那裡安裝(見 opencode.ai),然後重新連線。',
'settings.remoteInstances.page.error.action.setUiPassword': '設定介面密碼',
'settings.remoteInstances.page.error.action.pickRandomPort': '使用其他本機連接埠',
'settings.remoteInstances.page.error.action.setRemotePort': '設定遠端連接埠',
'settings.remoteInstances.page.validation.externalPortRequired': '請先指定遠端連接埠。在「已在執行」模式下,OpenChamber 需要知道伺服器監聽哪個連接埠。',
'settings.remoteInstances.page.empty.noInstances': '還沒有 SSH 連線。',
'settings.remoteInstances.page.field.uiPasswordRequired': '介面密碼(必填)',
'settings.remoteInstances.page.field.uiPasswordMissingForLan': '只要遠端伺服器可在其網路中存取,就必須填寫。',
'settings.remoteInstances.page.field.remoteLanAccess': '可在遠端網路中存取',
'settings.remoteInstances.page.field.remoteLanAccessHint': '也允許遠端機器所在網路中的其他裝置不經 SSH 隧道直接開啟這個 OpenChamber。必須設定介面密碼。',
'settings.remoteInstances.page.field.remoteLanAccessWarning': '該網路中的任何人都能存取遠端 OpenChamber,保護它的只有下面的介面密碼。',
'settings.remoteInstances.page.validation.remoteLanNeedsPassword': '請先設定介面密碼。沒有密碼時,遠端 OpenChamber 會對該網路中的所有裝置開放。',
'settings.remoteInstances.page.field.bindHostOption.loopback': '僅這台電腦 (127.0.0.1)',
'settings.remoteInstances.page.field.bindHostOption.localhost': '僅這台電腦 (localhost)',
'settings.remoteInstances.page.field.bindHostOption.lan': '我網路中的任何裝置 (0.0.0.0)',
'settings.remoteInstances.page.field.sshPasswordHint': '只有當該主機要求密碼而非接受 SSH 金鑰時才需要。',
'settings.remoteInstances.page.field.uiPasswordHintManaged': '用來保護遠端 OpenChamber 介面的密碼。OpenChamber 會把它設定到為你啟動的伺服器上。',
'settings.remoteInstances.page.field.uiPasswordHintExternal': '遠端機器上已在執行的 OpenChamber 伺服器的密碼,用於登入。',
'settings.remoteInstances.page.tunnelPreview.caption': '此連線的轉發:',
'settings.remoteInstances.page.empty.noInstancesWithOneImport': '還沒有 SSH 連線。可從你的 SSH 設定匯入 1 台主機。',
'settings.remoteInstances.page.empty.noInstancesWithImports': '還沒有 SSH 連線。可從你的 SSH 設定匯入 {count} 台主機。',
'settings.remoteInstances.page.state.loadingInstances': '正在載入連線...',
'settings.remoteInstances.page.import.loading': '正在載入 SSH 主機...',
'settings.remoteInstances.page.import.noneFound': '找不到 SSH 主機。',
'settings.remoteInstances.page.import.noneAvailable': '沒有可匯入的 SSH 主機。',