Compare commits
8 Commits
v0.12.3-de
...
ready/ui-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f59e66065 | ||
|
|
51ac7f152d | ||
|
|
df74c06ba2 | ||
|
|
5f144ca24d | ||
|
|
de66b1349a | ||
|
|
3d888fee64 | ||
|
|
1abcc8ee3c | ||
|
|
d0d5c309e6 |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"minServerVersion": "0.12.3",
|
||||
"latestUIVersion": "0.12.3-rtl",
|
||||
"uiPackageURL": "https://github.com/MusiCode1/CodeNomad/releases/download/v0.12.3-rtl/codenomad-ui-rtl.zip",
|
||||
"sha256": "a2ce1aaa04345a2f9ca9d3c3149567867f3a5e477cf6eb269381e6dc1bec7ca2"
|
||||
}
|
||||
67
packages/tauri-app/Cargo.lock
generated
67
packages/tauri-app/Cargo.lock
generated
@@ -473,7 +473,6 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"thiserror 1.0.69",
|
||||
@@ -1351,16 +1350,6 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||
dependencies = [
|
||||
"rustix 1.1.4",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
@@ -1493,24 +1482,6 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "global-hotkey"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"keyboard-types",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"windows-sys 0.59.0",
|
||||
"x11rb",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.18.0"
|
||||
@@ -4084,21 +4055,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-global-shortcut"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405"
|
||||
dependencies = [
|
||||
"global-hotkey",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.3.3"
|
||||
@@ -5779,29 +5735,6 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"rustix 1.1.4",
|
||||
"x11rb-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb-protocol"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[package]]
|
||||
name = "xkeysym"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
|
||||
@@ -23,7 +23,6 @@ keepawake = "0.6"
|
||||
tauri-plugin-dialog = "2"
|
||||
dirs = "5"
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
url = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2378,72 +2378,6 @@
|
||||
"const": "dialog:deny-save",
|
||||
"markdownDescription": "Denies the save command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:default",
|
||||
"markdownDescription": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n"
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_registered command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:allow-is-registered",
|
||||
"markdownDescription": "Enables the is_registered command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:allow-register",
|
||||
"markdownDescription": "Enables the register command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_all command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:allow-register-all",
|
||||
"markdownDescription": "Enables the register_all command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the unregister command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:allow-unregister",
|
||||
"markdownDescription": "Enables the unregister command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the unregister_all command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:allow-unregister-all",
|
||||
"markdownDescription": "Enables the unregister_all command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_registered command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:deny-is-registered",
|
||||
"markdownDescription": "Denies the is_registered command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:deny-register",
|
||||
"markdownDescription": "Denies the register command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_all command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:deny-register-all",
|
||||
"markdownDescription": "Denies the register_all command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the unregister command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:deny-unregister",
|
||||
"markdownDescription": "Denies the unregister command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the unregister_all command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "global-shortcut:deny-unregister-all",
|
||||
"markdownDescription": "Denies the unregister_all command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||
"type": "string",
|
||||
|
||||
@@ -8,14 +8,10 @@ use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::menu::{MenuBuilder, MenuItem, SubmenuBuilder};
|
||||
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
|
||||
use tauri::webview::Webview;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WindowEvent, Wry};
|
||||
use tauri_plugin_global_shortcut::{
|
||||
Code as ShortcutCode, GlobalShortcutExt, Shortcut, ShortcutState,
|
||||
};
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime, Wry};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use url::Url;
|
||||
|
||||
@@ -29,10 +25,6 @@ use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
|
||||
|
||||
static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||
const DEFAULT_ZOOM_LEVEL: f64 = 1.0;
|
||||
const ZOOM_STEP: f64 = 0.2;
|
||||
const MIN_ZOOM_LEVEL: f64 = 0.2;
|
||||
const MAX_ZOOM_LEVEL: f64 = 5.0;
|
||||
|
||||
#[cfg(windows)]
|
||||
const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client";
|
||||
@@ -40,7 +32,6 @@ const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client";
|
||||
pub struct AppState {
|
||||
pub manager: CliProcessManager,
|
||||
pub wake_lock: Mutex<Option<KeepAwake>>,
|
||||
pub zoom_level: Mutex<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@@ -166,83 +157,6 @@ fn emit_folder_drop_event(
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_zoom_level(value: f64) -> f64 {
|
||||
value.clamp(MIN_ZOOM_LEVEL, MAX_ZOOM_LEVEL)
|
||||
}
|
||||
|
||||
fn set_main_window_zoom(app_handle: &AppHandle, next_zoom: f64) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let normalized = clamp_zoom_level(next_zoom);
|
||||
if window.set_zoom(normalized).is_ok() {
|
||||
if let Ok(mut zoom_level) = app_handle.state::<AppState>().zoom_level.lock() {
|
||||
*zoom_level = normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reload_main_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.reload();
|
||||
}
|
||||
}
|
||||
|
||||
fn force_reload_main_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if let Ok(mut url) = window.url() {
|
||||
if should_allow_internal(&url) {
|
||||
let reload_token = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.to_string();
|
||||
|
||||
let existing_pairs: Vec<(String, String)> = url
|
||||
.query_pairs()
|
||||
.into_owned()
|
||||
.filter(|(key, _)| key != "__codenomad_force_reload")
|
||||
.collect();
|
||||
|
||||
{
|
||||
let mut pairs = url.query_pairs_mut();
|
||||
pairs.clear();
|
||||
for (key, value) in existing_pairs {
|
||||
pairs.append_pair(&key, &value);
|
||||
}
|
||||
pairs.append_pair("__codenomad_force_reload", &reload_token);
|
||||
}
|
||||
|
||||
let _ = window.navigate(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = window.reload();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_fullscreen_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let next_fullscreen = !window.is_fullscreen().unwrap_or(false);
|
||||
let _ = window.set_fullscreen(next_fullscreen);
|
||||
if cfg!(not(target_os = "macos")) {
|
||||
if next_fullscreen {
|
||||
let _ = window.hide_menu();
|
||||
} else {
|
||||
let _ = window.show_menu();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fullscreen_shortcut() -> Option<Shortcut> {
|
||||
if cfg!(target_os = "macos") {
|
||||
None
|
||||
} else {
|
||||
Some(Shortcut::new(None, ShortcutCode::F11))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn set_windows_app_user_model_id() {
|
||||
let app_id: Vec<u16> = OsStr::new(WINDOWS_APP_USER_MODEL_ID)
|
||||
@@ -267,48 +181,15 @@ fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(
|
||||
tauri_plugin_global_shortcut::Builder::new()
|
||||
.with_handler(|app, shortcut, event| {
|
||||
if event.state() != ShortcutState::Pressed {
|
||||
return;
|
||||
}
|
||||
|
||||
if fullscreen_shortcut().as_ref() == Some(shortcut) {
|
||||
toggle_fullscreen_window(app);
|
||||
}
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(navigation_guard)
|
||||
.manage(AppState {
|
||||
manager: CliProcessManager::new(),
|
||||
wake_lock: Mutex::new(None),
|
||||
zoom_level: Mutex::new(DEFAULT_ZOOM_LEVEL),
|
||||
})
|
||||
.setup(|app| {
|
||||
set_windows_app_user_model_id();
|
||||
build_menu(&app.handle())?;
|
||||
if let Some(shortcut) = fullscreen_shortcut() {
|
||||
let shortcut_manager = app.handle().global_shortcut();
|
||||
let _ = shortcut_manager.register(shortcut.clone());
|
||||
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let app_handle = app.handle().clone();
|
||||
window.on_window_event(move |event| {
|
||||
if let WindowEvent::Focused(focused) = event {
|
||||
let shortcut_manager = app_handle.global_shortcut();
|
||||
if *focused {
|
||||
let _ = shortcut_manager.register(shortcut.clone());
|
||||
} else {
|
||||
let _ = shortcut_manager.unregister(shortcut.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let dev_mode = is_dev_mode();
|
||||
let app_handle = app.handle().clone();
|
||||
let manager = app.state::<AppState>().manager.clone();
|
||||
@@ -333,42 +214,36 @@ fn main() {
|
||||
let _ = window.emit("menu:newInstance", ());
|
||||
}
|
||||
}
|
||||
"close" => {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.close();
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
app_handle.exit(0);
|
||||
}
|
||||
|
||||
// View menu
|
||||
"reload" => {
|
||||
reload_main_window(app_handle);
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.eval("window.location.reload()");
|
||||
}
|
||||
}
|
||||
"force_reload" => {
|
||||
force_reload_main_window(app_handle);
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.eval("window.location.reload(true)");
|
||||
}
|
||||
}
|
||||
"toggle_devtools" => {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
if window.is_devtools_open() {
|
||||
window.close_devtools();
|
||||
} else {
|
||||
window.open_devtools();
|
||||
}
|
||||
}
|
||||
}
|
||||
"reset_zoom" => {
|
||||
set_main_window_zoom(app_handle, DEFAULT_ZOOM_LEVEL);
|
||||
}
|
||||
"zoom_in" => {
|
||||
if let Ok(zoom_level) = app_handle.state::<AppState>().zoom_level.lock() {
|
||||
set_main_window_zoom(app_handle, *zoom_level + ZOOM_STEP);
|
||||
}
|
||||
}
|
||||
"zoom_out" => {
|
||||
if let Ok(zoom_level) = app_handle.state::<AppState>().zoom_level.lock() {
|
||||
set_main_window_zoom(app_handle, *zoom_level - ZOOM_STEP);
|
||||
window.open_devtools();
|
||||
}
|
||||
}
|
||||
|
||||
"toggle_fullscreen" => {
|
||||
toggle_fullscreen_window(app_handle);
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.set_fullscreen(!window.is_fullscreen().unwrap_or(false));
|
||||
}
|
||||
}
|
||||
|
||||
// Window menu
|
||||
@@ -382,11 +257,6 @@ fn main() {
|
||||
let _ = window.maximize();
|
||||
}
|
||||
}
|
||||
"close_window" => {
|
||||
if let Some(window) = app_handle.get_webview_window("main") {
|
||||
let _ = window.close();
|
||||
}
|
||||
}
|
||||
|
||||
// App menu (macOS)
|
||||
"about" => {
|
||||
@@ -474,7 +344,6 @@ fn main() {
|
||||
|
||||
fn build_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||
let is_mac = cfg!(target_os = "macos");
|
||||
let is_linux = cfg!(target_os = "linux");
|
||||
|
||||
// Create submenus
|
||||
let mut submenus = Vec::new();
|
||||
@@ -502,74 +371,16 @@ fn build_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||
Some("CmdOrCtrl+N"),
|
||||
)?;
|
||||
|
||||
let file_menu = if is_mac {
|
||||
SubmenuBuilder::new(app, "File")
|
||||
.item(&new_instance_item)
|
||||
.separator()
|
||||
.close_window()
|
||||
.build()?
|
||||
} else {
|
||||
SubmenuBuilder::new(app, "File")
|
||||
.item(&new_instance_item)
|
||||
.separator()
|
||||
.text("quit", "Quit")
|
||||
.build()?
|
||||
};
|
||||
let file_menu = SubmenuBuilder::new(app, "File")
|
||||
.item(&new_instance_item)
|
||||
.separator()
|
||||
.text(
|
||||
if is_mac { "close" } else { "quit" },
|
||||
if is_mac { "Close" } else { "Quit" },
|
||||
)
|
||||
.build()?;
|
||||
submenus.push(file_menu);
|
||||
|
||||
let reload_item = MenuItem::with_id(app, "reload", "Reload", true, Some("CmdOrCtrl+R"))?;
|
||||
let force_reload_item = MenuItem::with_id(
|
||||
app,
|
||||
"force_reload",
|
||||
"Force Reload",
|
||||
true,
|
||||
Some("CmdOrCtrl+Shift+R"),
|
||||
)?;
|
||||
let toggle_devtools_item = MenuItem::with_id(
|
||||
app,
|
||||
"toggle_devtools",
|
||||
"Toggle Developer Tools",
|
||||
true,
|
||||
Some("Alt+CmdOrCtrl+I"),
|
||||
)?;
|
||||
let reset_zoom_item =
|
||||
MenuItem::with_id(app, "reset_zoom", "Actual Size", true, Some("CmdOrCtrl+0"))?;
|
||||
let zoom_in_item = MenuItem::with_id(
|
||||
app,
|
||||
"zoom_in",
|
||||
if is_mac { "Zoom In" } else { "Zoom In\tCtrl++" },
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
let zoom_out_item = MenuItem::with_id(
|
||||
app,
|
||||
"zoom_out",
|
||||
if is_mac {
|
||||
"Zoom Out"
|
||||
} else {
|
||||
"Zoom Out\tCtrl+-"
|
||||
},
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
let toggle_fullscreen_item = MenuItem::with_id(
|
||||
app,
|
||||
"toggle_fullscreen",
|
||||
if is_mac {
|
||||
"Toggle Full Screen"
|
||||
} else {
|
||||
"Toggle Full Screen\tF11"
|
||||
},
|
||||
true,
|
||||
if is_mac {
|
||||
Some("Ctrl+Cmd+F")
|
||||
} else {
|
||||
None::<&str>
|
||||
},
|
||||
)?;
|
||||
let close_window_item =
|
||||
MenuItem::with_id(app, "close_window", "Close", true, Some("CmdOrCtrl+W"))?;
|
||||
|
||||
// Edit menu with predefined items for standard functionality
|
||||
let edit_menu = SubmenuBuilder::new(app, "Edit")
|
||||
.undo()
|
||||
@@ -585,39 +396,20 @@ fn build_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||
|
||||
// View menu
|
||||
let view_menu = SubmenuBuilder::new(app, "View")
|
||||
.item(&reload_item)
|
||||
.item(&force_reload_item)
|
||||
.item(&toggle_devtools_item)
|
||||
.text("reload", "Reload")
|
||||
.text("force_reload", "Force Reload")
|
||||
.text("toggle_devtools", "Toggle Developer Tools")
|
||||
.separator()
|
||||
.item(&reset_zoom_item)
|
||||
.item(&zoom_in_item)
|
||||
.item(&zoom_out_item)
|
||||
.separator()
|
||||
.item(&toggle_fullscreen_item)
|
||||
.text("toggle_fullscreen", "Toggle Full Screen")
|
||||
.build()?;
|
||||
submenus.push(view_menu);
|
||||
|
||||
// Window menu
|
||||
let window_menu = if is_linux {
|
||||
SubmenuBuilder::new(app, "Window")
|
||||
.text("minimize", "Minimize")
|
||||
.text("zoom", "Zoom")
|
||||
.separator()
|
||||
.item(&close_window_item)
|
||||
.build()?
|
||||
} else if is_mac {
|
||||
SubmenuBuilder::new(app, "Window")
|
||||
.minimize()
|
||||
.maximize()
|
||||
.build()?
|
||||
} else {
|
||||
SubmenuBuilder::new(app, "Window")
|
||||
.minimize()
|
||||
.maximize()
|
||||
.separator()
|
||||
.close_window()
|
||||
.build()?
|
||||
};
|
||||
let window_menu = SubmenuBuilder::new(app, "Window")
|
||||
.text("minimize", "Minimize")
|
||||
.text("zoom", "Zoom")
|
||||
.build()?;
|
||||
submenus.push(window_menu);
|
||||
|
||||
// Build the main menu with all submenus
|
||||
|
||||
@@ -404,7 +404,6 @@ const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-sm font-medium text-primary whitespace-normal break-words transition-colors"
|
||||
dir="auto"
|
||||
classList={{
|
||||
"text-accent": isFocused(),
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import {
|
||||
Show,
|
||||
Suspense,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
lazy,
|
||||
onCleanup,
|
||||
type Accessor,
|
||||
type Component,
|
||||
@@ -22,6 +20,11 @@ import type { Session } from "../../../../types/session"
|
||||
import type { DrawerViewState } from "../types"
|
||||
import type { DiffContextMode, DiffViewMode, DiffWordWrapMode, RightPanelTab } from "./types"
|
||||
|
||||
import ChangesTab from "./tabs/ChangesTab"
|
||||
import FilesTab from "./tabs/FilesTab"
|
||||
import GitChangesTab from "./tabs/GitChangesTab"
|
||||
import StatusTab from "./tabs/StatusTab"
|
||||
|
||||
import { getDefaultWorktreeSlug, getOrCreateWorktreeClient, getWorktreeSlugForSession } from "../../../../stores/worktrees"
|
||||
import { requestData } from "../../../../lib/opencode-api"
|
||||
import { buildUnifiedDiffFromSdkPatch, tryReverseApplyUnifiedDiff } from "../../../../lib/unified-diff-reverse"
|
||||
@@ -46,15 +49,6 @@ import {
|
||||
readStoredRightPanelTab,
|
||||
} from "../storage"
|
||||
|
||||
const LazyChangesTab = lazy(() => import("./tabs/ChangesTab"))
|
||||
const LazyGitChangesTab = lazy(() => import("./tabs/GitChangesTab"))
|
||||
const LazyFilesTab = lazy(() => import("./tabs/FilesTab"))
|
||||
const LazyStatusTab = lazy(() => import("./tabs/StatusTab"))
|
||||
|
||||
function RightPanelTabFallback() {
|
||||
return <div class="flex-1 min-h-0" />
|
||||
}
|
||||
|
||||
interface RightPanelProps {
|
||||
t: (key: string, vars?: Record<string, any>) => string
|
||||
|
||||
@@ -571,13 +565,6 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
void loadBrowserEntries(browserPath())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (rightPanelTab() === "files") return
|
||||
setBrowserSelectedContent(null)
|
||||
setBrowserSelectedLoading(false)
|
||||
setBrowserSelectedError(null)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (rightPanelTab() !== "git-changes") return
|
||||
if (gitStatusLoading()) return
|
||||
@@ -585,14 +572,6 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
void loadGitStatus()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (rightPanelTab() === "git-changes") return
|
||||
setGitSelectedBefore(null)
|
||||
setGitSelectedAfter(null)
|
||||
setGitSelectedLoading(false)
|
||||
setGitSelectedError(null)
|
||||
})
|
||||
|
||||
const handleSelectChangesFile = (file: string, closeList: boolean) => {
|
||||
setSelectedFile(file)
|
||||
if (closeList) {
|
||||
@@ -759,109 +738,101 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<Show when={rightPanelTab() === "changes"}>
|
||||
<Suspense fallback={<RightPanelTabFallback />}>
|
||||
<LazyChangesTab
|
||||
t={props.t}
|
||||
instanceId={props.instanceId}
|
||||
activeSessionId={props.activeSessionId}
|
||||
activeSessionDiffs={props.activeSessionDiffs}
|
||||
selectedFile={selectedFile}
|
||||
onSelectFile={handleSelectChangesFile}
|
||||
diffViewMode={diffViewMode}
|
||||
diffContextMode={diffContextMode}
|
||||
diffWordWrapMode={diffWordWrapMode}
|
||||
onViewModeChange={setDiffViewMode}
|
||||
onContextModeChange={setDiffContextMode}
|
||||
onWordWrapModeChange={setDiffWordWrapMode}
|
||||
listOpen={changesListOpen}
|
||||
onToggleList={toggleChangesList}
|
||||
splitWidth={changesSplitWidth}
|
||||
onResizeMouseDown={handleSplitResizeMouseDown("changes")}
|
||||
onResizeTouchStart={handleSplitResizeTouchStart("changes")}
|
||||
isPhoneLayout={props.isPhoneLayout}
|
||||
/>
|
||||
</Suspense>
|
||||
<ChangesTab
|
||||
t={props.t}
|
||||
instanceId={props.instanceId}
|
||||
activeSessionId={props.activeSessionId}
|
||||
activeSessionDiffs={props.activeSessionDiffs}
|
||||
selectedFile={selectedFile}
|
||||
onSelectFile={handleSelectChangesFile}
|
||||
diffViewMode={diffViewMode}
|
||||
diffContextMode={diffContextMode}
|
||||
diffWordWrapMode={diffWordWrapMode}
|
||||
onViewModeChange={setDiffViewMode}
|
||||
onContextModeChange={setDiffContextMode}
|
||||
onWordWrapModeChange={setDiffWordWrapMode}
|
||||
listOpen={changesListOpen}
|
||||
onToggleList={toggleChangesList}
|
||||
splitWidth={changesSplitWidth}
|
||||
onResizeMouseDown={handleSplitResizeMouseDown("changes")}
|
||||
onResizeTouchStart={handleSplitResizeTouchStart("changes")}
|
||||
isPhoneLayout={props.isPhoneLayout}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={rightPanelTab() === "git-changes"}>
|
||||
<Suspense fallback={<RightPanelTabFallback />}>
|
||||
<LazyGitChangesTab
|
||||
t={props.t}
|
||||
activeSessionId={props.activeSessionId}
|
||||
entries={gitStatusEntries}
|
||||
statusLoading={gitStatusLoading}
|
||||
statusError={gitStatusError}
|
||||
selectedPath={gitSelectedPath}
|
||||
selectedLoading={gitSelectedLoading}
|
||||
selectedError={gitSelectedError}
|
||||
selectedBefore={gitSelectedBefore}
|
||||
selectedAfter={gitSelectedAfter}
|
||||
mostChangedPath={gitMostChangedPath}
|
||||
scopeKey={gitScopeKey}
|
||||
diffViewMode={diffViewMode}
|
||||
diffContextMode={diffContextMode}
|
||||
diffWordWrapMode={diffWordWrapMode}
|
||||
onViewModeChange={setDiffViewMode}
|
||||
onContextModeChange={setDiffContextMode}
|
||||
onWordWrapModeChange={setDiffWordWrapMode}
|
||||
onOpenFile={(path: string) => void openGitFile(path)}
|
||||
onRefresh={() => void refreshGitStatus()}
|
||||
listOpen={gitChangesListOpen}
|
||||
onToggleList={toggleGitList}
|
||||
splitWidth={gitChangesSplitWidth}
|
||||
onResizeMouseDown={handleSplitResizeMouseDown("git-changes")}
|
||||
onResizeTouchStart={handleSplitResizeTouchStart("git-changes")}
|
||||
isPhoneLayout={props.isPhoneLayout}
|
||||
/>
|
||||
</Suspense>
|
||||
<GitChangesTab
|
||||
t={props.t}
|
||||
activeSessionId={props.activeSessionId}
|
||||
entries={gitStatusEntries}
|
||||
statusLoading={gitStatusLoading}
|
||||
statusError={gitStatusError}
|
||||
selectedPath={gitSelectedPath}
|
||||
selectedLoading={gitSelectedLoading}
|
||||
selectedError={gitSelectedError}
|
||||
selectedBefore={gitSelectedBefore}
|
||||
selectedAfter={gitSelectedAfter}
|
||||
mostChangedPath={gitMostChangedPath}
|
||||
scopeKey={gitScopeKey}
|
||||
diffViewMode={diffViewMode}
|
||||
diffContextMode={diffContextMode}
|
||||
diffWordWrapMode={diffWordWrapMode}
|
||||
onViewModeChange={setDiffViewMode}
|
||||
onContextModeChange={setDiffContextMode}
|
||||
onWordWrapModeChange={setDiffWordWrapMode}
|
||||
onOpenFile={(path) => void openGitFile(path)}
|
||||
onRefresh={() => void refreshGitStatus()}
|
||||
listOpen={gitChangesListOpen}
|
||||
onToggleList={toggleGitList}
|
||||
splitWidth={gitChangesSplitWidth}
|
||||
onResizeMouseDown={handleSplitResizeMouseDown("git-changes")}
|
||||
onResizeTouchStart={handleSplitResizeTouchStart("git-changes")}
|
||||
isPhoneLayout={props.isPhoneLayout}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={rightPanelTab() === "files"}>
|
||||
<Suspense fallback={<RightPanelTabFallback />}>
|
||||
<LazyFilesTab
|
||||
t={props.t}
|
||||
browserPath={browserPath}
|
||||
browserEntries={browserEntries}
|
||||
browserLoading={browserLoading}
|
||||
browserError={browserError}
|
||||
browserSelectedPath={browserSelectedPath}
|
||||
browserSelectedContent={browserSelectedContent}
|
||||
browserSelectedLoading={browserSelectedLoading}
|
||||
browserSelectedError={browserSelectedError}
|
||||
parentPath={browserParentPath}
|
||||
scopeKey={browserScopeKey}
|
||||
onLoadEntries={(path: string) => void loadBrowserEntries(path)}
|
||||
onOpenFile={(path: string) => void openBrowserFile(path)}
|
||||
onRefresh={() => void refreshFilesTab()}
|
||||
listOpen={filesListOpen}
|
||||
onToggleList={toggleFilesList}
|
||||
splitWidth={filesSplitWidth}
|
||||
onResizeMouseDown={handleSplitResizeMouseDown("files")}
|
||||
onResizeTouchStart={handleSplitResizeTouchStart("files")}
|
||||
isPhoneLayout={props.isPhoneLayout}
|
||||
/>
|
||||
</Suspense>
|
||||
<FilesTab
|
||||
t={props.t}
|
||||
browserPath={browserPath}
|
||||
browserEntries={browserEntries}
|
||||
browserLoading={browserLoading}
|
||||
browserError={browserError}
|
||||
browserSelectedPath={browserSelectedPath}
|
||||
browserSelectedContent={browserSelectedContent}
|
||||
browserSelectedLoading={browserSelectedLoading}
|
||||
browserSelectedError={browserSelectedError}
|
||||
parentPath={browserParentPath}
|
||||
scopeKey={browserScopeKey}
|
||||
onLoadEntries={(path) => void loadBrowserEntries(path)}
|
||||
onOpenFile={(path) => void openBrowserFile(path)}
|
||||
onRefresh={() => void refreshFilesTab()}
|
||||
listOpen={filesListOpen}
|
||||
onToggleList={toggleFilesList}
|
||||
splitWidth={filesSplitWidth}
|
||||
onResizeMouseDown={handleSplitResizeMouseDown("files")}
|
||||
onResizeTouchStart={handleSplitResizeTouchStart("files")}
|
||||
isPhoneLayout={props.isPhoneLayout}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={rightPanelTab() === "status"}>
|
||||
<Suspense fallback={<RightPanelTabFallback />}>
|
||||
<LazyStatusTab
|
||||
t={props.t}
|
||||
instanceId={props.instanceId}
|
||||
instance={props.instance}
|
||||
activeSessionId={props.activeSessionId}
|
||||
activeSession={props.activeSession}
|
||||
activeSessionDiffs={props.activeSessionDiffs}
|
||||
latestTodoState={props.latestTodoState}
|
||||
backgroundProcessList={props.backgroundProcessList}
|
||||
onOpenBackgroundOutput={props.onOpenBackgroundOutput}
|
||||
onStopBackgroundProcess={props.onStopBackgroundProcess}
|
||||
onTerminateBackgroundProcess={props.onTerminateBackgroundProcess}
|
||||
expandedItems={rightPanelExpandedItems}
|
||||
onExpandedItemsChange={handleAccordionChange}
|
||||
onOpenChangesTab={openChangesTabFromStatus}
|
||||
/>
|
||||
</Suspense>
|
||||
<StatusTab
|
||||
t={props.t}
|
||||
instanceId={props.instanceId}
|
||||
instance={props.instance}
|
||||
activeSessionId={props.activeSessionId}
|
||||
activeSession={props.activeSession}
|
||||
activeSessionDiffs={props.activeSessionDiffs}
|
||||
latestTodoState={props.latestTodoState}
|
||||
backgroundProcessList={props.backgroundProcessList}
|
||||
onOpenBackgroundOutput={props.onOpenBackgroundOutput}
|
||||
onStopBackgroundProcess={props.onStopBackgroundProcess}
|
||||
onTerminateBackgroundProcess={props.onTerminateBackgroundProcess}
|
||||
expandedItems={rightPanelExpandedItems}
|
||||
onExpandedItemsChange={handleAccordionChange}
|
||||
onOpenChangesTab={openChangesTabFromStatus}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -244,7 +244,6 @@ export function Markdown(props: MarkdownProps) {
|
||||
<div
|
||||
ref={containerRef}
|
||||
class="markdown-body"
|
||||
dir="auto"
|
||||
data-view="markdown"
|
||||
data-part-id={resolved().partId}
|
||||
data-markdown-theme={resolved().themeKey}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { For, Match, Show, Suspense, Switch, createEffect, createMemo, createSignal, lazy, onCleanup, untrack } from "solid-js"
|
||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup, untrack } from "solid-js"
|
||||
import { ChevronsDownUp, ChevronsUpDown, ExternalLink, FoldVertical, ListStart, Trash } from "lucide-solid"
|
||||
import MessageItem from "./message-item"
|
||||
import ToolCall from "./tool-call"
|
||||
import type { InstanceMessageStore } from "../stores/message-v2/instance-store"
|
||||
import type { ClientPart, MessageInfo } from "../types/message"
|
||||
import { partHasRenderableText } from "../types/message"
|
||||
@@ -28,12 +29,6 @@ const USER_BORDER_COLOR = "var(--message-user-border)"
|
||||
const ASSISTANT_BORDER_COLOR = "var(--message-assistant-border)"
|
||||
const TOOL_BORDER_COLOR = "var(--message-tool-border)"
|
||||
|
||||
const LazyToolCall = lazy(() => import("./tool-call"))
|
||||
|
||||
function ToolCallFallback() {
|
||||
return <div class="tool-call tool-call-loading" />
|
||||
}
|
||||
|
||||
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
||||
|
||||
|
||||
@@ -505,18 +500,16 @@ function ToolCallItem(props: ToolCallItemProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<ToolCallFallback />}>
|
||||
<LazyToolCall
|
||||
toolCall={resolvedToolPart()}
|
||||
toolCallId={props.partId}
|
||||
messageId={props.messageId}
|
||||
messageVersion={messageVersion()}
|
||||
partVersion={partVersion()}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
onContentRendered={props.onContentRendered}
|
||||
/>
|
||||
</Suspense>
|
||||
<ToolCall
|
||||
toolCall={resolvedToolPart()}
|
||||
toolCallId={props.partId}
|
||||
messageId={props.messageId}
|
||||
messageVersion={messageVersion()}
|
||||
partVersion={partVersion()}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
onContentRendered={props.onContentRendered}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
@@ -909,7 +902,6 @@ export default function MessageBlock(props: MessageBlockProps) {
|
||||
onDeleteMessagesUpTo={props.onDeleteMessagesUpTo}
|
||||
selectedMessageIds={props.selectedMessageIds}
|
||||
onToggleSelectedMessage={props.onToggleSelectedMessage}
|
||||
onContentRendered={props.onContentRendered}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -1288,7 +1280,6 @@ interface ReasoningCardProps {
|
||||
onDeleteMessagesUpTo?: (messageId: string) => void | Promise<void>
|
||||
selectedMessageIds?: () => Set<string>
|
||||
onToggleSelectedMessage?: (messageId: string, selected: boolean) => void
|
||||
onContentRendered?: () => void
|
||||
}
|
||||
|
||||
function ReasoningCard(props: ReasoningCardProps) {
|
||||
@@ -1297,25 +1288,6 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
const [deletingMessage, setDeletingMessage] = createSignal(false)
|
||||
const [deletingUpTo, setDeletingUpTo] = createSignal(false)
|
||||
const isSelectedForDeletion = () => Boolean(props.selectedMessageIds?.().has(props.messageId))
|
||||
let pendingRenderNotificationFrame: number | null = null
|
||||
|
||||
const notifyContentRendered = () => {
|
||||
if (!props.onContentRendered || typeof requestAnimationFrame !== "function") return
|
||||
if (pendingRenderNotificationFrame !== null) {
|
||||
cancelAnimationFrame(pendingRenderNotificationFrame)
|
||||
}
|
||||
pendingRenderNotificationFrame = requestAnimationFrame(() => {
|
||||
pendingRenderNotificationFrame = null
|
||||
props.onContentRendered?.()
|
||||
})
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (pendingRenderNotificationFrame !== null) {
|
||||
cancelAnimationFrame(pendingRenderNotificationFrame)
|
||||
pendingRenderNotificationFrame = null
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
setExpanded(Boolean(props.defaultExpanded))
|
||||
@@ -1384,12 +1356,6 @@ function ReasoningCard(props: ReasoningCardProps) {
|
||||
const viewHideLabel = () =>
|
||||
expanded() ? t("messageBlock.reasoning.indicator.hide") : t("messageBlock.reasoning.indicator.view")
|
||||
|
||||
createEffect(() => {
|
||||
if (!expanded()) return
|
||||
reasoningText()
|
||||
notifyContentRendered()
|
||||
})
|
||||
|
||||
const canDeleteMessage = () => Boolean(props.showDeleteMessage) && !deletingMessage()
|
||||
|
||||
const handleDeleteMessage = async (event: MouseEvent) => {
|
||||
|
||||
@@ -542,7 +542,7 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
|
||||
</header>
|
||||
|
||||
<div class="pt-1 whitespace-pre-wrap break-words leading-[1.1]" dir="auto">
|
||||
<div class="pt-1 whitespace-pre-wrap break-words leading-[1.1]">
|
||||
|
||||
|
||||
<Show when={props.isQueued && isUser()}>
|
||||
@@ -550,7 +550,7 @@ export default function MessageItem(props: MessageItemProps) {
|
||||
</Show>
|
||||
|
||||
<Show when={errorMessage()}>
|
||||
<div class="message-error-block" dir="auto">⚠️ {errorMessage()}</div>
|
||||
<div class="message-error-block">⚠️ {errorMessage()}</div>
|
||||
</Show>
|
||||
|
||||
<Show when={isGenerating()}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Match, Show, Suspense, Switch, lazy } from "solid-js"
|
||||
import { Show, Match, Switch } from "solid-js"
|
||||
import ToolCall from "./tool-call"
|
||||
import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state"
|
||||
import { Markdown } from "./markdown"
|
||||
import { useTheme } from "../lib/theme"
|
||||
@@ -6,8 +7,6 @@ import { partHasRenderableText, SDKPart, TextPart, ClientPart } from "../types/m
|
||||
|
||||
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
|
||||
|
||||
const LazyToolCall = lazy(() => import("./tool-call"))
|
||||
|
||||
interface MessagePartProps {
|
||||
part: ClientPart
|
||||
messageType?: "user" | "assistant"
|
||||
@@ -134,12 +133,11 @@ export default function MessagePart(props: MessagePartProps) {
|
||||
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
|
||||
<div
|
||||
class={canRenderMarkdown() ? markdownContainerClass() : textContainerClass()}
|
||||
dir="auto"
|
||||
data-role={textContainerRole()}
|
||||
data-part-type="text"
|
||||
data-part-id={typeof (props.part as any)?.id === "string" ? (props.part as any).id : undefined}
|
||||
>
|
||||
<Show when={canRenderMarkdown()} fallback={<span class="text-primary" dir="auto">{plainTextContent()}</span>}>
|
||||
<Show when={canRenderMarkdown()} fallback={<span class="text-primary">{plainTextContent()}</span>}>
|
||||
<Markdown
|
||||
part={createTextPartForMarkdown()}
|
||||
instanceId={props.instanceId}
|
||||
@@ -154,14 +152,12 @@ export default function MessagePart(props: MessagePartProps) {
|
||||
</Match>
|
||||
|
||||
<Match when={partType() === "tool"}>
|
||||
<Suspense fallback={<div class="tool-call tool-call-loading" />}>
|
||||
<LazyToolCall
|
||||
toolCall={props.part as ToolCallPart}
|
||||
toolCallId={props.part?.id}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
/>
|
||||
</Suspense>
|
||||
<ToolCall
|
||||
toolCall={props.part as ToolCallPart}
|
||||
toolCallId={props.part?.id}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
/>
|
||||
</Match>
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { DeleteHoverState } from "../types/delete-hover"
|
||||
import { buildRecordDisplayData } from "../stores/message-v2/record-display-cache"
|
||||
import { getPartCharCount } from "../lib/token-utils"
|
||||
|
||||
const SCROLL_SENTINEL_MARGIN_PX = 8
|
||||
const SCROLL_SENTINEL_MARGIN_PX = 48
|
||||
const MESSAGE_SCROLL_CACHE_SCOPE = "message-stream"
|
||||
const QUOTE_SELECTION_MAX_LENGTH = 2000
|
||||
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, Show, Suspense, createMemo, createSignal, createEffect, lazy, onCleanup, type Component } from "solid-js"
|
||||
import { For, Show, createMemo, createSignal, createEffect, onCleanup, type Component } from "solid-js"
|
||||
import type { PermissionRequestLike } from "../types/permission"
|
||||
import { getPermissionCallId, getPermissionDisplayTitle, getPermissionKind, getPermissionMessageId, getPermissionSessionId } from "../types/permission"
|
||||
import { getQuestionCallId, getQuestionMessageId, getQuestionSessionId, type QuestionRequest } from "../types/question"
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
} from "../stores/instances"
|
||||
import { ensureSessionParentExpanded, loadMessages, sessions as sessionStateSessions, setActiveSessionFromList } from "../stores/sessions"
|
||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||
|
||||
const LazyToolCall = lazy(() => import("./tool-call"))
|
||||
import ToolCall from "./tool-call"
|
||||
|
||||
interface PermissionApprovalModalProps {
|
||||
instanceId: string
|
||||
@@ -409,17 +408,15 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
|
||||
}
|
||||
>
|
||||
{(data) => (
|
||||
<Suspense fallback={<div class="tool-call tool-call-loading" />}>
|
||||
<LazyToolCall
|
||||
toolCall={data().toolPart}
|
||||
toolCallId={data().toolPart.id}
|
||||
messageId={data().messageId}
|
||||
messageVersion={data().messageVersion}
|
||||
partVersion={data().partVersion}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={data().sessionId}
|
||||
/>
|
||||
</Suspense>
|
||||
<ToolCall
|
||||
toolCall={data().toolPart}
|
||||
toolCallId={data().toolPart.id}
|
||||
messageId={data().messageId}
|
||||
messageVersion={data().messageVersion}
|
||||
partVersion={data().partVersion}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={data().sessionId}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Suspense, createEffect, createSignal, lazy, on, onCleanup, onMount, Show } from "solid-js"
|
||||
import { createSignal, Show, onMount, onCleanup, createEffect, on } from "solid-js"
|
||||
import { ArrowBigUp, ArrowBigDown } from "lucide-solid"
|
||||
import UnifiedPicker from "./unified-picker"
|
||||
import ExpandButton from "./expand-button"
|
||||
import { clearAttachments, removeAttachment } from "../stores/attachments"
|
||||
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
|
||||
@@ -19,7 +20,6 @@ import { usePromptAttachments } from "./prompt-input/usePromptAttachments"
|
||||
import { usePromptPicker } from "./prompt-input/usePromptPicker"
|
||||
import { usePromptKeyDown } from "./prompt-input/usePromptKeyDown"
|
||||
const log = getLogger("actions")
|
||||
const LazyUnifiedPicker = lazy(() => import("./unified-picker"))
|
||||
|
||||
function getConsumedPastedTextAttachmentIds(text: string, attachments: Attachment[]): string[] {
|
||||
if (!text || attachments.length === 0) return []
|
||||
@@ -467,20 +467,18 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Show when={showPicker() && instance()}>
|
||||
<Suspense fallback={null}>
|
||||
<LazyUnifiedPicker
|
||||
open={showPicker()}
|
||||
mode={pickerMode()}
|
||||
onClose={handlePickerClose}
|
||||
onSelect={handlePickerSelect}
|
||||
agents={instanceAgents()}
|
||||
commands={getCommands(props.instanceId)}
|
||||
instanceClient={instance()!.client}
|
||||
searchQuery={searchQuery()}
|
||||
textareaRef={textareaRef}
|
||||
workspaceId={props.instanceId}
|
||||
/>
|
||||
</Suspense>
|
||||
<UnifiedPicker
|
||||
open={showPicker()}
|
||||
mode={pickerMode()}
|
||||
onClose={handlePickerClose}
|
||||
onSelect={handlePickerSelect}
|
||||
agents={instanceAgents()}
|
||||
commands={getCommands(props.instanceId)}
|
||||
instanceClient={instance()!.client}
|
||||
searchQuery={searchQuery()}
|
||||
textareaRef={textareaRef}
|
||||
workspaceId={props.instanceId}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-1 flex-col">
|
||||
@@ -490,7 +488,6 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
class={`prompt-input ${mode() === "shell" ? "shell-mode" : ""} ${expandState() === "expanded" ? "is-expanded" : ""}`}
|
||||
dir="auto"
|
||||
placeholder={getPlaceholder()}
|
||||
value={prompt()}
|
||||
onInput={handleInput}
|
||||
|
||||
@@ -444,7 +444,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
</Show>
|
||||
|
||||
{rowProps.isChild ? <Bot class="w-4 h-4 flex-shrink-0" /> : <User class="w-4 h-4 flex-shrink-0" />}
|
||||
<span class="session-item-title session-item-title--clamp" dir="auto">{title()}</span>
|
||||
<span class="session-item-title session-item-title--clamp">{title()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-item-row session-item-meta">
|
||||
|
||||
@@ -76,7 +76,6 @@ const SessionRenameDialog: Component<SessionRenameDialogProps> = (props) => {
|
||||
inputRef = element
|
||||
}}
|
||||
type="text"
|
||||
dir="auto"
|
||||
value={title()}
|
||||
onInput={(event) => setTitle(event.currentTarget.value)}
|
||||
placeholder={t("sessionRenameDialog.input.placeholder")}
|
||||
|
||||
@@ -514,7 +514,6 @@ function ToolCallDetails(props: {
|
||||
})
|
||||
|
||||
const { renderDiffContent } = createDiffContentRenderer({
|
||||
toolState: props.toolState,
|
||||
preferences: props.preferences,
|
||||
setDiffViewMode: props.setDiffViewMode,
|
||||
isDark: props.isDark,
|
||||
|
||||
@@ -20,14 +20,6 @@ export function createAnsiContentRenderer(params: {
|
||||
const runningAnsiRenderer = createAnsiStreamRenderer()
|
||||
let runningAnsiSource = ""
|
||||
|
||||
const registerTracked = (element: HTMLDivElement | null) => {
|
||||
params.scrollHelpers.registerContainer(element)
|
||||
}
|
||||
|
||||
const registerUntracked = (element: HTMLDivElement | null) => {
|
||||
params.scrollHelpers.registerContainer(element, { disableTracking: true })
|
||||
}
|
||||
|
||||
const getMode = () => {
|
||||
const version = params.partVersion?.()
|
||||
return typeof version === "number" ? String(version) : undefined
|
||||
@@ -44,8 +36,6 @@ export function createAnsiContentRenderer(params: {
|
||||
const cached = cacheHandle.get<AnsiRenderCache>()
|
||||
const mode = getMode()
|
||||
const isRunningVariant = options.variant === "running"
|
||||
const disableScrollTracking = !isRunningVariant
|
||||
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
||||
|
||||
let nextCache: AnsiRenderCache
|
||||
|
||||
@@ -97,9 +87,9 @@ export function createAnsiContentRenderer(params: {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={messageClass} ref={registerRef} onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}>
|
||||
<pre class="tool-call-content tool-call-ansi" dir="auto" innerHTML={nextCache.html} />
|
||||
{params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
|
||||
<div class={messageClass} ref={params.scrollHelpers.registerContainer} onScroll={params.scrollHelpers.handleScroll}>
|
||||
<pre class="tool-call-content tool-call-ansi" innerHTML={nextCache.html} />
|
||||
{params.scrollHelpers.renderSentinel()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function renderDiagnosticsSection(
|
||||
{entry.displayPath}
|
||||
<span class="tool-call-diagnostic-coords">:L{entry.line || "-"}:C{entry.column || "-"}</span>
|
||||
</span>
|
||||
<span class="tool-call-diagnostic-message" dir="auto">{entry.message}</span>
|
||||
<span class="tool-call-diagnostic-message">{entry.message}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Suspense, lazy, onMount, type Accessor, type JSXElement } from "solid-js"
|
||||
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||
import type { RenderCache } from "../../types/message"
|
||||
import type { DiffViewMode } from "../../stores/preferences"
|
||||
import type { DiffPayload, DiffRenderOptions, ToolScrollHelpers } from "./types"
|
||||
@@ -32,7 +31,6 @@ type DiffPrefs = {
|
||||
}
|
||||
|
||||
export function createDiffContentRenderer(params: {
|
||||
toolState: Accessor<ToolState | undefined>
|
||||
preferences: Accessor<DiffPrefs>
|
||||
setDiffViewMode: (mode: DiffViewMode) => void
|
||||
isDark: Accessor<boolean>
|
||||
@@ -60,10 +58,7 @@ export function createDiffContentRenderer(params: {
|
||||
const cacheHandle = selectedVariant === "permission-diff" ? params.permissionDiffCache : params.diffCache
|
||||
const diffMode = () => (params.preferences().diffViewMode || "split") as DiffViewMode
|
||||
const themeKey = params.isDark() ? "dark" : "light"
|
||||
const state = params.toolState()
|
||||
const disableScrollTracking = Boolean(
|
||||
options?.disableScrollTracking || (state?.status !== "running" && state?.status !== "pending"),
|
||||
)
|
||||
const disableScrollTracking = Boolean(options?.disableScrollTracking)
|
||||
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
||||
|
||||
const baseEntryParams = cacheHandle.params() as any
|
||||
|
||||
@@ -31,9 +31,10 @@ export function createMarkdownContentRenderer(params: {
|
||||
const size = options.size || "default"
|
||||
const disableHighlight = options.disableHighlight || false
|
||||
const messageClass = `message-text tool-call-markdown${size === "large" ? " tool-call-markdown-large" : ""}`
|
||||
const state = params.toolState()
|
||||
const disableScrollTracking = options.disableScrollTracking || (state?.status !== "running" && state?.status !== "pending")
|
||||
const disableScrollTracking = options.disableScrollTracking || false
|
||||
const registerRef = disableScrollTracking ? registerUntracked : registerTracked
|
||||
|
||||
const state = params.toolState()
|
||||
const shouldDeferMarkdown = Boolean(state && (state.status === "running" || state.status === "pending") && disableHighlight)
|
||||
if (shouldDeferMarkdown) {
|
||||
return (
|
||||
@@ -42,7 +43,7 @@ export function createMarkdownContentRenderer(params: {
|
||||
ref={registerRef}
|
||||
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
||||
>
|
||||
<pre class="whitespace-pre-wrap break-words text-sm font-mono" dir="auto">{options.content}</pre>
|
||||
<pre class="whitespace-pre-wrap break-words text-sm font-mono">{options.content}</pre>
|
||||
{params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
|
||||
</div>
|
||||
)
|
||||
|
||||
200
packages/ui/src/lib/git-diff-lowlight.ts
Normal file
200
packages/ui/src/lib/git-diff-lowlight.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { createLowlight, common } from "lowlight"
|
||||
|
||||
type AstNode = {
|
||||
type: string
|
||||
value?: string
|
||||
children?: AstNode[]
|
||||
startIndex?: number
|
||||
endIndex?: number
|
||||
lineNumber?: number
|
||||
}
|
||||
|
||||
type SyntaxNodeEntry = {
|
||||
node: AstNode
|
||||
wrapper?: AstNode
|
||||
}
|
||||
|
||||
type SyntaxFileLine = {
|
||||
value: string
|
||||
lineNumber: number
|
||||
valueLength: number
|
||||
nodeList: SyntaxNodeEntry[]
|
||||
}
|
||||
|
||||
type LowlightApi = ReturnType<typeof createLowlight>
|
||||
|
||||
export function processAST(ast: { children: AstNode[] }) {
|
||||
let lineNumber = 1
|
||||
const syntaxObj: Record<number, SyntaxFileLine> = {}
|
||||
|
||||
const loopAST = (nodes: AstNode[], wrapper?: AstNode) => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.type === "text") {
|
||||
const textValue = node.value ?? ""
|
||||
if (!textValue.includes("\n")) {
|
||||
const valueLength = textValue.length
|
||||
if (!syntaxObj[lineNumber]) {
|
||||
node.startIndex = 0
|
||||
node.endIndex = valueLength - 1
|
||||
syntaxObj[lineNumber] = {
|
||||
value: textValue,
|
||||
lineNumber,
|
||||
valueLength,
|
||||
nodeList: [{ node, wrapper }],
|
||||
}
|
||||
} else {
|
||||
node.startIndex = syntaxObj[lineNumber].valueLength
|
||||
node.endIndex = node.startIndex + valueLength - 1
|
||||
syntaxObj[lineNumber].value += textValue
|
||||
syntaxObj[lineNumber].valueLength += valueLength
|
||||
syntaxObj[lineNumber].nodeList.push({ node, wrapper })
|
||||
}
|
||||
node.lineNumber = lineNumber
|
||||
return
|
||||
}
|
||||
|
||||
const lines = textValue.split("\n")
|
||||
node.children = node.children || []
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
const value = index === lines.length - 1 ? lines[index] : `${lines[index]}\n`
|
||||
const currentLineNumber = index === 0 ? lineNumber : ++lineNumber
|
||||
const valueLength = value.length
|
||||
const childNode: AstNode = {
|
||||
type: "text",
|
||||
value,
|
||||
startIndex: Infinity,
|
||||
endIndex: Infinity,
|
||||
lineNumber: currentLineNumber,
|
||||
}
|
||||
|
||||
if (!syntaxObj[currentLineNumber]) {
|
||||
childNode.startIndex = 0
|
||||
childNode.endIndex = valueLength - 1
|
||||
syntaxObj[currentLineNumber] = {
|
||||
value,
|
||||
lineNumber: currentLineNumber,
|
||||
valueLength,
|
||||
nodeList: [{ node: childNode, wrapper }],
|
||||
}
|
||||
} else {
|
||||
childNode.startIndex = syntaxObj[currentLineNumber].valueLength
|
||||
childNode.endIndex = childNode.startIndex + valueLength - 1
|
||||
syntaxObj[currentLineNumber].value += value
|
||||
syntaxObj[currentLineNumber].valueLength += valueLength
|
||||
syntaxObj[currentLineNumber].nodeList.push({ node: childNode, wrapper })
|
||||
}
|
||||
|
||||
node.children.push(childNode)
|
||||
}
|
||||
|
||||
node.lineNumber = lineNumber
|
||||
return
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
loopAST(node.children, node)
|
||||
node.lineNumber = lineNumber
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loopAST(ast.children)
|
||||
return { syntaxFileObject: syntaxObj, syntaxFileLineNumber: lineNumber }
|
||||
}
|
||||
|
||||
export function _getAST() {
|
||||
return {}
|
||||
}
|
||||
|
||||
const lowlight = createLowlight(common)
|
||||
|
||||
lowlight.register("vue", function hljsDefineVue(hljs: any) {
|
||||
return {
|
||||
subLanguage: "xml",
|
||||
contains: [
|
||||
hljs.COMMENT("<!--", "-->", { relevance: 10 }),
|
||||
{
|
||||
begin: /^(\s*)(<script>)/gm,
|
||||
end: /^(\s*)(<\/script>)/gm,
|
||||
subLanguage: "javascript",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true,
|
||||
},
|
||||
{
|
||||
begin: /^(?:\s*)(?:<script\s+lang=(["'])ts\1>)/gm,
|
||||
end: /^(\s*)(<\/script>)/gm,
|
||||
subLanguage: "typescript",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true,
|
||||
},
|
||||
{
|
||||
begin: /^(\s*)(<style(\s+scoped)?>)/gm,
|
||||
end: /^(\s*)(<\/style>)/gm,
|
||||
subLanguage: "css",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true,
|
||||
},
|
||||
{
|
||||
begin: /^(?:\s*)(?:<style(?:\s+scoped)?\s+lang=(["'])(?:s[ca]ss)\1(?:\s+scoped)?>)/gm,
|
||||
end: /^(\s*)(<\/style>)/gm,
|
||||
subLanguage: "scss",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true,
|
||||
},
|
||||
{
|
||||
begin: /^(?:\s*)(?:<style(?:\s+scoped)?\s+lang=(["'])stylus\1(?:\s+scoped)?>)/gm,
|
||||
end: /^(\s*)(<\/style>)/gm,
|
||||
subLanguage: "stylus",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
let maxLineToIgnoreSyntax = 2000
|
||||
const ignoreSyntaxHighlightList: (string | RegExp)[] = []
|
||||
|
||||
export const highlighter = {
|
||||
name: "lowlight",
|
||||
get maxLineToIgnoreSyntax() {
|
||||
return maxLineToIgnoreSyntax
|
||||
},
|
||||
setMaxLineToIgnoreSyntax(value: number) {
|
||||
maxLineToIgnoreSyntax = value
|
||||
},
|
||||
get ignoreSyntaxHighlightList() {
|
||||
return ignoreSyntaxHighlightList
|
||||
},
|
||||
setIgnoreSyntaxHighlightList(values: (string | RegExp)[]) {
|
||||
ignoreSyntaxHighlightList.length = 0
|
||||
ignoreSyntaxHighlightList.push(...values)
|
||||
},
|
||||
getAST(raw: string, fileName?: string, lang?: string) {
|
||||
const language = typeof lang === "string" ? lang.trim() : ""
|
||||
if (
|
||||
fileName &&
|
||||
ignoreSyntaxHighlightList.some((item) => (item instanceof RegExp ? item.test(fileName) : fileName === item))
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (language && lowlight.registered(language)) {
|
||||
return lowlight.highlight(language, raw)
|
||||
}
|
||||
|
||||
return lowlight.highlightAuto(raw)
|
||||
},
|
||||
processAST(ast: { children: AstNode[] }) {
|
||||
return processAST(ast)
|
||||
},
|
||||
hasRegisteredCurrentLang(lang: string) {
|
||||
return lowlight.registered(lang)
|
||||
},
|
||||
getHighlighterEngine(): LowlightApi {
|
||||
return lowlight
|
||||
},
|
||||
type: "class" as const,
|
||||
}
|
||||
|
||||
export const versions = "local-common"
|
||||
@@ -2,6 +2,11 @@ import { createContext, createEffect, createMemo, createSignal, onCleanup, onMou
|
||||
import type { ParentComponent } from "solid-js"
|
||||
import { useConfig } from "../../stores/preferences"
|
||||
import { enMessages } from "./messages/en"
|
||||
import { esMessages } from "./messages/es"
|
||||
import { frMessages } from "./messages/fr"
|
||||
import { ruMessages } from "./messages/ru"
|
||||
import { jaMessages } from "./messages/ja"
|
||||
import { zhHansMessages } from "./messages/zh-Hans"
|
||||
|
||||
type Messages = Record<string, string>
|
||||
|
||||
@@ -10,18 +15,14 @@ export type TranslateParams = Record<string, unknown>
|
||||
export type Locale = "en" | "es" | "fr" | "ru" | "ja" | "zh-Hans"
|
||||
|
||||
const SUPPORTED_LOCALES: readonly Locale[] = ["en", "es", "fr", "ru", "ja", "zh-Hans"] as const
|
||||
const SUPPORTED_LOCALES_BY_LOWER = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale]))
|
||||
|
||||
const localeMessagesCache = new Map<Locale, Messages>([["en", enMessages]])
|
||||
const localeMessagesPromises = new Map<Locale, Promise<Messages>>()
|
||||
|
||||
const localeLoaders: Record<Locale, () => Promise<Messages>> = {
|
||||
en: async () => enMessages,
|
||||
es: async () => (await import("./messages/es")).esMessages,
|
||||
fr: async () => (await import("./messages/fr")).frMessages,
|
||||
ru: async () => (await import("./messages/ru")).ruMessages,
|
||||
ja: async () => (await import("./messages/ja")).jaMessages,
|
||||
"zh-Hans": async () => (await import("./messages/zh-Hans")).zhHansMessages,
|
||||
const messagesByLocale: Record<Locale, Messages> = {
|
||||
en: enMessages,
|
||||
es: esMessages,
|
||||
fr: frMessages,
|
||||
ru: ruMessages,
|
||||
ja: jaMessages,
|
||||
"zh-Hans": zhHansMessages,
|
||||
}
|
||||
|
||||
function normalizeLocaleTag(value: string): string {
|
||||
@@ -33,7 +34,8 @@ function matchSupportedLocale(value: string | undefined): Locale | null {
|
||||
|
||||
const normalized = normalizeLocaleTag(value)
|
||||
const lower = normalized.toLowerCase()
|
||||
const exact = SUPPORTED_LOCALES_BY_LOWER.get(lower)
|
||||
const supportedLower = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale]))
|
||||
const exact = supportedLower.get(lower)
|
||||
if (exact) return exact
|
||||
|
||||
const parts = lower.split("-")
|
||||
@@ -41,11 +43,11 @@ function matchSupportedLocale(value: string | undefined): Locale | null {
|
||||
if (!base) return null
|
||||
|
||||
if (base === "zh") {
|
||||
const zhHans = SUPPORTED_LOCALES_BY_LOWER.get("zh-hans")
|
||||
const zhHans = supportedLower.get("zh-hans")
|
||||
return zhHans ?? null
|
||||
}
|
||||
|
||||
const baseMatch = SUPPORTED_LOCALES_BY_LOWER.get(base)
|
||||
const baseMatch = supportedLower.get(base)
|
||||
return baseMatch ?? null
|
||||
}
|
||||
|
||||
@@ -82,54 +84,8 @@ function translateFrom(messages: Messages, key: string, params?: TranslateParams
|
||||
}
|
||||
|
||||
const [globalRevision, setGlobalRevision] = createSignal(0)
|
||||
let globalMessages: Messages = enMessages
|
||||
let globalLocale: Locale = "en"
|
||||
|
||||
function getMessagesForLocale(locale: Locale): Messages {
|
||||
return localeMessagesCache.get(locale) ?? enMessages
|
||||
}
|
||||
|
||||
async function loadLocaleMessages(locale: Locale): Promise<Messages> {
|
||||
const cached = localeMessagesCache.get(locale)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const pending = localeMessagesPromises.get(locale)
|
||||
if (pending) {
|
||||
return pending
|
||||
}
|
||||
|
||||
const loader = localeLoaders[locale]
|
||||
const promise = loader()
|
||||
.then((messages) => {
|
||||
localeMessagesCache.set(locale, messages)
|
||||
localeMessagesPromises.delete(locale)
|
||||
return messages
|
||||
})
|
||||
.catch((error) => {
|
||||
localeMessagesPromises.delete(locale)
|
||||
throw error
|
||||
})
|
||||
|
||||
localeMessagesPromises.set(locale, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
export async function preloadLocaleMessages(preferredLocale?: string | null): Promise<Locale> {
|
||||
const resolvedLocale = matchSupportedLocale(preferredLocale ?? undefined) ?? detectNavigatorLocale() ?? "en"
|
||||
try {
|
||||
globalMessages = await loadLocaleMessages(resolvedLocale)
|
||||
globalLocale = resolvedLocale
|
||||
setGlobalRevision((value) => value + 1)
|
||||
return resolvedLocale
|
||||
} catch {
|
||||
globalMessages = enMessages
|
||||
globalLocale = "en"
|
||||
setGlobalRevision((value) => value + 1)
|
||||
return "en"
|
||||
}
|
||||
}
|
||||
const initialGlobalLocale: Locale = detectNavigatorLocale() ?? "en"
|
||||
let globalMessages: Messages = messagesByLocale[initialGlobalLocale]
|
||||
|
||||
export function tGlobal(key: string, params?: TranslateParams): string {
|
||||
globalRevision()
|
||||
@@ -145,10 +101,9 @@ const I18nContext = createContext<I18nContextValue>()
|
||||
|
||||
export const I18nProvider: ParentComponent = (props) => {
|
||||
const { preferences } = useConfig()
|
||||
const [detectedLocale, setDetectedLocale] = createSignal<Locale>(globalLocale)
|
||||
const [resolvedLocale, setResolvedLocale] = createSignal<Locale>(globalLocale)
|
||||
const previousGlobalMessages = globalMessages
|
||||
const previousGlobalLocale = globalLocale
|
||||
const [detectedLocale, setDetectedLocale] = createSignal<Locale>("en")
|
||||
|
||||
const previousMessages = globalMessages
|
||||
|
||||
onMount(() => {
|
||||
const detected = detectNavigatorLocale()
|
||||
@@ -160,44 +115,19 @@ export const I18nProvider: ParentComponent = (props) => {
|
||||
return configured ?? detectedLocale() ?? "en"
|
||||
})
|
||||
|
||||
const messages = createMemo<Messages>(() => getMessagesForLocale(resolvedLocale()))
|
||||
const messages = createMemo<Messages>(() => messagesByLocale[locale()])
|
||||
|
||||
function t(key: string, params?: TranslateParams): string {
|
||||
return translateFrom(messages(), key, params)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const nextLocale = locale()
|
||||
let cancelled = false
|
||||
|
||||
void loadLocaleMessages(nextLocale)
|
||||
.then((loadedMessages) => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
setResolvedLocale(nextLocale)
|
||||
globalLocale = nextLocale
|
||||
globalMessages = loadedMessages
|
||||
setGlobalRevision((value) => value + 1)
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
setResolvedLocale("en")
|
||||
globalMessages = enMessages
|
||||
globalLocale = "en"
|
||||
setGlobalRevision((value) => value + 1)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
cancelled = true
|
||||
})
|
||||
globalMessages = messages()
|
||||
setGlobalRevision((value) => value + 1)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
globalMessages = previousGlobalMessages
|
||||
globalLocale = previousGlobalLocale
|
||||
globalMessages = previousMessages
|
||||
setGlobalRevision((value) => value + 1)
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ThemeProvider } from "./lib/theme"
|
||||
import { ConfigProvider } from "./stores/preferences"
|
||||
import { InstanceConfigProvider } from "./stores/instance-config"
|
||||
import { runtimeEnv } from "./lib/runtime-env"
|
||||
import { I18nProvider, preloadLocaleMessages } from "./lib/i18n"
|
||||
import { I18nProvider } from "./lib/i18n"
|
||||
import { storage } from "./lib/storage"
|
||||
import "./index.css"
|
||||
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
||||
@@ -31,19 +31,15 @@ async function bootstrap() {
|
||||
|
||||
try {
|
||||
const uiConfig = await storage.loadConfigOwner("ui")
|
||||
const theme = (uiConfig as any)?.theme
|
||||
const locale = typeof (uiConfig as any)?.settings?.locale === "string" ? (uiConfig as any).settings.locale : undefined
|
||||
const theme = (uiConfig as any)?.theme ?? "system"
|
||||
|
||||
if (theme === "light" || theme === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", theme)
|
||||
} else {
|
||||
if (theme === "system") {
|
||||
document.documentElement.removeAttribute("data-theme")
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", theme)
|
||||
}
|
||||
|
||||
await preloadLocaleMessages(locale)
|
||||
} catch {
|
||||
// If config fails to load, fall back to CSS defaults.
|
||||
await preloadLocaleMessages()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,21 +24,6 @@
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Auto-detect text direction per block element for RTL language support (e.g. Hebrew, Arabic) */
|
||||
.markdown-body p,
|
||||
.markdown-body li,
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6,
|
||||
.markdown-body blockquote,
|
||||
.markdown-body td,
|
||||
.markdown-body th {
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
@@ -144,19 +129,16 @@
|
||||
}
|
||||
|
||||
.markdown-body blockquote {
|
||||
border-inline-start: 3px solid var(--border-base);
|
||||
border-left: 3px solid var(--border-base);
|
||||
color: var(--text-secondary);
|
||||
background-color: var(--surface-muted);
|
||||
padding: 0.5rem 1rem;
|
||||
border-start-start-radius: 0;
|
||||
border-start-end-radius: 8px;
|
||||
border-end-end-radius: 8px;
|
||||
border-end-start-radius: 0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
padding-inline-start: 1.5rem;
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
@@ -184,7 +166,7 @@
|
||||
.markdown-body td {
|
||||
border: 1px solid var(--border-base);
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: start;
|
||||
text-align: left;
|
||||
color: var(--text-primary);
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -239,7 +221,7 @@
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
|
||||
margin-inline-start: auto;
|
||||
margin-left: auto;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
|
||||
@@ -132,13 +132,6 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message-stream-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.message-step-start {
|
||||
background-color: var(--message-assistant-bg);
|
||||
border-left: 4px solid var(--message-assistant-border);
|
||||
|
||||
Reference in New Issue
Block a user