mirror of
https://github.com/block/goose.git
synced 2026-07-09 16:09:22 +00:00
removed tunnel rest api and ui (#9932)
This commit is contained in:
parent
79c6547afb
commit
efafdf72a9
26 changed files with 26 additions and 1801 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -5053,7 +5053,6 @@ dependencies = [
|
|||
"chrono",
|
||||
"clap",
|
||||
"config",
|
||||
"fs2",
|
||||
"futures",
|
||||
"goose",
|
||||
"goose-mcp",
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ url = { workspace = true }
|
|||
rand = { workspace = true }
|
||||
hex = { version = "0.4.3", default-features = false, features = ["std"] }
|
||||
socket2 = { version = "0.6", default-features = false }
|
||||
fs2 = { workspace = true }
|
||||
rustls = { workspace = true, optional = true }
|
||||
uuid = { workspace = true }
|
||||
rcgen = { version = "0.14", default-features = false, features = ["aws_lc_rs", "crypto", "pem"] }
|
||||
|
|
|
|||
|
|
@ -55,13 +55,6 @@ pub async fn run() -> Result<()> {
|
|||
boot_marker("appstate init start");
|
||||
let app_state = state::AppState::new(settings.tls).await?;
|
||||
|
||||
// Share the server secret with the tunnel manager so it uses the same
|
||||
// key for forwarded requests, without mutating the process environment.
|
||||
app_state
|
||||
.tunnel_manager
|
||||
.set_server_secret(secret_key.clone())
|
||||
.await;
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
|
|
@ -93,11 +86,6 @@ pub async fn run() -> Result<()> {
|
|||
|
||||
let addr = settings.socket_addr();
|
||||
|
||||
let tunnel_manager = app_state.tunnel_manager.clone();
|
||||
tokio::spawn(async move {
|
||||
tunnel_manager.check_auto_start().await;
|
||||
});
|
||||
|
||||
let gateway_manager = app_state.gateway_manager.clone();
|
||||
tokio::spawn(async move {
|
||||
gateway_manager.check_auto_start().await;
|
||||
|
|
|
|||
|
|
@ -476,8 +476,6 @@ derive_utoipa!(IconTheme as IconThemeSchema);
|
|||
super::routes::setup::start_openrouter_setup,
|
||||
super::routes::setup::start_tetrate_setup,
|
||||
super::routes::setup::start_nanogpt_setup,
|
||||
super::routes::tunnel::start_tunnel,
|
||||
super::routes::tunnel::stop_tunnel,
|
||||
super::routes::tunnel::get_tunnel_status,
|
||||
super::routes::telemetry::send_telemetry_event,
|
||||
super::routes::dictation::transcribe_dictation,
|
||||
|
|
|
|||
|
|
@ -3,58 +3,10 @@ use axum::{
|
|||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ErrorResponse {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
/// Start the tunnel
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tunnel/start",
|
||||
responses(
|
||||
(status = 200, description = "Tunnel started successfully", body = TunnelInfo),
|
||||
(status = 400, description = "Bad request", body = ErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||
)
|
||||
)]
|
||||
#[axum::debug_handler]
|
||||
pub async fn start_tunnel(State(state): State<Arc<AppState>>) -> Response {
|
||||
match state.tunnel_manager.start().await {
|
||||
Ok(info) => (StatusCode::OK, Json(info)).into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to start tunnel: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: e.to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the tunnel
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tunnel/stop",
|
||||
responses(
|
||||
(status = 200, description = "Tunnel stopped successfully"),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn stop_tunnel(State(state): State<Arc<AppState>>) -> Response {
|
||||
state.tunnel_manager.stop(true).await;
|
||||
StatusCode::OK.into_response()
|
||||
}
|
||||
|
||||
/// Get tunnel info
|
||||
#[utoipa::path(
|
||||
|
|
@ -71,8 +23,6 @@ pub async fn get_tunnel_status(State(state): State<Arc<AppState>>) -> Response {
|
|||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/tunnel/start", post(start_tunnel))
|
||||
.route("/tunnel/stop", post(stop_tunnel))
|
||||
.route("/tunnel/status", get(get_tunnel_status))
|
||||
.with_state(state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +1,6 @@
|
|||
pub mod lapstone;
|
||||
|
||||
use crate::configuration::Settings;
|
||||
use fs2::FileExt as _;
|
||||
use goose::config::{paths::Paths, Config};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
fn get_server_port() -> anyhow::Result<u16> {
|
||||
let settings = Settings::new()?;
|
||||
Ok(settings.port)
|
||||
}
|
||||
|
||||
fn get_lock_path() -> std::path::PathBuf {
|
||||
Paths::config_dir().join("tunnel.lock")
|
||||
}
|
||||
|
||||
fn try_acquire_tunnel_lock() -> anyhow::Result<File> {
|
||||
let lock_path = get_lock_path();
|
||||
|
||||
if let Some(parent) = lock_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&lock_path)?;
|
||||
|
||||
file.try_lock_exclusive()
|
||||
.map_err(|_| anyhow::anyhow!("Another goose instance is already running the tunnel"))?;
|
||||
|
||||
writeln!(file, "{}", std::process::id())?;
|
||||
file.sync_all()?;
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn is_tunnel_locked_by_another() -> bool {
|
||||
let lock_path = get_lock_path();
|
||||
|
||||
let file = match OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&lock_path)
|
||||
{
|
||||
Ok(f) => f,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
if file.try_lock_exclusive().is_err() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Lock released when file is dropped
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TunnelState {
|
||||
|
|
@ -81,16 +20,7 @@ pub struct TunnelInfo {
|
|||
pub secret: String,
|
||||
}
|
||||
|
||||
pub struct TunnelManager {
|
||||
state: Arc<RwLock<TunnelState>>,
|
||||
info: Arc<RwLock<Option<TunnelInfo>>>,
|
||||
lapstone_handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
|
||||
restart_tx: Arc<RwLock<Option<mpsc::Sender<()>>>>,
|
||||
watchdog_handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
|
||||
lock_file: Arc<std::sync::Mutex<Option<File>>>,
|
||||
scheme: String,
|
||||
server_secret: Arc<RwLock<Option<String>>>,
|
||||
}
|
||||
pub struct TunnelManager;
|
||||
|
||||
impl Default for TunnelManager {
|
||||
fn default() -> Self {
|
||||
|
|
@ -99,55 +29,8 @@ impl Default for TunnelManager {
|
|||
}
|
||||
|
||||
impl TunnelManager {
|
||||
pub fn new(tls: bool) -> Self {
|
||||
TunnelManager {
|
||||
state: Arc::new(RwLock::new(TunnelState::Idle)),
|
||||
info: Arc::new(RwLock::new(None)),
|
||||
lapstone_handle: Arc::new(RwLock::new(None)),
|
||||
restart_tx: Arc::new(RwLock::new(None)),
|
||||
watchdog_handle: Arc::new(RwLock::new(None)),
|
||||
lock_file: Arc::new(std::sync::Mutex::new(None)),
|
||||
scheme: if tls { "https" } else { "http" }.to_string(),
|
||||
server_secret: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_auto_start() -> bool {
|
||||
Config::global()
|
||||
.get_param("tunnel_auto_start")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn get_secret() -> Option<String> {
|
||||
Config::global().get_secret("tunnel_secret").ok()
|
||||
}
|
||||
|
||||
fn get_agent_id() -> Option<String> {
|
||||
Config::global().get_secret("tunnel_agent_id").ok()
|
||||
}
|
||||
|
||||
pub async fn check_auto_start(&self) {
|
||||
let auto_start = Self::get_auto_start();
|
||||
let state = self.state.read().await.clone();
|
||||
|
||||
if auto_start && state == TunnelState::Idle {
|
||||
if is_tunnel_locked_by_another() {
|
||||
tracing::info!(
|
||||
"Tunnel already running on another goose instance, skipping auto-start"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::info!("Auto-starting tunnel");
|
||||
match self.start().await {
|
||||
Ok(info) => {
|
||||
tracing::info!("Tunnel auto-started successfully: {:?}", info.url);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::info!("Tunnel auto-start skipped: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn new(_tls: bool) -> Self {
|
||||
TunnelManager
|
||||
}
|
||||
|
||||
fn is_tunnel_disabled() -> bool {
|
||||
|
|
@ -160,210 +43,15 @@ impl TunnelManager {
|
|||
}
|
||||
|
||||
pub async fn get_info(&self) -> TunnelInfo {
|
||||
if Self::is_tunnel_disabled() {
|
||||
return TunnelInfo {
|
||||
state: TunnelState::Disabled,
|
||||
url: String::new(),
|
||||
hostname: String::new(),
|
||||
secret: String::new(),
|
||||
};
|
||||
}
|
||||
|
||||
let state = self.state.read().await.clone();
|
||||
let info = self.info.read().await.clone();
|
||||
|
||||
match info {
|
||||
Some(mut tunnel_info) => {
|
||||
tunnel_info.state = state;
|
||||
tunnel_info
|
||||
}
|
||||
None => {
|
||||
let effective_state = if state == TunnelState::Idle && is_tunnel_locked_by_another()
|
||||
{
|
||||
TunnelState::Running
|
||||
} else {
|
||||
state
|
||||
};
|
||||
TunnelInfo {
|
||||
state: effective_state,
|
||||
url: String::new(),
|
||||
hostname: String::new(),
|
||||
secret: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_server_secret(&self, secret: String) {
|
||||
*self.server_secret.write().await = Some(secret);
|
||||
}
|
||||
|
||||
pub fn set_auto_start(auto_start: bool) -> anyhow::Result<()> {
|
||||
Config::global()
|
||||
.set_param("tunnel_auto_start", auto_start)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to save tunnel config: {}", e))
|
||||
}
|
||||
|
||||
pub fn set_secret(secret: &str) -> anyhow::Result<()> {
|
||||
Config::global()
|
||||
.set_secret("tunnel_secret", &secret.to_string())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to save tunnel secret: {}", e))
|
||||
}
|
||||
|
||||
pub fn set_agent_id(agent_id: &str) -> anyhow::Result<()> {
|
||||
Config::global()
|
||||
.set_secret("tunnel_agent_id", &agent_id.to_string())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to save tunnel agent_id: {}", e))
|
||||
}
|
||||
|
||||
async fn start_tunnel_internal(&self) -> anyhow::Result<(TunnelInfo, mpsc::Receiver<()>)> {
|
||||
let server_port = get_server_port()?;
|
||||
let tunnel_secret = Self::get_secret().unwrap_or_else(generate_secret);
|
||||
let server_secret = self
|
||||
.server_secret
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.expect("server_secret must be set before starting tunnel");
|
||||
let agent_id = Self::get_agent_id().unwrap_or_else(generate_agent_id);
|
||||
|
||||
Self::set_secret(&tunnel_secret)?;
|
||||
Self::set_agent_id(&agent_id)?;
|
||||
|
||||
let (restart_tx, restart_rx) = mpsc::channel::<()>(1);
|
||||
*self.restart_tx.write().await = Some(restart_tx.clone());
|
||||
|
||||
let result = lapstone::start(
|
||||
server_port,
|
||||
tunnel_secret,
|
||||
server_secret,
|
||||
agent_id,
|
||||
&self.scheme,
|
||||
self.lapstone_handle.clone(),
|
||||
restart_tx,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(info) => Ok((info, restart_rx)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> anyhow::Result<TunnelInfo> {
|
||||
if Self::is_tunnel_disabled() {
|
||||
anyhow::bail!("Tunnel is disabled via GOOSE_TUNNEL environment variable");
|
||||
}
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
if *state != TunnelState::Idle {
|
||||
anyhow::bail!("Tunnel is already running or starting");
|
||||
}
|
||||
|
||||
let lock = try_acquire_tunnel_lock()?;
|
||||
*self.lock_file.lock().unwrap() = Some(lock);
|
||||
|
||||
*state = TunnelState::Starting;
|
||||
drop(state);
|
||||
|
||||
match self.start_tunnel_internal().await {
|
||||
Ok((info, mut restart_rx)) => {
|
||||
*self.state.write().await = TunnelState::Running;
|
||||
*self.info.write().await = Some(info.clone());
|
||||
let _ = Self::set_auto_start(true);
|
||||
|
||||
let state = self.state.clone();
|
||||
let lapstone_handle = self.lapstone_handle.clone();
|
||||
let watchdog_handle_arc = self.watchdog_handle.clone();
|
||||
let manager = Arc::new(self.clone_for_watchdog());
|
||||
|
||||
let watchdog = tokio::spawn(async move {
|
||||
while restart_rx.recv().await.is_some() {
|
||||
let auto_start = Self::get_auto_start();
|
||||
if !auto_start {
|
||||
tracing::info!("Tunnel connection lost but auto_start is disabled");
|
||||
break;
|
||||
}
|
||||
|
||||
tracing::warn!("Tunnel connection lost, initiating restart...");
|
||||
lapstone::stop(lapstone_handle.clone()).await;
|
||||
*state.write().await = TunnelState::Idle;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
*state.write().await = TunnelState::Starting;
|
||||
|
||||
match manager.start_tunnel_internal().await {
|
||||
Ok((_, new_restart_rx)) => {
|
||||
*state.write().await = TunnelState::Running;
|
||||
tracing::info!("Tunnel restarted successfully");
|
||||
restart_rx = new_restart_rx;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to restart tunnel: {}", e);
|
||||
*state.write().await = TunnelState::Error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
*watchdog_handle_arc.write().await = Some(watchdog);
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
Err(e) => {
|
||||
self.release_lock();
|
||||
*self.state.write().await = TunnelState::Error;
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_for_watchdog(&self) -> Self {
|
||||
TunnelManager {
|
||||
state: self.state.clone(),
|
||||
info: self.info.clone(),
|
||||
lapstone_handle: self.lapstone_handle.clone(),
|
||||
restart_tx: self.restart_tx.clone(),
|
||||
watchdog_handle: self.watchdog_handle.clone(),
|
||||
lock_file: self.lock_file.clone(),
|
||||
scheme: self.scheme.clone(),
|
||||
server_secret: self.server_secret.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn release_lock(&self) {
|
||||
if let Ok(mut guard) = self.lock_file.lock() {
|
||||
// Dropping the file releases the lock
|
||||
guard.take();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop(&self, clear_auto_start: bool) {
|
||||
if let Some(handle) = self.watchdog_handle.write().await.take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
*self.restart_tx.write().await = None;
|
||||
|
||||
lapstone::stop(self.lapstone_handle.clone()).await;
|
||||
|
||||
self.release_lock();
|
||||
|
||||
*self.state.write().await = TunnelState::Idle;
|
||||
*self.info.write().await = None;
|
||||
|
||||
if clear_auto_start {
|
||||
let _ = Self::set_auto_start(false);
|
||||
TunnelInfo {
|
||||
state: if Self::is_tunnel_disabled() {
|
||||
TunnelState::Disabled
|
||||
} else {
|
||||
TunnelState::Idle
|
||||
},
|
||||
url: String::new(),
|
||||
hostname: String::new(),
|
||||
secret: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_secret() -> String {
|
||||
let bytes: [u8; 32] = rand::random();
|
||||
hex::encode(bytes)
|
||||
}
|
||||
|
||||
pub(super) fn generate_agent_id() -> String {
|
||||
let bytes: [u8; 32] = rand::random();
|
||||
hex::encode(bytes)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ unlisted: true
|
|||
---
|
||||
|
||||
:::info Archived
|
||||
goose Mobile has been archived. Mobile access to goose is now supported for [iOS devices via tunneling](/docs/experimental/remote-access/mobile-access).
|
||||
goose Mobile has been archived. The previous iOS mobile tunnel setup is also retired in current goose Desktop builds.
|
||||
:::
|
||||
|
||||
goose Mobile is an experimental Android project inspired by the goose application. It acts as an open agent on your phone, automating multistep tasks, responding to notifications, and even replacing your home screen for maximum efficiency.
|
||||
|
|
@ -40,4 +40,4 @@ We welcome contributions! See the [Contributing Guide](https://github.com/aaif-g
|
|||
|
||||
---
|
||||
|
||||
For more scenarios, instructions, and development setup, visit the [goose Mobile repository](https://github.com/aaif-goose/goose-mobile).
|
||||
For more scenarios, instructions, and development setup, visit the [goose Mobile repository](https://github.com/aaif-goose/goose-mobile).
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
title: Mobile Access via Secure Tunneling
|
||||
sidebar_position: 3
|
||||
sidebar_label: Mobile Access
|
||||
description: Enable remote access to goose from mobile devices using secure tunneling.
|
||||
description: Mobile access via secure tunneling is no longer available in goose Desktop.
|
||||
unlisted: true
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Remote Access
|
||||
sidebar_position: 2
|
||||
description: Access goose remotely from mobile devices and messaging platforms.
|
||||
description: Access goose remotely from messaging platforms.
|
||||
---
|
||||
|
||||
import Card from '@site/src/components/Card';
|
||||
|
|
@ -9,14 +9,9 @@ import styles from '@site/src/components/Card/styles.module.css';
|
|||
|
||||
# Remote Access
|
||||
|
||||
Access goose from anywhere using mobile apps or messaging platforms. These features let you interact with goose when you're away from your computer.
|
||||
Access goose from anywhere using supported messaging platforms. These features let you interact with goose when you're away from your computer.
|
||||
|
||||
<div className={styles.cardGrid}>
|
||||
<Card
|
||||
title="Mobile Access via Secure Tunneling"
|
||||
description="Connect to goose Desktop from the goose AI iOS app using secure tunneling."
|
||||
link="/docs/experimental/remote-access/mobile-access"
|
||||
/>
|
||||
<Card
|
||||
title="Telegram Gateway"
|
||||
description="Chat with goose through Telegram from any device."
|
||||
|
|
|
|||
|
|
@ -2,79 +2,10 @@
|
|||
title: Mobile Access via Secure Tunneling
|
||||
sidebar_position: 1
|
||||
sidebar_label: Mobile Access
|
||||
description: Enable remote access to goose from mobile devices using secure tunneling.
|
||||
description: Mobile access via secure tunneling is no longer available in goose Desktop.
|
||||
unlisted: true
|
||||
---
|
||||
|
||||
import { PanelLeft } from 'lucide-react';
|
||||
Mobile access via secure tunneling is no longer available in current goose Desktop builds.
|
||||
|
||||
Mobile access lets you connect to goose remotely from an iOS mobile device using secure tunneling.
|
||||
|
||||
:::warning Experimental Feature
|
||||
Mobile access is a preview feature in active development. Behavior and configuration may change in future releases.
|
||||
:::
|
||||
|
||||
## How Mobile Access Works
|
||||
|
||||
Mobile access connects your iOS device to goose Desktop through a secure tunnel. After you install and configure the **goose AI** app, you can access goose from anywhere.
|
||||
|
||||
**Key details:**
|
||||
- Uses [Lapstone](https://github.com/michaelneale/lapstone-tunnel), a public HTTPS tunnel service provided by Mic Neale
|
||||
- Easy setup using a QR code with a unique secret key to secure the connection
|
||||
- Your tunnel URL remains the same across sessions, so you only need to configure your mobile app once
|
||||
- The connection requires your computer to be awake with goose Desktop running
|
||||
- Automatically reconnects if interrupted and restarts when you launch goose Desktop
|
||||
|
||||
## Setup
|
||||
|
||||
### Install the App
|
||||
1. Install the **goose AI** app on your iOS mobile device from the [App Store](https://apps.apple.com/app/goose-ai/id6752889295)
|
||||
|
||||
:::tip App Store QR Code
|
||||
Follow the steps below to open the `Mobile App` section, then click "scan QR code" in the info box for quick access to the App Store.
|
||||
:::
|
||||
|
||||
### Start the Tunnel
|
||||
1. Open goose Desktop
|
||||
2. Click the <PanelLeft className="inline" size={16} /> button in the top-left to open the sidebar
|
||||
3. Click `Settings` in the sidebar
|
||||
4. Click `Session`
|
||||
5. Scroll down to the `Mobile App` section and click `Start Tunnel`
|
||||
|
||||
Once the tunnel starts, you'll see a `Mobile App Connection` QR code for configuring the app.
|
||||
|
||||
:::info
|
||||
Click `Stop Tunnel` at any time to close the connection.
|
||||
:::
|
||||
|
||||
### Connect the App
|
||||
1. Open the **goose AI** app on your iOS mobile device
|
||||
2. Scan the `Mobile App Connection` QR code displayed in goose Desktop
|
||||
3. The app will automatically configure the connection
|
||||
|
||||
You can now access goose Desktop from your mobile device.
|
||||
|
||||
## What You Can Do
|
||||
|
||||
The mobile app gives you full access to goose:
|
||||
- Start new conversations or continue existing sessions
|
||||
- Use all your goose extensions and configurations
|
||||
- Work from anywhere while your computer handles the processing
|
||||
|
||||
## Additional Resources
|
||||
|
||||
import ContentCardCarousel from '@site/src/components/ContentCardCarousel';
|
||||
import mobileShots from '@site/blog/2025-12-19-goose-mobile-terminal/mobile_shots.png';
|
||||
|
||||
<ContentCardCarousel
|
||||
items={[
|
||||
{
|
||||
type: 'blog',
|
||||
title: 'goose Mobile Access and Native Terminal Support',
|
||||
description: 'Learn about two new ways to use goose: iOS app for mobile access and native terminal support with seamless session continuity.',
|
||||
thumbnailUrl: mobileShots,
|
||||
linkUrl: '/blog/2025/12/19/goose-mobile-terminal',
|
||||
date: '2025-12-19',
|
||||
duration: '4 min read'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
The previous setup flow depended on a Desktop settings panel that started a tunnel and displayed a mobile connection QR code. That panel and the `/tunnel/start` and `/tunnel/stop` APIs have been removed, so new mobile app connections cannot be configured from Desktop.
|
||||
|
|
|
|||
|
|
@ -3727,47 +3727,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/tunnel/start": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::tunnel"
|
||||
],
|
||||
"summary": "Start the tunnel",
|
||||
"operationId": "start_tunnel",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Tunnel started successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TunnelInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tunnel/status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
@ -3788,30 +3747,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tunnel/stop": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::tunnel"
|
||||
],
|
||||
"summary": "Stop the tunnel",
|
||||
"operationId": "stop_tunnel",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Tunnel stopped successfully"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@
|
|||
"katex": "^0.16.33",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.575.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-icons": "^5.5.0",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4662,35 +4662,6 @@ export type SendTelemetryEventResponses = {
|
|||
202: unknown;
|
||||
};
|
||||
|
||||
export type StartTunnelData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/tunnel/start';
|
||||
};
|
||||
|
||||
export type StartTunnelErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: ErrorResponse;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: ErrorResponse;
|
||||
};
|
||||
|
||||
export type StartTunnelError = StartTunnelErrors[keyof StartTunnelErrors];
|
||||
|
||||
export type StartTunnelResponses = {
|
||||
/**
|
||||
* Tunnel started successfully
|
||||
*/
|
||||
200: TunnelInfo;
|
||||
};
|
||||
|
||||
export type StartTunnelResponse = StartTunnelResponses[keyof StartTunnelResponses];
|
||||
|
||||
export type GetTunnelStatusData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
|
@ -4706,26 +4677,3 @@ export type GetTunnelStatusResponses = {
|
|||
};
|
||||
|
||||
export type GetTunnelStatusResponse = GetTunnelStatusResponses[keyof GetTunnelStatusResponses];
|
||||
|
||||
export type StopTunnelData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/tunnel/stop';
|
||||
};
|
||||
|
||||
export type StopTunnelErrors = {
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: ErrorResponse;
|
||||
};
|
||||
|
||||
export type StopTunnelError = StopTunnelErrors[keyof StopTunnelErrors];
|
||||
|
||||
export type StopTunnelResponses = {
|
||||
/**
|
||||
* Tunnel stopped successfully
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import {
|
|||
KeyRound,
|
||||
} from 'lucide-react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import TunnelSection from './tunnel/TunnelSection';
|
||||
import GatewaySettingsSection from './gateways/GatewaySettingsSection';
|
||||
import { getTunnelStatus } from '../../api/sdk.gen';
|
||||
import ChatSettingsSection from './chat/ChatSettingsSection';
|
||||
|
|
@ -300,12 +299,7 @@ export default function SettingsView({
|
|||
<div className="space-y-8 pb-8">
|
||||
<SessionSharingSection />
|
||||
<ExternalBackendSection />
|
||||
{!tunnelDisabled && (
|
||||
<div className="space-y-4">
|
||||
<TunnelSection />
|
||||
<GatewaySettingsSection />
|
||||
</div>
|
||||
)}
|
||||
{!tunnelDisabled && <GatewaySettingsSection />}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,457 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../../ui/dialog';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import {
|
||||
Loader2,
|
||||
Copy,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Info,
|
||||
ExternalLink,
|
||||
QrCode,
|
||||
} from 'lucide-react';
|
||||
import { errorMessage } from '../../../utils/conversionUtils';
|
||||
import { startTunnel, stopTunnel, getTunnelStatus } from '../../../api/sdk.gen';
|
||||
import type { TunnelInfo } from '../../../api/types.gen';
|
||||
import { defineMessages, useIntl } from '../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
statusIdle: {
|
||||
id: 'tunnelSection.statusIdle',
|
||||
defaultMessage: 'Tunnel is not running',
|
||||
},
|
||||
statusStarting: {
|
||||
id: 'tunnelSection.statusStarting',
|
||||
defaultMessage: 'Starting tunnel...',
|
||||
},
|
||||
statusRunning: {
|
||||
id: 'tunnelSection.statusRunning',
|
||||
defaultMessage: 'Tunnel is active',
|
||||
},
|
||||
statusError: {
|
||||
id: 'tunnelSection.statusError',
|
||||
defaultMessage: 'Tunnel encountered an error',
|
||||
},
|
||||
statusDisabled: {
|
||||
id: 'tunnelSection.statusDisabled',
|
||||
defaultMessage: 'Tunnel is disabled',
|
||||
},
|
||||
mobileApp: {
|
||||
id: 'tunnelSection.mobileApp',
|
||||
defaultMessage: 'Mobile App',
|
||||
},
|
||||
previewFeature: {
|
||||
id: 'tunnelSection.previewFeature',
|
||||
defaultMessage: 'Preview feature:',
|
||||
},
|
||||
previewDescription: {
|
||||
id: 'tunnelSection.previewDescription',
|
||||
defaultMessage: 'Enable remote access to goose from mobile devices using secure tunneling.',
|
||||
},
|
||||
getIosApp: {
|
||||
id: 'tunnelSection.getIosApp',
|
||||
defaultMessage: 'Get the iOS app',
|
||||
},
|
||||
or: {
|
||||
id: 'tunnelSection.or',
|
||||
defaultMessage: 'or',
|
||||
},
|
||||
scanQrCode: {
|
||||
id: 'tunnelSection.scanQrCode',
|
||||
defaultMessage: 'scan QR code',
|
||||
},
|
||||
tunnelStatus: {
|
||||
id: 'tunnelSection.tunnelStatus',
|
||||
defaultMessage: 'Tunnel Status',
|
||||
},
|
||||
starting: {
|
||||
id: 'tunnelSection.starting',
|
||||
defaultMessage: 'Starting...',
|
||||
},
|
||||
showQrCode: {
|
||||
id: 'tunnelSection.showQrCode',
|
||||
defaultMessage: 'Show QR Code',
|
||||
},
|
||||
stopTunnel: {
|
||||
id: 'tunnelSection.stopTunnel',
|
||||
defaultMessage: 'Stop Tunnel',
|
||||
},
|
||||
retry: {
|
||||
id: 'tunnelSection.retry',
|
||||
defaultMessage: 'Retry',
|
||||
},
|
||||
startTunnel: {
|
||||
id: 'tunnelSection.startTunnel',
|
||||
defaultMessage: 'Start Tunnel',
|
||||
},
|
||||
url: {
|
||||
id: 'tunnelSection.url',
|
||||
defaultMessage: 'URL:',
|
||||
},
|
||||
mobileAppConnection: {
|
||||
id: 'tunnelSection.mobileAppConnection',
|
||||
defaultMessage: 'Mobile App Connection',
|
||||
},
|
||||
qrCodeInstructions: {
|
||||
id: 'tunnelSection.qrCodeInstructions',
|
||||
defaultMessage: 'Scan this QR code with the goose mobile app. Do not share this code with anyone else as it is for your personal access.',
|
||||
},
|
||||
connectionDetails: {
|
||||
id: 'tunnelSection.connectionDetails',
|
||||
defaultMessage: 'Connection Details',
|
||||
},
|
||||
tunnelUrl: {
|
||||
id: 'tunnelSection.tunnelUrl',
|
||||
defaultMessage: 'Tunnel URL',
|
||||
},
|
||||
secretKey: {
|
||||
id: 'tunnelSection.secretKey',
|
||||
defaultMessage: 'Secret Key',
|
||||
},
|
||||
close: {
|
||||
id: 'tunnelSection.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
downloadIosApp: {
|
||||
id: 'tunnelSection.downloadIosApp',
|
||||
defaultMessage: 'Download goose iOS App',
|
||||
},
|
||||
appStoreQrInstructions: {
|
||||
id: 'tunnelSection.appStoreQrInstructions',
|
||||
defaultMessage: 'Scan this QR code with your iPhone camera to install the goose mobile app from the App Store',
|
||||
},
|
||||
openInAppStore: {
|
||||
id: 'tunnelSection.openInAppStore',
|
||||
defaultMessage: 'Open in App Store',
|
||||
},
|
||||
failedToLoadStatus: {
|
||||
id: 'tunnelSection.failedToLoadStatus',
|
||||
defaultMessage: 'Failed to load tunnel status',
|
||||
},
|
||||
failedToStopTunnel: {
|
||||
id: 'tunnelSection.failedToStopTunnel',
|
||||
defaultMessage: 'Failed to stop tunnel',
|
||||
},
|
||||
failedToStartTunnel: {
|
||||
id: 'tunnelSection.failedToStartTunnel',
|
||||
defaultMessage: 'Failed to start tunnel',
|
||||
},
|
||||
});
|
||||
|
||||
const IOS_APP_STORE_URL = 'https://apps.apple.com/us/app/goose-ai/id6752889295';
|
||||
|
||||
const STATUS_MESSAGE_KEYS = {
|
||||
idle: 'statusIdle',
|
||||
starting: 'statusStarting',
|
||||
running: 'statusRunning',
|
||||
error: 'statusError',
|
||||
disabled: 'statusDisabled',
|
||||
} as const;
|
||||
|
||||
export default function TunnelSection() {
|
||||
const intl = useIntl();
|
||||
const [tunnelInfo, setTunnelInfo] = useState<TunnelInfo>({
|
||||
state: 'idle',
|
||||
url: '',
|
||||
hostname: '',
|
||||
secret: '',
|
||||
});
|
||||
const [showQRModal, setShowQRModal] = useState(false);
|
||||
const [showAppStoreQRModal, setShowAppStoreQRModal] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copiedUrl, setCopiedUrl] = useState(false);
|
||||
const [copiedSecret, setCopiedSecret] = useState(false);
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadTunnelInfo = async () => {
|
||||
try {
|
||||
const { data } = await getTunnelStatus();
|
||||
if (data) {
|
||||
setTunnelInfo(data);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = errorMessage(err, intl.formatMessage(i18n.failedToLoadStatus));
|
||||
setError(errorMsg);
|
||||
setTunnelInfo({ state: 'error', url: '', hostname: '', secret: '' });
|
||||
}
|
||||
};
|
||||
|
||||
loadTunnelInfo();
|
||||
}, [intl]);
|
||||
|
||||
const handleToggleTunnel = async () => {
|
||||
if (tunnelInfo.state === 'running') {
|
||||
try {
|
||||
await stopTunnel();
|
||||
setTunnelInfo({ state: 'idle', url: '', hostname: '', secret: '' });
|
||||
setShowQRModal(false);
|
||||
} catch (err) {
|
||||
setError(errorMessage(err, intl.formatMessage(i18n.failedToStopTunnel)));
|
||||
try {
|
||||
const { data } = await getTunnelStatus();
|
||||
if (data) {
|
||||
setTunnelInfo(data);
|
||||
}
|
||||
} catch (statusErr) {
|
||||
console.error('Failed to fetch tunnel status after stop error:', statusErr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setError(null);
|
||||
setTunnelInfo({ state: 'starting', url: '', hostname: '', secret: '' });
|
||||
|
||||
try {
|
||||
const { data } = await startTunnel();
|
||||
if (data) {
|
||||
setTunnelInfo(data);
|
||||
setShowQRModal(true);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = errorMessage(err, intl.formatMessage(i18n.failedToStartTunnel));
|
||||
setError(errorMsg);
|
||||
setTunnelInfo({ state: 'error', url: '', hostname: '', secret: '' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string, type: 'url' | 'secret') => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (type === 'url') {
|
||||
setCopiedUrl(true);
|
||||
setTimeout(() => setCopiedUrl(false), 2000);
|
||||
} else {
|
||||
setCopiedSecret(true);
|
||||
setTimeout(() => setCopiedSecret(false), 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const getQRCodeData = () => {
|
||||
if (tunnelInfo.state !== 'running') return '';
|
||||
|
||||
const configJson = JSON.stringify({
|
||||
url: tunnelInfo.url,
|
||||
secret: tunnelInfo.secret,
|
||||
});
|
||||
const urlEncodedConfig = encodeURIComponent(configJson);
|
||||
return `goosechat://configure?data=${urlEncodedConfig}`;
|
||||
};
|
||||
|
||||
if (tunnelInfo.state === 'disabled') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="rounded-lg">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="mb-1">{intl.formatMessage(i18n.mobileApp)}</CardTitle>
|
||||
<CardDescription className="flex flex-col gap-2">
|
||||
<div className="flex items-start gap-2 p-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded">
|
||||
<Info className="h-4 w-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-xs text-blue-800 dark:text-blue-200">
|
||||
<strong>{intl.formatMessage(i18n.previewFeature)}</strong> {intl.formatMessage(i18n.previewDescription)}{' '}
|
||||
<a
|
||||
href={IOS_APP_STORE_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 underline hover:no-underline"
|
||||
>
|
||||
{intl.formatMessage(i18n.getIosApp)}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
{' '}{intl.formatMessage(i18n.or)}{' '}
|
||||
<button
|
||||
onClick={() => setShowAppStoreQRModal(true)}
|
||||
className="inline-flex items-center gap-1 underline hover:no-underline"
|
||||
>
|
||||
{intl.formatMessage(i18n.scanQrCode)}
|
||||
<QrCode className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 px-4 space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded text-sm text-red-800 dark:text-red-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-text-primary text-xs">{intl.formatMessage(i18n.tunnelStatus)}</h3>
|
||||
<p className="text-xs text-text-secondary max-w-md mt-[2px]">
|
||||
{intl.formatMessage(i18n[STATUS_MESSAGE_KEYS[tunnelInfo.state]])}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{tunnelInfo.state === 'starting' ? (
|
||||
<Button disabled variant="secondary" size="sm">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{intl.formatMessage(i18n.starting)}
|
||||
</Button>
|
||||
) : tunnelInfo.state === 'running' ? (
|
||||
<>
|
||||
<Button onClick={() => setShowQRModal(true)} variant="default" size="sm">
|
||||
{intl.formatMessage(i18n.showQrCode)}
|
||||
</Button>
|
||||
<Button onClick={handleToggleTunnel} variant="destructive" size="sm">
|
||||
{intl.formatMessage(i18n.stopTunnel)}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={handleToggleTunnel} variant="default" size="sm">
|
||||
{tunnelInfo.state === 'error' ? intl.formatMessage(i18n.retry) : intl.formatMessage(i18n.startTunnel)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tunnelInfo.state === 'running' && (
|
||||
<div className="p-3 bg-green-100 dark:bg-green-900/20 border border-green-300 dark:border-green-800 rounded">
|
||||
<p className="text-xs text-green-800 dark:text-green-200">
|
||||
<strong>{intl.formatMessage(i18n.url)}</strong> {tunnelInfo.url}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={showQRModal} onOpenChange={setShowQRModal}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{intl.formatMessage(i18n.mobileAppConnection)}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{tunnelInfo.state === 'running' && (
|
||||
<div className="py-4 space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<div className="p-4 bg-white rounded-lg">
|
||||
<QRCodeSVG value={getQRCodeData()} size={200} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-text-secondary">
|
||||
{intl.formatMessage(i18n.qrCodeInstructions)}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="flex items-center justify-between w-full text-sm font-medium hover:opacity-70 transition-opacity"
|
||||
>
|
||||
<span>{intl.formatMessage(i18n.connectionDetails)}</span>
|
||||
{showDetails ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{showDetails && (
|
||||
<div className="mt-3 space-y-3">
|
||||
<div>
|
||||
<h3 className="text-xs font-medium mb-1 text-text-secondary">{intl.formatMessage(i18n.tunnelUrl)}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs break-all overflow-hidden">
|
||||
{tunnelInfo.url}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="flex-shrink-0"
|
||||
onClick={() => tunnelInfo.url && copyToClipboard(tunnelInfo.url, 'url')}
|
||||
>
|
||||
{copiedUrl ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs font-medium mb-1 text-text-secondary">{intl.formatMessage(i18n.secretKey)}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs break-all overflow-hidden">
|
||||
{tunnelInfo.secret}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="flex-shrink-0"
|
||||
onClick={() =>
|
||||
tunnelInfo.secret && copyToClipboard(tunnelInfo.secret, 'secret')
|
||||
}
|
||||
>
|
||||
{copiedSecret ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowQRModal(false)}>
|
||||
{intl.formatMessage(i18n.close)}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleToggleTunnel}>
|
||||
{intl.formatMessage(i18n.stopTunnel)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={showAppStoreQRModal} onOpenChange={setShowAppStoreQRModal}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{intl.formatMessage(i18n.downloadIosApp)}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4 space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<div className="p-4 bg-white rounded-lg">
|
||||
<QRCodeSVG value={IOS_APP_STORE_URL} size={200} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-text-secondary">
|
||||
{intl.formatMessage(i18n.appStoreQrInstructions)}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<a
|
||||
href={IOS_APP_STORE_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
{intl.formatMessage(i18n.openInAppStore)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowAppStoreQRModal(false)}>
|
||||
{intl.formatMessage(i18n.close)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -4610,96 +4610,6 @@
|
|||
"toolConfirmation.gooseWouldLikeToCallWithName": {
|
||||
"defaultMessage": "Goose would like to call {toolName}. Allow?"
|
||||
},
|
||||
"tunnelSection.appStoreQrInstructions": {
|
||||
"defaultMessage": "Scan this QR code with your iPhone camera to install the goose mobile app from the App Store"
|
||||
},
|
||||
"tunnelSection.close": {
|
||||
"defaultMessage": "Close"
|
||||
},
|
||||
"tunnelSection.connectionDetails": {
|
||||
"defaultMessage": "Connection Details"
|
||||
},
|
||||
"tunnelSection.downloadIosApp": {
|
||||
"defaultMessage": "Download goose iOS App"
|
||||
},
|
||||
"tunnelSection.failedToLoadStatus": {
|
||||
"defaultMessage": "Failed to load tunnel status"
|
||||
},
|
||||
"tunnelSection.failedToStartTunnel": {
|
||||
"defaultMessage": "Failed to start tunnel"
|
||||
},
|
||||
"tunnelSection.failedToStopTunnel": {
|
||||
"defaultMessage": "Failed to stop tunnel"
|
||||
},
|
||||
"tunnelSection.getIosApp": {
|
||||
"defaultMessage": "Get the iOS app"
|
||||
},
|
||||
"tunnelSection.mobileApp": {
|
||||
"defaultMessage": "Mobile App"
|
||||
},
|
||||
"tunnelSection.mobileAppConnection": {
|
||||
"defaultMessage": "Mobile App Connection"
|
||||
},
|
||||
"tunnelSection.openInAppStore": {
|
||||
"defaultMessage": "Open in App Store"
|
||||
},
|
||||
"tunnelSection.or": {
|
||||
"defaultMessage": "or"
|
||||
},
|
||||
"tunnelSection.previewDescription": {
|
||||
"defaultMessage": "Enable remote access to goose from mobile devices using secure tunneling."
|
||||
},
|
||||
"tunnelSection.previewFeature": {
|
||||
"defaultMessage": "Preview feature:"
|
||||
},
|
||||
"tunnelSection.qrCodeInstructions": {
|
||||
"defaultMessage": "Scan this QR code with the goose mobile app. Do not share this code with anyone else as it is for your personal access."
|
||||
},
|
||||
"tunnelSection.retry": {
|
||||
"defaultMessage": "Retry"
|
||||
},
|
||||
"tunnelSection.scanQrCode": {
|
||||
"defaultMessage": "scan QR code"
|
||||
},
|
||||
"tunnelSection.secretKey": {
|
||||
"defaultMessage": "Secret Key"
|
||||
},
|
||||
"tunnelSection.showQrCode": {
|
||||
"defaultMessage": "Show QR Code"
|
||||
},
|
||||
"tunnelSection.startTunnel": {
|
||||
"defaultMessage": "Start Tunnel"
|
||||
},
|
||||
"tunnelSection.starting": {
|
||||
"defaultMessage": "Starting..."
|
||||
},
|
||||
"tunnelSection.statusDisabled": {
|
||||
"defaultMessage": "Tunnel is disabled"
|
||||
},
|
||||
"tunnelSection.statusError": {
|
||||
"defaultMessage": "Tunnel encountered an error"
|
||||
},
|
||||
"tunnelSection.statusIdle": {
|
||||
"defaultMessage": "Tunnel is not running"
|
||||
},
|
||||
"tunnelSection.statusRunning": {
|
||||
"defaultMessage": "Tunnel is active"
|
||||
},
|
||||
"tunnelSection.statusStarting": {
|
||||
"defaultMessage": "Starting tunnel..."
|
||||
},
|
||||
"tunnelSection.stopTunnel": {
|
||||
"defaultMessage": "Stop Tunnel"
|
||||
},
|
||||
"tunnelSection.tunnelStatus": {
|
||||
"defaultMessage": "Tunnel Status"
|
||||
},
|
||||
"tunnelSection.tunnelUrl": {
|
||||
"defaultMessage": "Tunnel URL"
|
||||
},
|
||||
"tunnelSection.url": {
|
||||
"defaultMessage": "URL:"
|
||||
},
|
||||
"updateSection.autoDownload": {
|
||||
"defaultMessage": "Goose will download the update in the background and install it the next time you quit or restart."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4610,96 +4610,6 @@
|
|||
"toolConfirmation.gooseWouldLikeToCallWithName": {
|
||||
"defaultMessage": "Goose quiere invocar {toolName}. ¿Permitir?"
|
||||
},
|
||||
"tunnelSection.appStoreQrInstructions": {
|
||||
"defaultMessage": "Escanea este código QR con la cámara de tu iPhone para instalar la app móvil de goose desde la App Store"
|
||||
},
|
||||
"tunnelSection.close": {
|
||||
"defaultMessage": "Cerrar"
|
||||
},
|
||||
"tunnelSection.connectionDetails": {
|
||||
"defaultMessage": "Detalles de conexión"
|
||||
},
|
||||
"tunnelSection.downloadIosApp": {
|
||||
"defaultMessage": "Descargar la app de goose para iOS"
|
||||
},
|
||||
"tunnelSection.failedToLoadStatus": {
|
||||
"defaultMessage": "No se pudo cargar el estado del túnel"
|
||||
},
|
||||
"tunnelSection.failedToStartTunnel": {
|
||||
"defaultMessage": "No se pudo iniciar el túnel"
|
||||
},
|
||||
"tunnelSection.failedToStopTunnel": {
|
||||
"defaultMessage": "No se pudo detener el túnel"
|
||||
},
|
||||
"tunnelSection.getIosApp": {
|
||||
"defaultMessage": "Obtén la app para iOS"
|
||||
},
|
||||
"tunnelSection.mobileApp": {
|
||||
"defaultMessage": "App móvil"
|
||||
},
|
||||
"tunnelSection.mobileAppConnection": {
|
||||
"defaultMessage": "Conexión de la app móvil"
|
||||
},
|
||||
"tunnelSection.openInAppStore": {
|
||||
"defaultMessage": "Abrir en la App Store"
|
||||
},
|
||||
"tunnelSection.or": {
|
||||
"defaultMessage": "o"
|
||||
},
|
||||
"tunnelSection.previewDescription": {
|
||||
"defaultMessage": "Activa el acceso remoto a goose desde dispositivos móviles mediante un túnel seguro."
|
||||
},
|
||||
"tunnelSection.previewFeature": {
|
||||
"defaultMessage": "Función en vista previa:"
|
||||
},
|
||||
"tunnelSection.qrCodeInstructions": {
|
||||
"defaultMessage": "Escanea este código QR con la app móvil de goose. No compartas este código con nadie, ya que es para tu acceso personal."
|
||||
},
|
||||
"tunnelSection.retry": {
|
||||
"defaultMessage": "Reintentar"
|
||||
},
|
||||
"tunnelSection.scanQrCode": {
|
||||
"defaultMessage": "escanear código QR"
|
||||
},
|
||||
"tunnelSection.secretKey": {
|
||||
"defaultMessage": "Clave secreta"
|
||||
},
|
||||
"tunnelSection.showQrCode": {
|
||||
"defaultMessage": "Mostrar código QR"
|
||||
},
|
||||
"tunnelSection.startTunnel": {
|
||||
"defaultMessage": "Iniciar túnel"
|
||||
},
|
||||
"tunnelSection.starting": {
|
||||
"defaultMessage": "Iniciando..."
|
||||
},
|
||||
"tunnelSection.statusDisabled": {
|
||||
"defaultMessage": "El túnel está desactivado"
|
||||
},
|
||||
"tunnelSection.statusError": {
|
||||
"defaultMessage": "El túnel encontró un error"
|
||||
},
|
||||
"tunnelSection.statusIdle": {
|
||||
"defaultMessage": "El túnel no está en ejecución"
|
||||
},
|
||||
"tunnelSection.statusRunning": {
|
||||
"defaultMessage": "El túnel está activo"
|
||||
},
|
||||
"tunnelSection.statusStarting": {
|
||||
"defaultMessage": "Iniciando el túnel..."
|
||||
},
|
||||
"tunnelSection.stopTunnel": {
|
||||
"defaultMessage": "Detener túnel"
|
||||
},
|
||||
"tunnelSection.tunnelStatus": {
|
||||
"defaultMessage": "Estado del túnel"
|
||||
},
|
||||
"tunnelSection.tunnelUrl": {
|
||||
"defaultMessage": "URL del túnel"
|
||||
},
|
||||
"tunnelSection.url": {
|
||||
"defaultMessage": "URL:"
|
||||
},
|
||||
"updateSection.autoDownload": {
|
||||
"defaultMessage": "La actualización se descargará automáticamente en segundo plano."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4610,96 +4610,6 @@
|
|||
"toolConfirmation.gooseWouldLikeToCallWithName": {
|
||||
"defaultMessage": "Goose {toolName} को कॉल करना चाहेंगे। अनुमति दें?"
|
||||
},
|
||||
"tunnelSection.appStoreQrInstructions": {
|
||||
"defaultMessage": "ऐप स्टोर से goose मोबाइल ऐप इंस्टॉल करने के लिए इस QR कोड को अपने iPhone कैमरे से स्कैन करें"
|
||||
},
|
||||
"tunnelSection.close": {
|
||||
"defaultMessage": "बंद करें"
|
||||
},
|
||||
"tunnelSection.connectionDetails": {
|
||||
"defaultMessage": "कनेक्शन विवरण"
|
||||
},
|
||||
"tunnelSection.downloadIosApp": {
|
||||
"defaultMessage": "goose iOS ऐप डाउनलोड करें"
|
||||
},
|
||||
"tunnelSection.failedToLoadStatus": {
|
||||
"defaultMessage": "सुरंग स्थिति लोड करने में विफल"
|
||||
},
|
||||
"tunnelSection.failedToStartTunnel": {
|
||||
"defaultMessage": "सुरंग प्रारंभ करने में विफल"
|
||||
},
|
||||
"tunnelSection.failedToStopTunnel": {
|
||||
"defaultMessage": "सुरंग रोकने में विफल"
|
||||
},
|
||||
"tunnelSection.getIosApp": {
|
||||
"defaultMessage": "आईओएस ऐप प्राप्त करें"
|
||||
},
|
||||
"tunnelSection.mobileApp": {
|
||||
"defaultMessage": "मोबाइल ऐप"
|
||||
},
|
||||
"tunnelSection.mobileAppConnection": {
|
||||
"defaultMessage": "मोबाइल ऐप कनेक्शन"
|
||||
},
|
||||
"tunnelSection.openInAppStore": {
|
||||
"defaultMessage": "ऐप स्टोर में खोलें"
|
||||
},
|
||||
"tunnelSection.or": {
|
||||
"defaultMessage": "या"
|
||||
},
|
||||
"tunnelSection.previewDescription": {
|
||||
"defaultMessage": "सुरक्षित टनलिंग का उपयोग करके मोबाइल उपकरणों से goose तक दूरस्थ पहुंच सक्षम करें।"
|
||||
},
|
||||
"tunnelSection.previewFeature": {
|
||||
"defaultMessage": "पूर्वावलोकन सुविधा:"
|
||||
},
|
||||
"tunnelSection.qrCodeInstructions": {
|
||||
"defaultMessage": "इस QR कोड को goose मोबाइल ऐप से स्कैन करें। इस कोड को किसी और के साथ साझा न करें क्योंकि यह आपकी व्यक्तिगत पहुंच के लिए है।"
|
||||
},
|
||||
"tunnelSection.retry": {
|
||||
"defaultMessage": "पुनः प्रयास करें"
|
||||
},
|
||||
"tunnelSection.scanQrCode": {
|
||||
"defaultMessage": "QR कोड स्कैन करें"
|
||||
},
|
||||
"tunnelSection.secretKey": {
|
||||
"defaultMessage": "गुप्त कुंजी"
|
||||
},
|
||||
"tunnelSection.showQrCode": {
|
||||
"defaultMessage": "QR कोड दिखाएँ"
|
||||
},
|
||||
"tunnelSection.startTunnel": {
|
||||
"defaultMessage": "सुरंग प्रारंभ करें"
|
||||
},
|
||||
"tunnelSection.starting": {
|
||||
"defaultMessage": "प्रारंभ..."
|
||||
},
|
||||
"tunnelSection.statusDisabled": {
|
||||
"defaultMessage": "सुरंग अक्षम है"
|
||||
},
|
||||
"tunnelSection.statusError": {
|
||||
"defaultMessage": "टनल में एक त्रुटि आई"
|
||||
},
|
||||
"tunnelSection.statusIdle": {
|
||||
"defaultMessage": "सुरंग नहीं चल रही है"
|
||||
},
|
||||
"tunnelSection.statusRunning": {
|
||||
"defaultMessage": "सुरंग सक्रिय है"
|
||||
},
|
||||
"tunnelSection.statusStarting": {
|
||||
"defaultMessage": "सुरंग शुरू हो रही है..."
|
||||
},
|
||||
"tunnelSection.stopTunnel": {
|
||||
"defaultMessage": "सुरंग बंद करो"
|
||||
},
|
||||
"tunnelSection.tunnelStatus": {
|
||||
"defaultMessage": "सुरंग की स्थिति"
|
||||
},
|
||||
"tunnelSection.tunnelUrl": {
|
||||
"defaultMessage": "सुरंग URL"
|
||||
},
|
||||
"tunnelSection.url": {
|
||||
"defaultMessage": "URL:"
|
||||
},
|
||||
"updateSection.autoDownload": {
|
||||
"defaultMessage": "अपडेट बैकग्राउंड में अपने आप डाउनलोड हो जाएगा."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4610,96 +4610,6 @@
|
|||
"toolConfirmation.gooseWouldLikeToCallWithName": {
|
||||
"defaultMessage": "Gooseが{toolName}を実行しようとしています。許可しますか?"
|
||||
},
|
||||
"tunnelSection.appStoreQrInstructions": {
|
||||
"defaultMessage": "このQRコードをiPhoneのカメラでスキャンして、App Storeからgooseモバイルアプリをインストールしてください"
|
||||
},
|
||||
"tunnelSection.close": {
|
||||
"defaultMessage": "閉じる"
|
||||
},
|
||||
"tunnelSection.connectionDetails": {
|
||||
"defaultMessage": "接続の詳細"
|
||||
},
|
||||
"tunnelSection.downloadIosApp": {
|
||||
"defaultMessage": "goose iOSアプリをダウンロード"
|
||||
},
|
||||
"tunnelSection.failedToLoadStatus": {
|
||||
"defaultMessage": "トンネルステータスの読み込みに失敗しました"
|
||||
},
|
||||
"tunnelSection.failedToStartTunnel": {
|
||||
"defaultMessage": "トンネルの開始に失敗しました"
|
||||
},
|
||||
"tunnelSection.failedToStopTunnel": {
|
||||
"defaultMessage": "トンネルの停止に失敗しました"
|
||||
},
|
||||
"tunnelSection.getIosApp": {
|
||||
"defaultMessage": "iOSアプリを入手"
|
||||
},
|
||||
"tunnelSection.mobileApp": {
|
||||
"defaultMessage": "モバイルアプリ"
|
||||
},
|
||||
"tunnelSection.mobileAppConnection": {
|
||||
"defaultMessage": "モバイルアプリ接続"
|
||||
},
|
||||
"tunnelSection.openInAppStore": {
|
||||
"defaultMessage": "App Storeで開く"
|
||||
},
|
||||
"tunnelSection.or": {
|
||||
"defaultMessage": "または"
|
||||
},
|
||||
"tunnelSection.previewDescription": {
|
||||
"defaultMessage": "安全なトンネリングを使用して、モバイルデバイスからgooseへリモートアクセスできるようにします。"
|
||||
},
|
||||
"tunnelSection.previewFeature": {
|
||||
"defaultMessage": "プレビュー機能:"
|
||||
},
|
||||
"tunnelSection.qrCodeInstructions": {
|
||||
"defaultMessage": "このQRコードをgooseモバイルアプリでスキャンしてください。このコードは個人アクセス用です。他人と共有しないでください。"
|
||||
},
|
||||
"tunnelSection.retry": {
|
||||
"defaultMessage": "再試行"
|
||||
},
|
||||
"tunnelSection.scanQrCode": {
|
||||
"defaultMessage": "QRコードをスキャン"
|
||||
},
|
||||
"tunnelSection.secretKey": {
|
||||
"defaultMessage": "シークレットキー"
|
||||
},
|
||||
"tunnelSection.showQrCode": {
|
||||
"defaultMessage": "QRコードを表示"
|
||||
},
|
||||
"tunnelSection.startTunnel": {
|
||||
"defaultMessage": "トンネルを開始"
|
||||
},
|
||||
"tunnelSection.starting": {
|
||||
"defaultMessage": "開始中..."
|
||||
},
|
||||
"tunnelSection.statusDisabled": {
|
||||
"defaultMessage": "トンネルは無効です"
|
||||
},
|
||||
"tunnelSection.statusError": {
|
||||
"defaultMessage": "トンネルでエラーが発生しました"
|
||||
},
|
||||
"tunnelSection.statusIdle": {
|
||||
"defaultMessage": "トンネルは実行されていません"
|
||||
},
|
||||
"tunnelSection.statusRunning": {
|
||||
"defaultMessage": "トンネルは実行中です"
|
||||
},
|
||||
"tunnelSection.statusStarting": {
|
||||
"defaultMessage": "トンネルを開始中..."
|
||||
},
|
||||
"tunnelSection.stopTunnel": {
|
||||
"defaultMessage": "トンネルを停止"
|
||||
},
|
||||
"tunnelSection.tunnelStatus": {
|
||||
"defaultMessage": "トンネルステータス"
|
||||
},
|
||||
"tunnelSection.tunnelUrl": {
|
||||
"defaultMessage": "トンネルURL"
|
||||
},
|
||||
"tunnelSection.url": {
|
||||
"defaultMessage": "URL:"
|
||||
},
|
||||
"updateSection.autoDownload": {
|
||||
"defaultMessage": "アップデートはバックグラウンドで自動的にダウンロードされます。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4610,96 +4610,6 @@
|
|||
"toolConfirmation.gooseWouldLikeToCallWithName": {
|
||||
"defaultMessage": "goose가 {toolName}을 호출하려고 합니다. 허용하시겠습니까?"
|
||||
},
|
||||
"tunnelSection.appStoreQrInstructions": {
|
||||
"defaultMessage": "QR 코드를 iPhone 카메라로 스캔하여 App Store에서 goose 모바일 앱을 설치하세요."
|
||||
},
|
||||
"tunnelSection.close": {
|
||||
"defaultMessage": "닫기"
|
||||
},
|
||||
"tunnelSection.connectionDetails": {
|
||||
"defaultMessage": "연결 세부정보"
|
||||
},
|
||||
"tunnelSection.downloadIosApp": {
|
||||
"defaultMessage": "goose iOS 앱 다운로드"
|
||||
},
|
||||
"tunnelSection.failedToLoadStatus": {
|
||||
"defaultMessage": "터널 상태를 로드하지 못했습니다."
|
||||
},
|
||||
"tunnelSection.failedToStartTunnel": {
|
||||
"defaultMessage": "터널을 시작하지 못했습니다."
|
||||
},
|
||||
"tunnelSection.failedToStopTunnel": {
|
||||
"defaultMessage": "터널을 중지하지 못했습니다."
|
||||
},
|
||||
"tunnelSection.getIosApp": {
|
||||
"defaultMessage": "iOS 앱 받기"
|
||||
},
|
||||
"tunnelSection.mobileApp": {
|
||||
"defaultMessage": "모바일 앱"
|
||||
},
|
||||
"tunnelSection.mobileAppConnection": {
|
||||
"defaultMessage": "모바일 앱 연결"
|
||||
},
|
||||
"tunnelSection.openInAppStore": {
|
||||
"defaultMessage": "앱 스토어에서 열기"
|
||||
},
|
||||
"tunnelSection.or": {
|
||||
"defaultMessage": "또는"
|
||||
},
|
||||
"tunnelSection.previewDescription": {
|
||||
"defaultMessage": "보안 터널링을 사용해 모바일 기기에서 goose에 원격으로 접속할 수 있습니다."
|
||||
},
|
||||
"tunnelSection.previewFeature": {
|
||||
"defaultMessage": "미리 보기 기능:"
|
||||
},
|
||||
"tunnelSection.qrCodeInstructions": {
|
||||
"defaultMessage": "QR 코드를 goose 모바일 앱으로 스캔하세요. 이 코드는 개인 액세스용이므로 다른 사람과 공유하지 마세요."
|
||||
},
|
||||
"tunnelSection.retry": {
|
||||
"defaultMessage": "다시 시도"
|
||||
},
|
||||
"tunnelSection.scanQrCode": {
|
||||
"defaultMessage": "QR 코드 스캔"
|
||||
},
|
||||
"tunnelSection.secretKey": {
|
||||
"defaultMessage": "비밀 키"
|
||||
},
|
||||
"tunnelSection.showQrCode": {
|
||||
"defaultMessage": "QR 코드 표시"
|
||||
},
|
||||
"tunnelSection.startTunnel": {
|
||||
"defaultMessage": "터널 시작"
|
||||
},
|
||||
"tunnelSection.starting": {
|
||||
"defaultMessage": "시작 중..."
|
||||
},
|
||||
"tunnelSection.statusDisabled": {
|
||||
"defaultMessage": "터널이 비활성화되었습니다."
|
||||
},
|
||||
"tunnelSection.statusError": {
|
||||
"defaultMessage": "터널에 오류가 발생했습니다."
|
||||
},
|
||||
"tunnelSection.statusIdle": {
|
||||
"defaultMessage": "터널이 실행 중이 아닙니다."
|
||||
},
|
||||
"tunnelSection.statusRunning": {
|
||||
"defaultMessage": "터널이 활성화되었습니다"
|
||||
},
|
||||
"tunnelSection.statusStarting": {
|
||||
"defaultMessage": "터널 시작 중..."
|
||||
},
|
||||
"tunnelSection.stopTunnel": {
|
||||
"defaultMessage": "터널 중지"
|
||||
},
|
||||
"tunnelSection.tunnelStatus": {
|
||||
"defaultMessage": "터널 상태"
|
||||
},
|
||||
"tunnelSection.tunnelUrl": {
|
||||
"defaultMessage": "터널 URL"
|
||||
},
|
||||
"tunnelSection.url": {
|
||||
"defaultMessage": "URL:"
|
||||
},
|
||||
"updateSection.autoDownload": {
|
||||
"defaultMessage": "업데이트는 백그라운드에서 자동으로 다운로드됩니다."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4610,96 +4610,6 @@
|
|||
"toolConfirmation.gooseWouldLikeToCallWithName": {
|
||||
"defaultMessage": "Goose хочет вызвать {toolName}. Разрешить?"
|
||||
},
|
||||
"tunnelSection.appStoreQrInstructions": {
|
||||
"defaultMessage": "Отсканируйте этот QR-код камерой iPhone, чтобы установить мобильное приложение goose из App Store"
|
||||
},
|
||||
"tunnelSection.close": {
|
||||
"defaultMessage": "Закрыть"
|
||||
},
|
||||
"tunnelSection.connectionDetails": {
|
||||
"defaultMessage": "Сведения о подключении"
|
||||
},
|
||||
"tunnelSection.downloadIosApp": {
|
||||
"defaultMessage": "Скачать приложение goose для iOS"
|
||||
},
|
||||
"tunnelSection.failedToLoadStatus": {
|
||||
"defaultMessage": "Не удалось загрузить статус туннеля"
|
||||
},
|
||||
"tunnelSection.failedToStartTunnel": {
|
||||
"defaultMessage": "Не удалось запустить туннель"
|
||||
},
|
||||
"tunnelSection.failedToStopTunnel": {
|
||||
"defaultMessage": "Не удалось остановить туннель"
|
||||
},
|
||||
"tunnelSection.getIosApp": {
|
||||
"defaultMessage": "Получить приложение iOS"
|
||||
},
|
||||
"tunnelSection.mobileApp": {
|
||||
"defaultMessage": "Мобильное приложение"
|
||||
},
|
||||
"tunnelSection.mobileAppConnection": {
|
||||
"defaultMessage": "Подключение мобильного приложения"
|
||||
},
|
||||
"tunnelSection.openInAppStore": {
|
||||
"defaultMessage": "Открыть в App Store"
|
||||
},
|
||||
"tunnelSection.or": {
|
||||
"defaultMessage": "или"
|
||||
},
|
||||
"tunnelSection.previewDescription": {
|
||||
"defaultMessage": "Включите удаленный доступ к goose с мобильных устройств через защищенное туннелирование."
|
||||
},
|
||||
"tunnelSection.previewFeature": {
|
||||
"defaultMessage": "Предварительная функция:"
|
||||
},
|
||||
"tunnelSection.qrCodeInstructions": {
|
||||
"defaultMessage": "Отсканируйте этот QR-код мобильным приложением goose. Не делитесь этим кодом: он предназначен для вашего личного доступа."
|
||||
},
|
||||
"tunnelSection.retry": {
|
||||
"defaultMessage": "Повторить"
|
||||
},
|
||||
"tunnelSection.scanQrCode": {
|
||||
"defaultMessage": "сканировать QR-код"
|
||||
},
|
||||
"tunnelSection.secretKey": {
|
||||
"defaultMessage": "Секретный ключ"
|
||||
},
|
||||
"tunnelSection.showQrCode": {
|
||||
"defaultMessage": "Показать QR-код"
|
||||
},
|
||||
"tunnelSection.startTunnel": {
|
||||
"defaultMessage": "Запустить туннель"
|
||||
},
|
||||
"tunnelSection.starting": {
|
||||
"defaultMessage": "Запуск..."
|
||||
},
|
||||
"tunnelSection.statusDisabled": {
|
||||
"defaultMessage": "Туннель отключен"
|
||||
},
|
||||
"tunnelSection.statusError": {
|
||||
"defaultMessage": "В туннеле возникла ошибка"
|
||||
},
|
||||
"tunnelSection.statusIdle": {
|
||||
"defaultMessage": "Туннель не запущен"
|
||||
},
|
||||
"tunnelSection.statusRunning": {
|
||||
"defaultMessage": "Туннель активен"
|
||||
},
|
||||
"tunnelSection.statusStarting": {
|
||||
"defaultMessage": "Запуск туннеля..."
|
||||
},
|
||||
"tunnelSection.stopTunnel": {
|
||||
"defaultMessage": "Остановить туннель"
|
||||
},
|
||||
"tunnelSection.tunnelStatus": {
|
||||
"defaultMessage": "Статус туннеля"
|
||||
},
|
||||
"tunnelSection.tunnelUrl": {
|
||||
"defaultMessage": "URL туннеля"
|
||||
},
|
||||
"tunnelSection.url": {
|
||||
"defaultMessage": "URL:"
|
||||
},
|
||||
"updateSection.autoDownload": {
|
||||
"defaultMessage": "Обновление будет автоматически скачано в фоновом режиме."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4610,96 +4610,6 @@
|
|||
"toolConfirmation.gooseWouldLikeToCallWithName": {
|
||||
"defaultMessage": "Goose, {toolName}'yi aramak istiyor. İzin vermek?"
|
||||
},
|
||||
"tunnelSection.appStoreQrInstructions": {
|
||||
"defaultMessage": "App Store'dan goose mobil uygulamasını yüklemek için bu QR kodunu iPhone kameranızla tarayın"
|
||||
},
|
||||
"tunnelSection.close": {
|
||||
"defaultMessage": "Kapat"
|
||||
},
|
||||
"tunnelSection.connectionDetails": {
|
||||
"defaultMessage": "Bağlantı Detayları"
|
||||
},
|
||||
"tunnelSection.downloadIosApp": {
|
||||
"defaultMessage": "goose iOS Uygulamasını İndirin"
|
||||
},
|
||||
"tunnelSection.failedToLoadStatus": {
|
||||
"defaultMessage": "Tünel durumu yüklenemedi"
|
||||
},
|
||||
"tunnelSection.failedToStartTunnel": {
|
||||
"defaultMessage": "Tünel başlatılamadı"
|
||||
},
|
||||
"tunnelSection.failedToStopTunnel": {
|
||||
"defaultMessage": "Tünel durdurulamadı"
|
||||
},
|
||||
"tunnelSection.getIosApp": {
|
||||
"defaultMessage": "iOS uygulamasını edinin"
|
||||
},
|
||||
"tunnelSection.mobileApp": {
|
||||
"defaultMessage": "Mobil Uygulama"
|
||||
},
|
||||
"tunnelSection.mobileAppConnection": {
|
||||
"defaultMessage": "Mobil Uygulama Bağlantısı"
|
||||
},
|
||||
"tunnelSection.openInAppStore": {
|
||||
"defaultMessage": "App Store'da aç"
|
||||
},
|
||||
"tunnelSection.or": {
|
||||
"defaultMessage": "veya"
|
||||
},
|
||||
"tunnelSection.previewDescription": {
|
||||
"defaultMessage": "Güvenli tünellemeyi kullanarak mobil cihazlardan goose'a uzaktan erişimi etkinleştirin."
|
||||
},
|
||||
"tunnelSection.previewFeature": {
|
||||
"defaultMessage": "Önizleme özelliği:"
|
||||
},
|
||||
"tunnelSection.qrCodeInstructions": {
|
||||
"defaultMessage": "Bu QR kodunu goose mobil uygulamasıyla tarayın. Bu kodu kişisel erişiminiz için olduğundan başkasıyla paylaşmayınız."
|
||||
},
|
||||
"tunnelSection.retry": {
|
||||
"defaultMessage": "Yeniden dene"
|
||||
},
|
||||
"tunnelSection.scanQrCode": {
|
||||
"defaultMessage": "QR kodunu tara"
|
||||
},
|
||||
"tunnelSection.secretKey": {
|
||||
"defaultMessage": "Gizli Anahtar"
|
||||
},
|
||||
"tunnelSection.showQrCode": {
|
||||
"defaultMessage": "QR Kodunu Göster"
|
||||
},
|
||||
"tunnelSection.startTunnel": {
|
||||
"defaultMessage": "Tüneli Başlat"
|
||||
},
|
||||
"tunnelSection.starting": {
|
||||
"defaultMessage": "Başlıyor..."
|
||||
},
|
||||
"tunnelSection.statusDisabled": {
|
||||
"defaultMessage": "Tünel devre dışı"
|
||||
},
|
||||
"tunnelSection.statusError": {
|
||||
"defaultMessage": "Tünel bir hatayla karşılaştı"
|
||||
},
|
||||
"tunnelSection.statusIdle": {
|
||||
"defaultMessage": "Tünel çalışmıyor"
|
||||
},
|
||||
"tunnelSection.statusRunning": {
|
||||
"defaultMessage": "Tünel aktif"
|
||||
},
|
||||
"tunnelSection.statusStarting": {
|
||||
"defaultMessage": "Tünel başlatılıyor..."
|
||||
},
|
||||
"tunnelSection.stopTunnel": {
|
||||
"defaultMessage": "Tüneli Durdur"
|
||||
},
|
||||
"tunnelSection.tunnelStatus": {
|
||||
"defaultMessage": "Tünel Durumu"
|
||||
},
|
||||
"tunnelSection.tunnelUrl": {
|
||||
"defaultMessage": "Tünel URL'si"
|
||||
},
|
||||
"tunnelSection.url": {
|
||||
"defaultMessage": "URL'si:"
|
||||
},
|
||||
"updateSection.autoDownload": {
|
||||
"defaultMessage": "Güncelleme arka planda otomatik olarak indirilecektir."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4610,96 +4610,6 @@
|
|||
"toolConfirmation.gooseWouldLikeToCallWithName": {
|
||||
"defaultMessage": "Goose 想调用 {toolName},允许吗?"
|
||||
},
|
||||
"tunnelSection.appStoreQrInstructions": {
|
||||
"defaultMessage": "用 iPhone 相机扫描此二维码,从 App Store 安装 goose 手机应用"
|
||||
},
|
||||
"tunnelSection.close": {
|
||||
"defaultMessage": "关闭"
|
||||
},
|
||||
"tunnelSection.connectionDetails": {
|
||||
"defaultMessage": "连接详情"
|
||||
},
|
||||
"tunnelSection.downloadIosApp": {
|
||||
"defaultMessage": "下载 goose iOS 应用"
|
||||
},
|
||||
"tunnelSection.failedToLoadStatus": {
|
||||
"defaultMessage": "加载通道状态失败"
|
||||
},
|
||||
"tunnelSection.failedToStartTunnel": {
|
||||
"defaultMessage": "启动通道失败"
|
||||
},
|
||||
"tunnelSection.failedToStopTunnel": {
|
||||
"defaultMessage": "停止通道失败"
|
||||
},
|
||||
"tunnelSection.getIosApp": {
|
||||
"defaultMessage": "获取 iOS 应用"
|
||||
},
|
||||
"tunnelSection.mobileApp": {
|
||||
"defaultMessage": "移动应用"
|
||||
},
|
||||
"tunnelSection.mobileAppConnection": {
|
||||
"defaultMessage": "移动应用连接"
|
||||
},
|
||||
"tunnelSection.openInAppStore": {
|
||||
"defaultMessage": "在 App Store 中打开"
|
||||
},
|
||||
"tunnelSection.or": {
|
||||
"defaultMessage": "或"
|
||||
},
|
||||
"tunnelSection.previewDescription": {
|
||||
"defaultMessage": "使用安全通道从移动设备远程访问 goose。"
|
||||
},
|
||||
"tunnelSection.previewFeature": {
|
||||
"defaultMessage": "预览功能:"
|
||||
},
|
||||
"tunnelSection.qrCodeInstructions": {
|
||||
"defaultMessage": "用 goose 手机应用扫描此二维码。请不要把此码分享给任何人,它仅用于你的个人访问。"
|
||||
},
|
||||
"tunnelSection.retry": {
|
||||
"defaultMessage": "重试"
|
||||
},
|
||||
"tunnelSection.scanQrCode": {
|
||||
"defaultMessage": "扫描二维码"
|
||||
},
|
||||
"tunnelSection.secretKey": {
|
||||
"defaultMessage": "密钥"
|
||||
},
|
||||
"tunnelSection.showQrCode": {
|
||||
"defaultMessage": "显示二维码"
|
||||
},
|
||||
"tunnelSection.startTunnel": {
|
||||
"defaultMessage": "启动通道"
|
||||
},
|
||||
"tunnelSection.starting": {
|
||||
"defaultMessage": "启动中…"
|
||||
},
|
||||
"tunnelSection.statusDisabled": {
|
||||
"defaultMessage": "通道已禁用"
|
||||
},
|
||||
"tunnelSection.statusError": {
|
||||
"defaultMessage": "通道发生错误"
|
||||
},
|
||||
"tunnelSection.statusIdle": {
|
||||
"defaultMessage": "通道未运行"
|
||||
},
|
||||
"tunnelSection.statusRunning": {
|
||||
"defaultMessage": "通道已启用"
|
||||
},
|
||||
"tunnelSection.statusStarting": {
|
||||
"defaultMessage": "正在启动通道…"
|
||||
},
|
||||
"tunnelSection.stopTunnel": {
|
||||
"defaultMessage": "停止通道"
|
||||
},
|
||||
"tunnelSection.tunnelStatus": {
|
||||
"defaultMessage": "通道状态"
|
||||
},
|
||||
"tunnelSection.tunnelUrl": {
|
||||
"defaultMessage": "通道 URL"
|
||||
},
|
||||
"tunnelSection.url": {
|
||||
"defaultMessage": "URL:"
|
||||
},
|
||||
"updateSection.autoDownload": {
|
||||
"defaultMessage": "更新将在后台自动下载。"
|
||||
},
|
||||
|
|
|
|||
12
ui/pnpm-lock.yaml
generated
12
ui/pnpm-lock.yaml
generated
|
|
@ -117,9 +117,6 @@ importers:
|
|||
lucide-react:
|
||||
specifier: ^0.575.0
|
||||
version: 0.575.0(react@19.2.4)
|
||||
qrcode.react:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0(react@19.2.4)
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
|
|
@ -6026,11 +6023,6 @@ packages:
|
|||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
qrcode.react@4.2.0:
|
||||
resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==}
|
||||
peerDependencies:
|
||||
react: ^19.2.4
|
||||
|
||||
qs@6.15.0:
|
||||
resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
|
@ -13726,10 +13718,6 @@ snapshots:
|
|||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
qrcode.react@4.2.0(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
|
||||
qs@6.15.0:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue