feat: sidebar update card (web + macOS) with app-first mac update flow and Sparkle beta track (#104171)

* feat: sidebar update card (web + macOS) with app-first mac update flow and Sparkle beta track

Squashed from claude/update-notification-display-c6cfb9 after semantic merge
with #104178 (channel-aware CLI installs). See PR #104171 body for details.

* chore(i18n): resync generated inventories after rebase

* chore(i18n): resync locale metadata after rebase
This commit is contained in:
Peter Steinberger
2026-07-11 00:34:10 -07:00
committed by GitHub
parent 7da12cbbe5
commit ebc848deec
86 changed files with 1318 additions and 274 deletions
+4 -4
View File
@@ -15195,7 +15195,7 @@
},
{
"kind": "conditional-branch",
"line": 107,
"line": 173,
"path": "apps/macos/Sources/OpenClaw/CLIInstallPrompter.swift",
"source": "CLI install failed",
"surface": "apple",
@@ -15203,7 +15203,7 @@
},
{
"kind": "conditional-branch",
"line": 107,
"line": 173,
"path": "apps/macos/Sources/OpenClaw/CLIInstallPrompter.swift",
"source": "CLI install finished",
"surface": "apple",
@@ -18347,7 +18347,7 @@
},
{
"kind": "conditional-branch",
"line": 322,
"line": 327,
"path": "apps/macos/Sources/OpenClaw/MenuBar.swift",
"source": "Close Canvas",
"surface": "apple",
@@ -18355,7 +18355,7 @@
},
{
"kind": "conditional-branch",
"line": 322,
"line": 327,
"path": "apps/macos/Sources/OpenClaw/MenuBar.swift",
"source": "Open Canvas",
"surface": "apple",
@@ -22,15 +22,40 @@ final class CLIInstallPrompter {
guard AppStateStore.shared.connectionMode == .local else { return }
guard let version = Self.appVersion() else { return }
let status = await CLIInstaller.status()
let managedStatus = await CLIInstaller.managedStatus()
guard AppStateStore.shared.onboardingSeen else { return }
guard AppStateStore.shared.connectionMode == .local else { return }
let shouldRepairManaged = Self.shouldAutomaticallyRepair(
status: managedStatus,
launchAgentUsesManagedCLI: Self.launchAgentUsesManagedCLI(
programArguments: GatewayLaunchAgentManager.launchdConfigSnapshot()?.programArguments ?? []),
gatewayUpdateChannel: OpenClawConfigFile.gatewayUpdateChannel(),
launchAgentWriteDisabled: GatewayLaunchAgentManager.isLaunchAgentWriteDisabled())
if await self.completePendingManagedRestartIfNeeded(managedStatus: managedStatus) {
return
}
if shouldRepairManaged {
// Only repair the app-owned install; external package-manager installs
// remain under their owner's control. Repair restores the exact pin
// that produced the incompatible status (channel policies never pin,
// so they never reach this branch). No persisted attempt marker:
// a transient failure must retry on the next launch/mode change, and
// success clears the incompatible status that gates this branch.
if await self.installCLI(
target: .exact(version),
showCompletionAlert: false,
restartManagedGateway: !AppStateStore.shared.isPaused)
{
return
}
}
guard !status.isReady else { return }
let lastPrompt = UserDefaults.standard.string(forKey: cliInstallPromptedVersionKey)
guard lastPrompt != version else { return }
UserDefaults.standard.set(version, forKey: cliInstallPromptedVersionKey)
if let target = self.installTargetForCurrentBuild(confirmStable: true) {
Task { await self.installCLI(target: target) }
Task { _ = await self.installCLI(target: target) }
}
self.logger.debug("cli install prompt handled reason=\(reason, privacy: .public)")
@@ -84,14 +109,52 @@ final class CLIInstallPrompter {
return channels[index]
}
private func installCLI(target: CLIInstaller.InstallTarget) async {
private func installCLI(
target: CLIInstaller.InstallTarget,
showCompletionAlert: Bool = true,
restartManagedGateway: Bool = false) async -> Bool
{
let status = StatusBox()
let previousPID = restartManagedGateway
? await GatewayLaunchAgentManager.runningGatewayPID()
: nil
let installed = await CLIInstaller.install(target: target) { message in
await status.set(message)
if !showCompletionAlert {
self.logger.info("managed CLI repair: \(message, privacy: .public)")
}
}
var activated = false
if installed {
if restartManagedGateway {
let restarted = await self.ensureManagedGatewayRestarted(
previousPID: previousPID,
status: status)
guard restarted else {
// The on-disk CLI is already replaced, so the incompatible
// status that gates auto-repair will read ready next launch.
// Persist the unfinished restart or the old gateway process
// would keep running the previous version indefinitely.
Self.setPendingManagedRestart()
return false
}
}
await status.set("Starting OpenClaw Gateway…")
if !showCompletionAlert {
self.logger.info("managed CLI repair: Starting OpenClaw Gateway…")
}
let activation = await CLIInstaller.activateLocalGateway()
activated = activation != .failed
if restartManagedGateway {
// Only proven gateway health closes the recovery loop; the
// on-disk CLI already reads ready, so a lost marker here means
// no later trigger would ever restart a failed gateway.
if activated {
Self.clearPendingManagedRestart()
} else {
Self.setPendingManagedRestart()
}
}
let message = switch activation {
case .ready:
"OpenClaw Gateway is ready."
@@ -101,13 +164,91 @@ final class CLIInstallPrompter {
"OpenClaw was installed, but the Gateway did not start. Open Settings to retry."
}
await status.set(message)
if !showCompletionAlert {
self.logger.info("managed CLI repair: \(message, privacy: .public)")
}
}
if let message = await status.get() {
if showCompletionAlert, let message = await status.get() {
let alert = NSAlert()
alert.messageText = installed ? "CLI install finished" : "CLI install failed"
alert.informativeText = message
alert.runModal()
}
return installed && activated
}
/// Finishes an update whose install succeeded but whose gateway restart did
/// not verify: by then the on-disk CLI reads ready, so the auto-repair gate
/// can never fire again for that version while the old process keeps running.
private func completePendingManagedRestartIfNeeded(managedStatus: CLIInstaller.Status) async -> Bool {
guard Self.hasPendingManagedRestart() else { return false }
guard case .ready = managedStatus else {
// A new incompatible/missing cycle owns the next repair.
Self.clearPendingManagedRestart()
return false
}
guard Self.launchAgentUsesManagedCLI(
programArguments: GatewayLaunchAgentManager.launchdConfigSnapshot()?.programArguments ?? []),
!GatewayLaunchAgentManager.isLaunchAgentWriteDisabled(),
!AppStateStore.shared.isPaused
else { return false }
if let error = await GatewayLaunchAgentManager.kickstart() {
self.logger.error("pending managed Gateway restart failed: \(error, privacy: .public)")
return false
}
await GatewayConnection.shared.shutdown()
guard await CLIInstaller.activateLocalGateway() != .failed else { return false }
Self.clearPendingManagedRestart()
self.logger.info("pending managed Gateway restart completed")
return true
}
static func hasPendingManagedRestart() -> Bool {
UserDefaults.standard.bool(forKey: cliManagedRestartPendingKey)
}
static func setPendingManagedRestart() {
UserDefaults.standard.set(true, forKey: cliManagedRestartPendingKey)
}
static func clearPendingManagedRestart() {
UserDefaults.standard.removeObject(forKey: cliManagedRestartPendingKey)
}
private func ensureManagedGatewayRestarted(previousPID: Int32?, status: StatusBox) async -> Bool {
guard previousPID != nil else {
await GatewayConnection.shared.shutdown()
return true
}
if await self.waitForManagedGatewayRestart(previousPID: previousPID) {
await GatewayConnection.shared.shutdown()
return true
}
if let error = await GatewayLaunchAgentManager.kickstart() {
let message = "Managed Gateway restart failed: \(error)"
await status.set(message)
self.logger.error("\(message, privacy: .public)")
return false
}
await GatewayConnection.shared.shutdown()
guard await self.waitForManagedGatewayRestart(previousPID: previousPID) else {
let message = "Managed Gateway restart could not be verified."
await status.set(message)
self.logger.error("\(message, privacy: .public)")
return false
}
return true
}
private func waitForManagedGatewayRestart(previousPID: Int32?) async -> Bool {
for _ in 0..<20 {
let currentPID = await GatewayLaunchAgentManager.runningGatewayPID()
if Self.didManagedGatewayRestart(previousPID: previousPID, currentPID: currentPID) {
return true
}
try? await Task.sleep(nanoseconds: 150_000_000)
}
return false
}
private func openSettings(tab: SettingsTab) {
@@ -121,6 +262,97 @@ final class CLIInstallPrompter {
private static func appVersion() -> String? {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
}
/// Shared gate for auto-repair and the dashboard's native update bridge.
/// If these drift apart, the card can route to Sparkle while the
/// post-relaunch gateway repair refuses, stranding an old gateway.
static func managedRepairGatesOpen(
launchAgentUsesManagedCLI: Bool,
gatewayUpdateChannel: String?,
launchAgentWriteDisabled: Bool) -> Bool
{
guard !launchAgentWriteDisabled else { return false }
guard launchAgentUsesManagedCLI else { return false }
// Extended-stable pins an intentionally older gateway; moving it to the
// app's newer stable version without consent keeps the prompt instead.
return gatewayUpdateChannel?.lowercased() != "extended-stable"
}
static func shouldAutomaticallyRepair(
status: CLIInstaller.Status,
launchAgentUsesManagedCLI: Bool,
gatewayUpdateChannel: String? = nil,
launchAgentWriteDisabled: Bool = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled()) -> Bool
{
guard self.managedRepairGatesOpen(
launchAgentUsesManagedCLI: launchAgentUsesManagedCLI,
gatewayUpdateChannel: gatewayUpdateChannel,
launchAgentWriteDisabled: launchAgentWriteDisabled)
else { return false }
guard case let .incompatible(location, found, required) = status else { return false }
// Auto-repair only moves the managed install forward. A gateway newer
// than the app (e.g. beta channel ahead of the app track) was a user
// choice; silently downgrading it keeps the consent prompt instead.
guard Self.isManagedUpgrade(found: found, required: required) else { return false }
return location == CLIInstaller.managedExecutableLocation()
}
static func isManagedUpgrade(found: String, required: String) -> Bool {
guard let foundVersion = Semver.parse(found),
let requiredVersion = Semver.parse(required)
else { return false }
if foundVersion != requiredVersion { return foundVersion < requiredVersion }
// Same numeric triple: a prerelease sorts below its release, so
// beta -> stable is an upgrade and stable -> beta is a downgrade.
switch (Self.prereleaseTail(found), Self.prereleaseTail(required)) {
case (nil, nil), (nil, .some):
return false
case (.some, nil):
return true
case let (.some(foundTail), .some(requiredTail)):
return foundTail.compare(requiredTail, options: .numeric) == .orderedAscending
}
}
private static func prereleaseTail(_ version: String) -> String? {
let trimmed = version.trimmingCharacters(in: .whitespacesAndNewlines)
guard let separator = trimmed.firstIndex(of: "-") else { return nil }
let tail = String(trimmed[trimmed.index(after: separator)...])
return tail.isEmpty ? nil : tail
}
static func launchAgentUsesManagedCLI(programArguments: [String]) -> Bool {
var command = programArguments[...]
if command.count >= 3,
command[command.startIndex] == "/bin/sh",
command[command.index(after: command.startIndex)].hasSuffix("-env-wrapper.sh")
{
command = command.dropFirst(3)
} else if command.count >= 2,
command[command.startIndex].hasSuffix("-env-wrapper.sh")
{
command = command.dropFirst(2)
}
let managedRoot = URL(fileURLWithPath: CLIInstaller.managedExecutableLocation())
.deletingLastPathComponent()
.deletingLastPathComponent()
.standardizedFileURL.path + "/"
guard let executable = command.first else { return false }
let executablePath = URL(fileURLWithPath: executable).standardizedFileURL.path
let managedRuntimeRoot = managedRoot + "tools/node/"
if executablePath.hasPrefix(managedRoot), !executablePath.hasPrefix(managedRuntimeRoot) {
return true
}
guard command.count >= 2 else { return false }
let entrypoint = command[command.index(after: command.startIndex)]
return URL(fileURLWithPath: entrypoint).standardizedFileURL.path.hasPrefix(managedRoot)
}
static func didManagedGatewayRestart(previousPID: Int32?, currentPID: Int32?) -> Bool {
guard let currentPID else { return false }
guard let previousPID else { return true }
return currentPID != previousPID
}
}
private actor StatusBox {
@@ -241,6 +241,13 @@ enum CLIInstaller {
guard let required = Semver.parse(expectedVersion) else {
return .ready(location: location, version: normalized)
}
let requiresExactVersion = Self.isPrerelease(expectedVersion) || Self.isPrerelease(normalized)
if requiresExactVersion, normalized != expectedVersion {
return .incompatible(
location: location,
found: normalized,
required: expectedVersion ?? required.description)
}
guard installed.compatible(with: required) else {
return .incompatible(
location: location,
@@ -250,6 +257,13 @@ enum CLIInstaller {
return .ready(location: location, version: normalized)
}
private static func isPrerelease(_ version: String?) -> Bool {
guard let version = version?.lowercased() else { return false }
return ["alpha", "beta"].contains { lane in
version.contains("-\(lane).") || version.contains(".\(lane).")
}
}
static func probeEnvironment(
location: String,
processEnvironment: [String: String] = ProcessInfo.processInfo.environment,
@@ -44,6 +44,7 @@ let peekabooBridgeEnabledKey = "openclaw.peekabooBridgeEnabled"
let deepLinkKeyKey = "openclaw.deepLinkKey"
let cliInstallPromptedVersionKey = "openclaw.cliInstallPromptedVersion"
let cliInstallPolicyKey = "openclaw.cliInstallPolicy"
let cliManagedRestartPendingKey = "openclaw.cliManagedRestartPending"
let cliValidatedExecutableKey = "openclaw.cliValidatedExecutable"
let cliValidatedVersionKey = "openclaw.cliValidatedVersion"
let macNodeIdentityProfileKey = "openclaw.macNodeIdentityProfile"
@@ -11,10 +11,28 @@ final class DashboardManager {
private var controller: DashboardWindowController?
private var endpointTask: Task<Void, Never>?
private var updater: UpdaterProviding?
private static let failureURL = URL(string: "about:blank")!
private init() {}
func configure(updater: UpdaterProviding) {
self.updater = updater
}
/// The card's native update path only makes sense when the app owns the
/// local gateway and the post-relaunch repair is allowed to run; otherwise
/// (external CLI, write-disabled launchd, extended-stable pin) the card
/// must keep the direct gateway `update.run` flow, so no bridge is exposed.
static func updateBridgeEnabled(mode: AppState.ConnectionMode) -> Bool {
guard mode == .local else { return false }
return CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: CLIInstallPrompter.launchAgentUsesManagedCLI(
programArguments: GatewayLaunchAgentManager.launchdConfigSnapshot()?.programArguments ?? []),
gatewayUpdateChannel: OpenClawConfigFile.gatewayUpdateChannel(),
launchAgentWriteDisabled: GatewayLaunchAgentManager.isLaunchAgentWriteDisabled())
}
/// The remote SSH tunnel can be recreated on a new ephemeral local port while
/// the dashboard stays open; without following endpoint changes the WebView
/// keeps reconnecting to the dead old port forever (#100476).
@@ -34,11 +52,17 @@ final class DashboardManager {
guard let controller, controller.isWindowOpen else { return }
let config: GatewayConnection.Config = (url, token, password)
let authToken = await GatewayConnection.shared.controlUiAutoAuthToken(config: config)
guard let dashboardURL = try? GatewayEndpointStore.dashboardURL(for: config, mode: mode, authToken: authToken),
dashboardURL != controller.currentURL
guard let dashboardURL = try? GatewayEndpointStore.dashboardURL(
for: config,
mode: mode,
authToken: authToken)
else {
return
}
if dashboardURL == controller.currentURL {
controller.setUpdateBridgeEnabled(Self.updateBridgeEnabled(mode: mode))
return
}
let auth = DashboardWindowAuth(
gatewayUrl: Self.websocketURLString(for: dashboardURL),
token: authToken,
@@ -46,7 +70,7 @@ final class DashboardManager {
guard auth.hasCredential, controller.isWindowOpen else { return }
dashboardManagerLogger.info(
"dashboard endpoint changed; reloading url=\(dashboardLogString(for: dashboardURL), privacy: .public)")
controller.update(url: dashboardURL, auth: auth)
controller.update(url: dashboardURL, auth: auth, updateBridgeEnabled: Self.updateBridgeEnabled(mode: mode))
}
@discardableResult
@@ -68,9 +92,13 @@ final class DashboardManager {
return false
}
if let controller {
controller.show(url: url, auth: auth)
controller.show(url: url, auth: auth, updateBridgeEnabled: Self.updateBridgeEnabled(mode: mode))
} else {
let controller = DashboardWindowController(url: url, auth: auth)
let controller = DashboardWindowController(
url: url,
auth: auth,
updater: self.updater,
updateBridgeEnabled: Self.updateBridgeEnabled(mode: mode))
self.controller = controller
controller.show(url: url, auth: auth)
}
@@ -93,13 +121,17 @@ final class DashboardManager {
if let controller {
dashboardManagerLogger.info("dashboard reuse window url=\(dashboardLogString(for: url), privacy: .public)")
controller.show(url: url, auth: auth)
controller.show(url: url, auth: auth, updateBridgeEnabled: Self.updateBridgeEnabled(mode: mode))
self.observeEndpointChanges()
return
}
dashboardManagerLogger.info("dashboard create window url=\(dashboardLogString(for: url), privacy: .public)")
let controller = DashboardWindowController(url: url, auth: auth)
let controller = DashboardWindowController(
url: url,
auth: auth,
updater: self.updater,
updateBridgeEnabled: Self.updateBridgeEnabled(mode: mode))
self.controller = controller
controller.show(url: url, auth: auth)
self.observeEndpointChanges()
@@ -113,7 +145,9 @@ final class DashboardManager {
dashboardManagerLogger.error("dashboard setup failed error=\(message, privacy: .public)")
let controller = self.controller ?? DashboardWindowController(
url: Self.failureURL,
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil),
updater: self.updater,
updateBridgeEnabled: Self.updateBridgeEnabled(mode: AppStateStore.shared.connectionMode))
self.controller = controller
// Keep observing while the failure page is up so a recovered tunnel
// swaps the window back to the live dashboard.
@@ -36,25 +36,46 @@ private final class DashboardWindowDragMessageHandler: NSObject, WKScriptMessage
}
}
@MainActor
private final class DashboardUpdateMessageHandler: NSObject, WKScriptMessageHandler {
weak var owner: DashboardWindowController?
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
self.owner?.receiveUpdateMessage(message)
}
}
@MainActor
final class DashboardWindowController: NSWindowController, WKNavigationDelegate, WKUIDelegate, NSWindowDelegate {
private static let linkMessageHandlerName = "openclawLink"
private static let windowDragMessageHandlerName = "openclawWindowDrag"
private static let updateMessageHandlerName = "openclawUpdate"
private let webView: WKWebView
private let linkBrowser: DashboardLinkBrowserView
private let linkBrowserItem: NSSplitViewItem
private let splitViewController: NSSplitViewController
private let updateMessageHandler: DashboardUpdateMessageHandler
private(set) var currentURL: URL
private var auth: DashboardWindowAuth
private let updater: UpdaterProviding?
private var updateBridgeEnabled: Bool
private var backButton: NSButton?
private var forwardButton: NSButton?
private var canGoBackObservation: NSKeyValueObservation?
private var canGoForwardObservation: NSKeyValueObservation?
init(url: URL, auth: DashboardWindowAuth) {
init(
url: URL,
auth: DashboardWindowAuth,
updater: UpdaterProviding? = nil,
updateBridgeEnabled: Bool = true)
{
let shouldEnableUpdateBridge = updater?.isAvailable == true && updateBridgeEnabled
self.currentURL = url
self.auth = auth
self.updater = updater
self.updateBridgeEnabled = shouldEnableUpdateBridge
let dataStore = WKWebsiteDataStore.default()
let config = WKWebViewConfiguration()
@@ -67,6 +88,13 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
config.userContentController.add(linkMessageHandler, name: Self.linkMessageHandlerName)
let windowDragMessageHandler = DashboardWindowDragMessageHandler()
config.userContentController.add(windowDragMessageHandler, name: Self.windowDragMessageHandlerName)
let updateMessageHandler = DashboardUpdateMessageHandler()
self.updateMessageHandler = updateMessageHandler
if shouldEnableUpdateBridge {
// Handler presence is the Control UI feature probe; unsigned builds
// and remote dashboards must not advertise a local app update.
config.userContentController.add(updateMessageHandler, name: Self.updateMessageHandlerName)
}
Self.installNativeChromeScript(into: config.userContentController)
Self.installNativeAuthScript(into: config.userContentController, url: url, auth: auth)
@@ -117,6 +145,7 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
self.linkBrowserItem.isCollapsed = true
linkMessageHandler.owner = self
windowDragMessageHandler.owner = self
updateMessageHandler.owner = self
self.webView.navigationDelegate = self
self.webView.uiDelegate = self
self.linkBrowser.webViewNavigationDelegate = self
@@ -127,6 +156,17 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
self.installNavigationControls()
}
func setUpdateBridgeEnabled(_ enabled: Bool) {
let nextEnabled = self.updater?.isAvailable == true && enabled
guard nextEnabled != self.updateBridgeEnabled else { return }
self.updateBridgeEnabled = nextEnabled
let controller = self.webView.configuration.userContentController
controller.removeScriptMessageHandler(forName: Self.updateMessageHandlerName)
if nextEnabled {
controller.add(self.updateMessageHandler, name: Self.updateMessageHandlerName)
}
}
// MARK: - WKUIDelegate
/// Bridges `<input type="file">` clicks in the embedded Control UI to a native
@@ -192,8 +232,8 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
fatalError("init(coder:) is not supported")
}
func show(url: URL, auth: DashboardWindowAuth) {
self.update(url: url, auth: auth)
func show(url: URL, auth: DashboardWindowAuth, updateBridgeEnabled: Bool? = nil) {
self.update(url: url, auth: auth, updateBridgeEnabled: updateBridgeEnabled)
self.show()
}
@@ -202,10 +242,13 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
/// the remote tunnel is recreated on a new local port while the window stays
/// open; ordering the window front here would steal focus on background
/// tunnel recreation.
func update(url: URL, auth: DashboardWindowAuth) {
func update(url: URL, auth: DashboardWindowAuth, updateBridgeEnabled: Bool? = nil) {
self.currentURL = url
self.auth = auth
self.refreshNativeAuthScript(url: url, auth: auth)
if let updateBridgeEnabled {
self.setUpdateBridgeEnabled(updateBridgeEnabled)
}
self.load(url)
}
@@ -239,6 +282,7 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
func showFailure(title: String, message: String, detail: String? = nil) {
self.currentURL = URL(string: "about:blank")!
self.auth = DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil)
self.setUpdateBridgeEnabled(false)
self.refreshNativeAuthScript(url: self.currentURL, auth: self.auth)
self.webView.stopLoading()
self.webView.loadHTMLString(
@@ -321,6 +365,32 @@ final class DashboardWindowController: NSWindowController, WKNavigationDelegate,
return payload["type"] as? String == "window-drag"
}
fileprivate func receiveUpdateMessage(_ message: WKScriptMessage) {
guard message.name == Self.updateMessageHandlerName,
message.webView === self.webView,
message.frameInfo.isMainFrame,
Self.isTrustedLinkSource(message.frameInfo.request.url, dashboardURL: self.currentURL),
Self.isStartUpdateRequest(message.body),
let updater = self.updater
else {
return
}
// Eligibility is cached at window setup, but update.channel or launchd
// ownership can change while the dashboard stays open. Revalidate here;
// dropping the bridge makes the Control UI's next click fall back to
// the direct gateway update flow.
guard DashboardManager.updateBridgeEnabled(mode: AppStateStore.shared.connectionMode) else {
self.setUpdateBridgeEnabled(false)
return
}
updater.checkForUpdates(nil)
}
static func isStartUpdateRequest(_ body: Any) -> Bool {
guard let payload = body as? [String: Any] else { return false }
return payload["type"] as? String == "start-update"
}
static func linkRequest(from body: Any) -> DashboardLinkRequest? {
guard let payload = body as? [String: Any],
payload["type"] as? String == "open-link",
@@ -861,6 +931,10 @@ extension DashboardWindowController {
self.webView.configuration.userContentController.userScripts
}
var _testUpdateBridgeAvailable: Bool {
self.updateBridgeEnabled
}
var _testLinkBrowserIsCollapsed: Bool {
self.linkBrowserItem.isCollapsed
}
@@ -95,8 +95,8 @@ enum GatewayLaunchAgentManager {
return await self.runDaemonCommand(["uninstall"])
}
static func kickstart() async {
_ = await self.runDaemonCommand(["restart"], timeout: 20)
static func kickstart() async -> String? {
await self.runDaemonCommand(["restart"], timeout: 20)
}
static func launchdConfigSnapshot() -> LaunchAgentPlistSnapshot? {
+19 -1
View File
@@ -308,6 +308,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
var openDashboardAction: @MainActor () -> Void = { AppNavigationActions.openDashboard() }
let updaterController: UpdaterProviding = makeUpdaterController()
func applicationWillFinishLaunching(_: Notification) {
// URL/reopen callbacks can create the dashboard before didFinishLaunching.
DashboardManager.shared.configure(updater: self.updaterController)
}
func applicationDockMenu(_: NSApplication) -> NSMenu? {
let menu = NSMenu()
menu.autoenablesItems = false
@@ -673,7 +678,20 @@ final class SparkleUpdaterController: NSObject, UpdaterProviding {
}
}
extension SparkleUpdaterController: SPUUpdaterDelegate {}
func allowedSparkleChannels(forGatewayUpdateChannel channel: String?) -> Set<String> {
switch channel?.lowercased() {
case "beta", "dev":
["beta"]
default:
[]
}
}
extension SparkleUpdaterController: SPUUpdaterDelegate {
func allowedChannels(for _: SPUUpdater) -> Set<String> {
allowedSparkleChannels(forGatewayUpdateChannel: OpenClawConfigFile.gatewayUpdateChannel())
}
}
private func isDeveloperIDSigned(bundleURL: URL) -> Bool {
var staticCode: SecStaticCode?
@@ -202,6 +202,12 @@ enum OpenClawConfigFile {
self.saveDict(root)
}
static func gatewayUpdateChannel() -> String? {
let root = self.loadDict()
let update = root["update"] as? [String: Any]
return update?["channel"] as? String
}
static func browserControlEnabled(defaultValue: Bool = true) -> Bool {
let root = self.loadDict()
let browser = root["browser"] as? [String: Any]
@@ -381,7 +381,7 @@ struct TailscaleIntegrationSection: View {
private func restartGatewayIfNeeded() {
guard self.connectionMode == .local, !self.isPaused else { return }
Task { await GatewayLaunchAgentManager.kickstart() }
Task { _ = await GatewayLaunchAgentManager.kickstart() }
}
private func currentSettingsSnapshot() -> GatewayTailscaleSettingsSnapshot {
@@ -130,6 +130,27 @@ struct CLIInstallerTests {
location: location,
found: "2026.6.1",
required: "2026.7.3"))
#expect(CLIInstaller.classifyVersion(
location: location,
output: "2026.7.3-beta.1\n",
expectedVersion: "2026.7.3-beta.2") == .incompatible(
location: location,
found: "2026.7.3-beta.1",
required: "2026.7.3-beta.2"))
#expect(CLIInstaller.classifyVersion(
location: location,
output: "2026.7.3-beta.2\n",
expectedVersion: "2026.7.3") == .incompatible(
location: location,
found: "2026.7.3-beta.2",
required: "2026.7.3"))
#expect(CLIInstaller.classifyVersion(
location: location,
output: "2026.7.3-alpha.1\n",
expectedVersion: "2026.7.3") == .incompatible(
location: location,
found: "2026.7.3-alpha.1",
required: "2026.7.3"))
}
@Test func `compatible external CLI satisfies setup`() async throws {
@@ -0,0 +1,189 @@
import Foundation
import Testing
@testable import OpenClaw
@Suite(.serialized)
@MainActor
struct UpdateOrchestrationTests {
@Test func `Sparkle channels follow the Gateway update channel`() {
#expect(allowedSparkleChannels(forGatewayUpdateChannel: "beta") == ["beta"])
#expect(allowedSparkleChannels(forGatewayUpdateChannel: "dev") == ["beta"])
#expect(allowedSparkleChannels(forGatewayUpdateChannel: "stable").isEmpty)
#expect(allowedSparkleChannels(forGatewayUpdateChannel: "extended-stable").isEmpty)
#expect(allowedSparkleChannels(forGatewayUpdateChannel: "future").isEmpty)
#expect(allowedSparkleChannels(forGatewayUpdateChannel: nil).isEmpty)
}
@Test func `dashboard accepts only start update payloads`() {
#expect(DashboardWindowController.isStartUpdateRequest(["type": "start-update"]))
#expect(!DashboardWindowController.isStartUpdateRequest(["type": "update.run"]))
#expect(!DashboardWindowController.isStartUpdateRequest("start-update"))
}
@Test func `dashboard exposes update bridge only for available updater`() throws {
let url = try #require(URL(string: "http://127.0.0.1:18789/control/"))
let auth = DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil)
let available = TestUpdater(isAvailable: true)
let enabled = DashboardWindowController(url: url, auth: auth, updater: available)
let disabled = DashboardWindowController(
url: url,
auth: auth,
updater: TestUpdater(isAvailable: false))
let remote = DashboardWindowController(
url: url,
auth: auth,
updater: available,
updateBridgeEnabled: false)
#expect(enabled._testUpdateBridgeAvailable)
#expect(!disabled._testUpdateBridgeAvailable)
#expect(!remote._testUpdateBridgeAvailable)
remote.setUpdateBridgeEnabled(true)
#expect(remote._testUpdateBridgeAvailable)
}
@Test func `automatic repair is limited to incompatible managed install`() {
let managed = CLIInstaller.managedExecutableLocation()
#expect(CLIInstallPrompter.shouldAutomaticallyRepair(status: .incompatible(
location: managed,
found: "2026.7.1",
required: "2026.7.2"), launchAgentUsesManagedCLI: true, launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(status: .incompatible(
location: "/opt/homebrew/bin/openclaw",
found: "2026.7.1",
required: "2026.7.2"), launchAgentUsesManagedCLI: true, launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(
status: .missing(location: managed),
launchAgentUsesManagedCLI: true,
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(
status: .unusable(location: managed),
launchAgentUsesManagedCLI: true,
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(
status: .incompatible(location: managed, found: "2026.7.1", required: "2026.7.2"),
launchAgentUsesManagedCLI: false,
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(
status: .incompatible(location: managed, found: "2026.7.1", required: "2026.7.2"),
launchAgentUsesManagedCLI: true,
launchAgentWriteDisabled: true))
// Never silently downgrade a gateway the user moved ahead of the app.
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(status: .incompatible(
location: managed,
found: "2026.7.3",
required: "2026.7.2"), launchAgentUsesManagedCLI: true, launchAgentWriteDisabled: false))
// Extended-stable pins an older gateway on purpose; keep the prompt.
#expect(!CLIInstallPrompter.shouldAutomaticallyRepair(
status: .incompatible(location: managed, found: "2026.7.1", required: "2026.7.2"),
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: "extended-stable",
launchAgentWriteDisabled: false))
#expect(CLIInstallPrompter.shouldAutomaticallyRepair(
status: .incompatible(location: managed, found: "2026.7.1", required: "2026.7.2"),
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: "beta",
launchAgentWriteDisabled: false))
}
@Test func `managed repair only upgrades`() {
#expect(CLIInstallPrompter.isManagedUpgrade(found: "2026.7.1", required: "2026.7.2"))
#expect(!CLIInstallPrompter.isManagedUpgrade(found: "2026.7.2", required: "2026.7.1"))
#expect(!CLIInstallPrompter.isManagedUpgrade(found: "2026.7.2", required: "2026.7.2"))
// Prerelease of the same triple sorts below its release.
#expect(CLIInstallPrompter.isManagedUpgrade(found: "2026.7.2-beta.1", required: "2026.7.2"))
#expect(!CLIInstallPrompter.isManagedUpgrade(found: "2026.7.2", required: "2026.7.2-beta.1"))
#expect(CLIInstallPrompter.isManagedUpgrade(
found: "2026.7.2-beta.1",
required: "2026.7.2-beta.2"))
#expect(CLIInstallPrompter.isManagedUpgrade(
found: "2026.7.2-beta.2",
required: "2026.7.2-beta.10"))
#expect(!CLIInstallPrompter.isManagedUpgrade(
found: "2026.7.2-beta.2",
required: "2026.7.2-beta.1"))
#expect(!CLIInstallPrompter.isManagedUpgrade(found: "garbage", required: "2026.7.2"))
}
@Test func `managed Gateway ownership ignores the generated environment wrapper`() {
let home = FileManager.default.homeDirectoryForCurrentUser.path
let wrapper = "\(home)/.openclaw/state/service-env/ai.openclaw.gateway-env-wrapper.sh"
let environment = "\(home)/.openclaw/state/service-env/ai.openclaw.gateway.env"
let managedEntry = "\(home)/.openclaw/lib/node_modules/openclaw/dist/index.js"
#expect(CLIInstallPrompter.launchAgentUsesManagedCLI(programArguments: [
wrapper,
environment,
"/usr/local/bin/node",
managedEntry,
"gateway",
]))
#expect(!CLIInstallPrompter.launchAgentUsesManagedCLI(programArguments: [
wrapper,
environment,
"/usr/local/bin/node",
"/opt/homebrew/lib/node_modules/openclaw/dist/index.js",
"gateway",
]))
#expect(!CLIInstallPrompter.launchAgentUsesManagedCLI(programArguments: [
wrapper,
environment,
"\(home)/.openclaw/tools/node/bin/node",
"/opt/homebrew/lib/node_modules/openclaw/dist/index.js",
"gateway",
]))
}
@Test func `managed repair gates cover bridge and repair alike`() {
#expect(CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: nil,
launchAgentWriteDisabled: false))
#expect(CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: "beta",
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: false,
gatewayUpdateChannel: nil,
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: "extended-stable",
launchAgentWriteDisabled: false))
#expect(!CLIInstallPrompter.managedRepairGatesOpen(
launchAgentUsesManagedCLI: true,
gatewayUpdateChannel: nil,
launchAgentWriteDisabled: true))
}
@Test func `pending managed restart marker round trips`() {
CLIInstallPrompter.clearPendingManagedRestart()
#expect(!CLIInstallPrompter.hasPendingManagedRestart())
CLIInstallPrompter.setPendingManagedRestart()
#expect(CLIInstallPrompter.hasPendingManagedRestart())
CLIInstallPrompter.clearPendingManagedRestart()
#expect(!CLIInstallPrompter.hasPendingManagedRestart())
}
@Test func `managed Gateway restart requires a new running process`() {
#expect(CLIInstallPrompter.didManagedGatewayRestart(previousPID: nil, currentPID: 41))
#expect(CLIInstallPrompter.didManagedGatewayRestart(previousPID: 40, currentPID: 41))
#expect(!CLIInstallPrompter.didManagedGatewayRestart(previousPID: 41, currentPID: 41))
#expect(!CLIInstallPrompter.didManagedGatewayRestart(previousPID: 41, currentPID: nil))
}
}
@MainActor
private final class TestUpdater: UpdaterProviding {
var automaticallyChecksForUpdates = false
var automaticallyDownloadsUpdates = false
let isAvailable: Bool
let updateStatus = UpdateStatus()
init(isAvailable: Bool) {
self.isAvailable = isAvailable
}
func checkForUpdates(_: Any?) {}
}
+1
View File
@@ -5177,6 +5177,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
- Headings:
- H2: Download
- H2: First run
- H2: Updates
- H2: Open dashboard links
- H2: Choose a Gateway mode
- H2: What the app owns
+4
View File
@@ -241,6 +241,10 @@ LaunchAgent when possible. If the Gateway cannot make that handoff safely,
`update.run` reports a safe shell command instead of running the package
manager in-process.
The Control UI sidebar update card starts this same `update.run` flow. In the
signed macOS app, the card updates the app through Sparkle first; after relaunch,
the app brings its managed local Gateway to the matching version.
## After updating
<Steps>
+13
View File
@@ -41,6 +41,19 @@ available for recovery.
For the CLI/Gateway setup path, use [Getting started](/start/getting-started).
For permission recovery, use [macOS permissions](/platforms/mac/permissions).
## Updates
The dashboard update card updates the signed macOS app through Sparkle first.
After the app relaunches, it automatically updates and restarts the matching
app-managed local Gateway. Homebrew and other user-managed CLI installs keep
the normal Gateway update flow (the card runs the Gateway update directly),
and the automatic repair never downgrades a newer Gateway or overrides an
`extended-stable` channel pin.
Sparkle follows the Gateway's `update.channel` setting. `beta` and `dev` opt in
to beta app builds; `stable`, `extended-stable`, and missing or unknown values
stay on stable app builds.
## Open dashboard links
In the macOS app's embedded dashboard, clicking an external web link opens it in a resizable browser sidebar. Each link opens in its own tab; clicking the same link again reuses its existing tab. Drag tabs to reorder them, close them with the tab close button or a middle-click, and right-click a tab for **Open in Default Browser**, **Copy Link**, **Reload**, **Close Tab**, and **Close Other Tabs**. The window's titlebar back/forward controls and trackpad swipes navigate dashboard history; the sidebar's own back/forward controls navigate the active tab's history. The sidebar also has reload, open-in-default-browser, and close controls, and it remembers its width.
+10
View File
@@ -38,6 +38,15 @@ if [[ -z "$VERSION" ]]; then
fi
fi
CHANNEL_ARGS=()
if [[ "$VERSION" == *-alpha.* || "$VERSION" == *.alpha.* ]]; then
echo "Alpha releases do not ship via Sparkle: $VERSION" >&2
exit 1
fi
if [[ "$VERSION" == *-beta.* || "$VERSION" == *.beta.* ]]; then
CHANNEL_ARGS=(--channel beta)
fi
TMP_DIR="$(mktemp -d)"
NOTES_HTML=""
cleanup() {
@@ -74,6 +83,7 @@ fi
--download-url-prefix "$DOWNLOAD_URL_PREFIX" \
--embed-release-notes \
--link "$FEED_URL" \
"${CHANNEL_ARGS[@]}" \
"$TMP_DIR"
cp -f "$TMP_DIR/appcast.xml" "$ROOT/appcast.xml"
+4
View File
@@ -1107,6 +1107,7 @@ export function collectAppcastSparkleVersionErrors(xml: string): string[] {
const title = extractTag(item, "title") ?? "unknown";
const shortVersion = extractTag(item, "sparkle:shortVersionString");
const sparkleVersion = extractTag(item, "sparkle:version");
const sparkleChannel = extractTag(item, "sparkle:channel");
if (!sparkleVersion) {
errors.push(`appcast item '${title}' is missing sparkle:version.`);
@@ -1120,6 +1121,9 @@ export function collectAppcastSparkleVersionErrors(xml: string): string[] {
if (!shortVersion) {
continue;
}
if (/(?:^|[.-])beta(?:[.-]|$)/i.test(shortVersion) && sparkleChannel !== "beta") {
errors.push(`appcast item '${title}' must set sparkle:channel to 'beta'.`);
}
const floors = sparkleBuildFloorsFromShortVersion(shortVersion);
if (floors === null) {
errors.push(
+7 -1
View File
@@ -482,7 +482,13 @@ describe("gateway broadcaster", () => {
broadcast("health", { ok: true });
broadcast("tick", { ts: 2 });
broadcast("shutdown", { reason: "restart" });
broadcast("update.available", { updateAvailable: { version: "2026.4.20" } });
broadcast("update.available", {
updateAvailable: {
currentVersion: "2026.4.19",
latestVersion: "2026.4.20",
channel: "stable",
},
});
broadcast("unknown.future.event", { hidden: true });
expectSentEvents(pairingSocket, [
+18 -3
View File
@@ -45,8 +45,9 @@ import {
} from "../src/infra/package-dist-inventory.ts";
import { withEnv } from "../src/test-utils/env.js";
function makeItem(shortVersion: string, sparkleVersion: string): string {
return `<item><title>${shortVersion}</title><sparkle:shortVersionString>${shortVersion}</sparkle:shortVersionString><sparkle:version>${sparkleVersion}</sparkle:version></item>`;
function makeItem(shortVersion: string, sparkleVersion: string, channel?: string): string {
const channelElement = channel ? `<sparkle:channel>${channel}</sparkle:channel>` : "";
return `<item><title>${shortVersion}</title><sparkle:shortVersionString>${shortVersion}</sparkle:shortVersionString><sparkle:version>${sparkleVersion}</sparkle:version>${channelElement}</item>`;
}
function makePackResult(filename: string, unpackedSize: number) {
@@ -82,8 +83,22 @@ describe("collectAppcastSparkleVersionErrors", () => {
expect(collectAppcastSparkleVersionErrors(xml)).toStrictEqual([]);
});
it("accepts canonical beta lane builds", () => {
const xml = `<rss><channel>${makeItem("2026.6.5-beta.2", "2606000502", "beta")}</channel></rss>`;
expect(collectAppcastSparkleVersionErrors(xml)).toStrictEqual([]);
});
it("rejects beta builds on the default channel", () => {
const xml = `<rss><channel>${makeItem("2026.6.5-beta.2", "2606000502")}</channel></rss>`;
expect(collectAppcastSparkleVersionErrors(xml)).toEqual([
"appcast item '2026.6.5-beta.2' must set sparkle:channel to 'beta'.",
]);
});
it("rejects appcast entries with invalid prerelease lanes", () => {
const xml = `<rss><channel>${makeItem("2026.6.5-beta.0", "2606000500")}</channel></rss>`;
const xml = `<rss><channel>${makeItem("2026.6.5-beta.0", "2606000500", "beta")}</channel></rss>`;
expect(collectAppcastSparkleVersionErrors(xml)).toEqual([
"appcast item '2026.6.5-beta.0' has invalid sparkle:shortVersionString '2026.6.5-beta.0'.",
+9
View File
@@ -21,4 +21,13 @@ describe("make_appcast cleanup", () => {
);
expect(setupBlock).toContain('rm -f "$NOTES_HTML"');
});
it("adds the beta channel and refuses alpha releases", () => {
const script = readFileSync(scriptPath, "utf8");
expect(script).toContain('if [[ "$VERSION" == *-beta.* || "$VERSION" == *.beta.* ]]; then');
expect(script).toContain("CHANNEL_ARGS=(--channel beta)");
expect(script).toContain('if [[ "$VERSION" == *-alpha.* || "$VERSION" == *.alpha.* ]]; then');
expect(script).toContain('"${CHANNEL_ARGS[@]}"');
});
});
+6 -5
View File
@@ -897,6 +897,9 @@ class OpenClawShell extends OpenClawLightDomElement {
context.config.current.serverVersion ??
gatewaySnapshot.hello?.server?.version ??
"",
updateAvailable: overlaySnapshot.updateAvailable,
updateRunning: overlaySnapshot.updateRunning,
onUpdate: () => void context.overlays.runUpdate(),
searchQuery: this.settingsSearchQuery,
onExit: () => this.exitSettings(),
onNavigate: (routeId) => this.navigate(routeId),
@@ -924,6 +927,9 @@ class OpenClawShell extends OpenClawLightDomElement {
gatewaySnapshot.hello?.server?.version ??
null}
.devGitBranch=${context.config.current.devGitBranch}
.updateAvailable=${overlaySnapshot.updateAvailable}
.updateRunning=${overlaySnapshot.updateRunning}
.onUpdate=${() => void context.overlays.runUpdate()}
.onOpenPalette=${this.openPalette}
.onToggleSidebar=${() => this.toggleNavigationSurface()}
.onOpenNewSession=${(agentId: string) => {
@@ -977,11 +983,6 @@ class OpenClawShell extends OpenClawLightDomElement {
<openclaw-update-banner
.props=${{
statusBanner: overlaySnapshot.updateStatusBanner,
updateAvailable: overlaySnapshot.updateAvailable,
updateRunning: overlaySnapshot.updateRunning,
connected: gatewaySnapshot.connected,
onUpdate: () => context.overlays.runUpdate(),
onDismiss: () => context.overlays.dismissUpdate(),
}}
></openclaw-update-banner>
<openclaw-router-outlet
+24
View File
@@ -13,6 +13,14 @@ type WebKitMessageHandler = {
postMessage(message: NativeLinkMessage): void;
};
type NativeUpdateMessage = {
type: "start-update";
};
type WebKitUpdateMessageHandler = {
postMessage(message: NativeUpdateMessage): void;
};
export type NativeLinkRouting = {
dispose(): void;
};
@@ -27,6 +35,22 @@ function getNativeLinkPoster(): WebKitMessageHandler["postMessage"] | undefined
return handler?.postMessage.bind(handler);
}
export function postNativeUpdate(): boolean {
const handler = (
window as unknown as {
webkit?: { messageHandlers?: { openclawUpdate?: WebKitUpdateMessageHandler } };
}
).webkit?.messageHandlers?.openclawUpdate;
if (!handler) {
return false;
}
// Bound single-argument WebKit handler call, not window.postMessage;
// binding also keeps oxlint's targetOrigin rule out of the wrong context.
const poster = handler.postMessage.bind(handler);
poster({ type: "start-update" });
return true;
}
function anchorFromEvent(event: Event): HTMLAnchorElement | null {
for (const target of event.composedPath()) {
if (target instanceof HTMLAnchorElement) {
-5
View File
@@ -49,7 +49,6 @@ export type ApplicationOverlays = {
readonly snapshot: ApplicationOverlaySnapshot;
subscribe: (listener: (snapshot: ApplicationOverlaySnapshot) => void) => () => void;
runUpdate: () => Promise<void>;
dismissUpdate: () => void;
decideApproval: (decision: ExecApprovalDecision) => Promise<void>;
openDevicePairSetup: () => Promise<void>;
refreshDevicePairSetup: () => Promise<void>;
@@ -611,10 +610,6 @@ export function createApplicationOverlays(gateway: ApplicationGateway): Applicat
}
}
},
dismissUpdate() {
snapshot = { ...snapshot, updateAvailable: null };
publish();
},
async decideApproval(decision) {
const active = promptState.execApprovalQueue[0];
const client = gateway.snapshot.client;
+41 -1
View File
@@ -2,7 +2,7 @@
import { ContextProvider } from "@lit/context";
import { LitElement } from "lit";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { GatewayBrowserClient } from "../api/gateway.ts";
import type { SessionsListResult } from "../api/types.ts";
import type { RouteId } from "../app-route-paths.ts";
@@ -13,6 +13,7 @@ import {
type ApplicationGatewaySnapshot,
} from "../app/context.ts";
import type { SessionCapability, SessionState } from "../lib/sessions/index.ts";
import { createStorageMock } from "../test-helpers/storage.ts";
import "./app-sidebar.ts";
const PROVIDER_ELEMENT_NAME = "test-app-sidebar-context-provider";
@@ -38,6 +39,9 @@ type SidebarLifecycleState = HTMLElement & {
sessionsAgentId: string | null;
sessionsResult: SessionsListResult | null;
updateComplete: Promise<boolean>;
updateAvailable: { currentVersion: string; latestVersion: string; channel: string } | null;
updateRunning: boolean;
onUpdate: () => void;
variant: "panel" | "drawer";
};
@@ -154,6 +158,16 @@ function createSessions(agentId: string, keys: string[]): SessionCapability {
return createSessionsHarness(agentId, keys).sessions;
}
let originalLocalStorage: PropertyDescriptor | undefined;
beforeEach(() => {
originalLocalStorage = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: createStorageMock(),
});
});
function createContext(
gateway: ApplicationGateway,
sessions: SessionCapability,
@@ -192,6 +206,32 @@ async function mountSidebar(
afterEach(() => {
document.body.replaceChildren();
if (originalLocalStorage) {
Object.defineProperty(globalThis, "localStorage", originalLocalStorage);
} else {
Reflect.deleteProperty(globalThis, "localStorage");
}
});
describe("AppSidebar update card wiring", () => {
it("renders the update card first in the footer and forwards its action", async () => {
const gateway = createGateway({} as GatewayBrowserClient);
const { sidebar } = await mountSidebar(gateway, createSessions("main", ["agent:main:main"]));
const onUpdate = vi.fn();
sidebar.updateAvailable = {
currentVersion: "1.0.0",
latestVersion: "2.0.0",
channel: "stable",
};
sidebar.onUpdate = onUpdate;
await sidebar.updateComplete;
const footer = sidebar.querySelector(".sidebar-shell__footer");
const card = footer?.firstElementChild;
expect(card?.localName).toBe("openclaw-sidebar-update-card");
card?.querySelector<HTMLButtonElement>(".sidebar-update-card__action")?.click();
expect(onUpdate).toHaveBeenCalledOnce();
});
});
describe("AppSidebar lobster outcome wiring", () => {
+10 -1
View File
@@ -3,7 +3,7 @@ import { html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { keyed } from "lit/directives/keyed.js";
import type { GatewayBrowserClient, GatewayControlUiPluginTab } from "../api/gateway.ts";
import type { SessionsListResult } from "../api/types.ts";
import type { SessionsListResult, UpdateAvailable } from "../api/types.ts";
import {
cancelRoutePreload,
DEFAULT_SIDEBAR_PINNED_ROUTES,
@@ -26,6 +26,7 @@ import { controlUiPublicAssetPath } from "../app/public-assets.ts";
import { isViteDevPage } from "../app/settings.ts";
import type { ThemeMode } from "../app/theme.ts";
import "./session-menu.ts";
import "./sidebar-update-card.ts";
import "./theme-mode-toggle.ts";
import "./tooltip.ts";
import { CONTROL_UI_BUILD_INFO } from "../build-info.ts";
@@ -208,6 +209,9 @@ class AppSidebar extends OpenClawLightDomContentsElement {
@property({ attribute: false }) lobsterPetSounds = false;
@property({ attribute: false }) gatewayVersion: string | null = null;
@property({ attribute: false }) devGitBranch: string | null = null;
@property({ attribute: false }) updateAvailable: UpdateAvailable | null = null;
@property({ attribute: false }) updateRunning = false;
@property({ attribute: false }) onUpdate: () => void = () => undefined;
@property({ attribute: false }) onOpenPalette?: () => void;
@property({ attribute: false }) onToggleSidebar?: () => void;
@property({ attribute: false }) onOpenNewSession?: (agentId: string) => void;
@@ -1924,6 +1928,11 @@ class AppSidebar extends OpenClawLightDomContentsElement {
${this.renderSessions()}
</div>
<div class="sidebar-shell__footer">
<openclaw-sidebar-update-card
.updateAvailable=${this.updateAvailable}
.updateRunning=${this.updateRunning}
.onUpdate=${this.onUpdate}
></openclaw-sidebar-update-card>
<openclaw-lobster-pet
.seed=${lobsterPetSeed(this.sessionKey)}
.mode=${resolveLobsterPetMode(this.connected, this.sessionsResult?.sessions)}
@@ -28,6 +28,9 @@ describe("settings sidebar search", () => {
activeRouteId: "config",
connected: true,
version: "",
updateAvailable: null,
updateRunning: false,
onUpdate: vi.fn(),
searchQuery,
onExit: vi.fn(),
onNavigate,
@@ -81,4 +84,37 @@ describe("settings sidebar search", () => {
?.click();
expect(onNavigate).toHaveBeenCalledWith("channels");
});
it("keeps the update card above the settings footer", async () => {
const onUpdate = vi.fn();
render(
renderSettingsSidebar({
basePath: "",
activeRouteId: "config",
connected: true,
version: "1.0.0",
updateAvailable: {
currentVersion: "1.0.0",
latestVersion: "2.0.0",
channel: "stable",
},
updateRunning: false,
onUpdate,
searchQuery: "",
onExit: vi.fn(),
onNavigate: vi.fn(),
onSearchQueryChange: vi.fn(),
preloadTimers: new Map(),
}),
container,
);
const card = container.querySelector<HTMLElement & { updateComplete: Promise<boolean> }>(
"openclaw-sidebar-update-card",
);
await card?.updateComplete;
expect(card?.nextElementSibling?.classList.contains("settings-sidebar__footer")).toBe(true);
card?.querySelector<HTMLButtonElement>(".sidebar-update-card__action")?.click();
expect(onUpdate).toHaveBeenCalledOnce();
});
});
+10
View File
@@ -1,5 +1,6 @@
// Dedicated sidebar for the full-page settings takeover (see app-host.ts).
import { html, nothing } from "lit";
import type { UpdateAvailable } from "../api/types.ts";
import {
cancelRoutePreload,
navigationIconForRoute,
@@ -13,12 +14,16 @@ import { pathForRoute, type RouteId } from "../app-route-paths.ts";
import { t } from "../i18n/index.ts";
import { normalizeLowercaseStringOrEmpty } from "../lib/string-coerce.ts";
import { icons } from "./icons.ts";
import "./sidebar-update-card.ts";
type SettingsSidebarProps = {
basePath: string;
activeRouteId: RouteId;
connected: boolean;
version: string;
updateAvailable: UpdateAvailable | null;
updateRunning: boolean;
onUpdate: () => void;
searchQuery: string;
onExit: () => void;
onNavigate: (routeId: RouteId) => void;
@@ -164,6 +169,11 @@ export function renderSettingsSidebar(props: SettingsSidebarProps) {
`,
)}
</nav>
<openclaw-sidebar-update-card
.updateAvailable=${props.updateAvailable}
.updateRunning=${props.updateRunning}
.onUpdate=${props.onUpdate}
></openclaw-sidebar-update-card>
<footer class="settings-sidebar__footer">
<span
class="sidebar-status__dot ${props.connected
@@ -0,0 +1,179 @@
/* @vitest-environment jsdom */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { UpdateAvailable } from "../api/types.ts";
import { createStorageMock } from "../test-helpers/storage.ts";
import "./sidebar-update-card.ts";
const DISMISS_KEY = "openclaw:control-ui:update-banner-dismissed:v1";
type SidebarUpdateCardElement = HTMLElement & {
updateAvailable: UpdateAvailable | null;
updateRunning: boolean;
onUpdate: () => void;
updateComplete: Promise<boolean>;
};
let originalWebkit: PropertyDescriptor | undefined;
let originalLocalStorage: PropertyDescriptor | undefined;
async function mount(update: UpdateAvailable | null) {
const element = document.createElement(
"openclaw-sidebar-update-card",
) as SidebarUpdateCardElement;
element.updateAvailable = update;
document.body.append(element);
await element.updateComplete;
return element;
}
beforeEach(() => {
originalWebkit = Object.getOwnPropertyDescriptor(window, "webkit");
originalLocalStorage = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: createStorageMock(),
});
});
afterEach(() => {
document.body.replaceChildren();
if (originalLocalStorage) {
Object.defineProperty(globalThis, "localStorage", originalLocalStorage);
} else {
Reflect.deleteProperty(globalThis, "localStorage");
}
if (originalWebkit) {
Object.defineProperty(window, "webkit", originalWebkit);
} else {
Reflect.deleteProperty(window, "webkit");
}
});
describe("SidebarUpdateCard", () => {
it("renders an available update and invokes the gateway action", async () => {
const element = await mount({
currentVersion: "1.0.0",
latestVersion: "2.0.0",
channel: "stable",
});
const onUpdate = vi.fn();
element.onUpdate = onUpdate;
const action = element.querySelector<HTMLButtonElement>(".sidebar-update-card__action");
expect(element.querySelector(".sidebar-update-card")?.getAttribute("role")).toBe("status");
expect(action?.textContent).toContain("Update available");
expect(action?.textContent).toContain("v2.0.0");
action?.click();
expect(onUpdate).toHaveBeenCalledOnce();
});
it.each([null, { currentVersion: "2.0.0", latestVersion: "2.0.0", channel: "stable" }] as const)(
"renders nothing when no newer update is available",
async (update) => {
const element = await mount(update);
expect(element.querySelector(".sidebar-update-card")).toBeNull();
},
);
it("renders nothing for a dismissed version and channel", async () => {
localStorage.setItem(
DISMISS_KEY,
JSON.stringify({ latestVersion: "2.0.0", channel: "beta", dismissedAtMs: 1 }),
);
const element = await mount({
currentVersion: "1.0.0",
latestVersion: "2.0.0",
channel: "beta",
});
expect(element.querySelector(".sidebar-update-card")).toBeNull();
});
it("routes updates to the native bridge when present", async () => {
const postMessage = vi.fn();
Object.defineProperty(window, "webkit", {
configurable: true,
value: { messageHandlers: { openclawUpdate: { postMessage } } },
});
const element = await mount({
currentVersion: "1.0.0",
latestVersion: "2.0.0",
channel: "stable",
});
const onUpdate = vi.fn();
element.onUpdate = onUpdate;
element.querySelector<HTMLButtonElement>(".sidebar-update-card__action")?.click();
expect(postMessage).toHaveBeenCalledWith({ type: "start-update" });
expect(onUpdate).not.toHaveBeenCalled();
});
it("disables the action while updating", async () => {
const element = await mount({
currentVersion: "1.0.0",
latestVersion: "2.0.0",
channel: "stable",
});
element.updateRunning = true;
await element.updateComplete;
const action = element.querySelector<HTMLButtonElement>(".sidebar-update-card__action");
expect(action?.disabled).toBe(true);
expect(action?.textContent).toContain("Updating…");
});
it("persists dismissal and hides the card", async () => {
const element = await mount({
currentVersion: "1.0.0",
latestVersion: "2.0.0",
channel: "stable",
});
element.querySelector<HTMLButtonElement>(".sidebar-update-card__dismiss")?.click();
await element.updateComplete;
expect(JSON.parse(localStorage.getItem(DISMISS_KEY) ?? "null")).toMatchObject({
latestVersion: "2.0.0",
channel: "stable",
});
expect(element.querySelector(".sidebar-update-card")).toBeNull();
});
it("hides the card when dismissal persistence fails", async () => {
const storage = createStorageMock();
storage.setItem = () => {
throw new Error("quota exceeded");
};
Object.defineProperty(globalThis, "localStorage", { configurable: true, value: storage });
const update = { currentVersion: "1.0.0", latestVersion: "3.0.0", channel: "stable" };
const element = await mount(update);
element.querySelector<HTMLButtonElement>(".sidebar-update-card__dismiss")?.click();
await element.updateComplete;
expect(element.querySelector(".sidebar-update-card")).toBeNull();
element.remove();
const replacement = await mount(update);
expect(replacement.querySelector(".sidebar-update-card")).not.toBeNull();
});
it("shows a newer update after dismissing an older version", async () => {
const element = await mount({
currentVersion: "1.0.0",
latestVersion: "2.0.0",
channel: "stable",
});
element.querySelector<HTMLButtonElement>(".sidebar-update-card__dismiss")?.click();
await element.updateComplete;
element.updateAvailable = {
currentVersion: "1.0.0",
latestVersion: "3.0.0",
channel: "stable",
};
await element.updateComplete;
expect(element.querySelector(".sidebar-update-card")?.textContent).toContain("v3.0.0");
});
});
+104
View File
@@ -0,0 +1,104 @@
import { html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import type { UpdateAvailable } from "../api/types.ts";
import { postNativeUpdate } from "../app/native-link-routing.ts";
import { t } from "../i18n/index.ts";
import { OpenClawLightDomContentsElement } from "../lit/openclaw-element.ts";
import { getSafeLocalStorage } from "../local-storage.ts";
import { icons } from "./icons.ts";
const UPDATE_BANNER_DISMISS_KEY = "openclaw:control-ui:update-banner-dismissed:v1";
type DismissedUpdate = {
latestVersion: string;
channel: string | null;
dismissedAtMs: number;
};
function updateKey(update: UpdateAvailable): string {
return `${update.latestVersion}\u0000${update.channel}`;
}
function isDismissed(update: UpdateAvailable): boolean {
try {
const raw = getSafeLocalStorage()?.getItem(UPDATE_BANNER_DISMISS_KEY);
if (!raw) {
return false;
}
const dismissed = JSON.parse(raw) as Partial<DismissedUpdate>;
return dismissed.latestVersion === update.latestVersion && dismissed.channel === update.channel;
} catch {
return false;
}
}
function dismiss(update: UpdateAvailable): void {
try {
getSafeLocalStorage()?.setItem(
UPDATE_BANNER_DISMISS_KEY,
JSON.stringify({
latestVersion: update.latestVersion,
channel: update.channel,
dismissedAtMs: Date.now(),
} satisfies DismissedUpdate),
);
} catch {
// Dismissal persistence is best effort.
}
}
class SidebarUpdateCard extends OpenClawLightDomContentsElement {
@property({ attribute: false }) updateAvailable: UpdateAvailable | null = null;
@property({ attribute: false }) updateRunning = false;
@property({ attribute: false }) onUpdate: () => void = () => undefined;
@state() private dismissedUpdateKey: string | null = null;
override render() {
const update = this.updateAvailable;
if (
!update ||
update.latestVersion === update.currentVersion ||
this.dismissedUpdateKey === updateKey(update) ||
isDismissed(update)
) {
return nothing;
}
const title = this.updateRunning ? t("chat.updating") : t("chat.sidebar.updateAvailable");
return html`
<div class="sidebar-update-card" role="status" aria-live="polite">
<button
class="sidebar-update-card__action"
type="button"
?disabled=${this.updateRunning}
@click=${() => {
if (!postNativeUpdate()) {
this.onUpdate();
}
}}
>
<span class="sidebar-update-card__icon" aria-hidden="true">${icons.download}</span>
<span class="sidebar-update-card__copy">
<span class="sidebar-update-card__title">${title}</span>
<span class="sidebar-update-card__subtitle">v${update.latestVersion}</span>
</span>
<span class="sidebar-update-card__arrow" aria-hidden="true">${icons.chevronRight}</span>
</button>
<button
class="sidebar-update-card__dismiss"
type="button"
aria-label=${t("chat.dismissUpdateBanner")}
@click=${() => {
this.dismissedUpdateKey = updateKey(update);
dismiss(update);
}}
>
${icons.x}
</button>
</div>
`;
}
}
if (!customElements.get("openclaw-sidebar-update-card")) {
customElements.define("openclaw-sidebar-update-card", SidebarUpdateCard);
}
-90
View File
@@ -1,71 +1,10 @@
// Control UI component renders update status and available-update actions.
import { html, nothing } from "lit";
import { property } from "lit/decorators.js";
import type { UpdateAvailable } from "../api/types.ts";
import { t } from "../i18n/index.ts";
import { OpenClawLightDomContentsElement } from "../lit/openclaw-element.ts";
import { getSafeLocalStorage } from "../local-storage.ts";
import { icons } from "./icons.ts";
const UPDATE_BANNER_DISMISS_KEY = "openclaw:control-ui:update-banner-dismissed:v1";
type DismissedUpdateBanner = {
latestVersion: string;
channel: string | null;
dismissedAtMs: number;
};
function loadDismissedUpdateBanner(): DismissedUpdateBanner | null {
try {
const raw = getSafeLocalStorage()?.getItem(UPDATE_BANNER_DISMISS_KEY);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw) as Partial<DismissedUpdateBanner>;
if (!parsed || typeof parsed.latestVersion !== "string") {
return null;
}
return {
latestVersion: parsed.latestVersion,
channel: typeof parsed.channel === "string" ? parsed.channel : null,
dismissedAtMs: typeof parsed.dismissedAtMs === "number" ? parsed.dismissedAtMs : Date.now(),
};
} catch {
return null;
}
}
function isDismissed(updateAvailable: UpdateAvailable): boolean {
const dismissed = loadDismissedUpdateBanner();
return Boolean(
dismissed &&
dismissed.latestVersion === updateAvailable.latestVersion &&
dismissed.channel === updateAvailable.channel,
);
}
function dismiss(updateAvailable: UpdateAvailable) {
try {
getSafeLocalStorage()?.setItem(
UPDATE_BANNER_DISMISS_KEY,
JSON.stringify({
latestVersion: updateAvailable.latestVersion,
channel: updateAvailable.channel,
dismissedAtMs: Date.now(),
} satisfies DismissedUpdateBanner),
);
} catch {
// Best effort only; dismissing the banner is not a product failure.
}
}
type UpdateBannerProps = {
statusBanner: { tone: "danger" | "warn" | "info"; text: string } | null;
updateAvailable: UpdateAvailable | null;
updateRunning: boolean;
connected: boolean;
onUpdate: () => void | Promise<void>;
onDismiss: () => void;
};
class UpdateBanner extends OpenClawLightDomContentsElement {
@@ -76,41 +15,12 @@ class UpdateBanner extends OpenClawLightDomContentsElement {
if (!props) {
return nothing;
}
const updateAvailable = props.updateAvailable;
return html`
${props.statusBanner
? html`<div class="callout ${props.statusBanner.tone}" role="alert">
${props.statusBanner.text}
</div>`
: nothing}
${updateAvailable &&
updateAvailable.latestVersion !== updateAvailable.currentVersion &&
!isDismissed(updateAvailable)
? html`<div class="update-banner callout danger" role="alert">
<strong>${t("chat.updateAvailable")}</strong> v${updateAvailable.latestVersion}
(${t("chat.runningVersion", { version: updateAvailable.currentVersion })}).
<button
class="btn btn--sm update-banner__btn"
?disabled=${props.updateRunning || !props.connected}
@click=${() => props.onUpdate()}
>
${props.updateRunning ? t("chat.updating") : t("chat.updateNow")}
</button>
<openclaw-tooltip .content=${t("common.dismiss")}>
<button
class="update-banner__close"
type="button"
aria-label=${t("chat.dismissUpdateBanner")}
@click=${() => {
dismiss(updateAvailable);
props.onDismiss();
}}
>
${icons.x}
</button>
</openclaw-tooltip>
</div>`
: nothing}
`;
}
}
+2 -2
View File
@@ -62,7 +62,7 @@ describeControlUiE2e("Control UI coalesced update E2E", () => {
},
});
await page.getByRole("button", { name: "Update now" }).click();
await page.getByRole("button", { name: /Update available/ }).click();
await page
.getByText(
"Update installed. A gateway restart is already in progress; status will refresh after it reconnects.",
@@ -71,7 +71,7 @@ describeControlUiE2e("Control UI coalesced update E2E", () => {
.waitFor();
expect(await gateway.getRequests("update.run")).toHaveLength(1);
expect(await page.getByRole("button", { name: "Update now" }).isEnabled()).toBe(true);
expect(await page.getByRole("button", { name: /Update available/ }).isEnabled()).toBe(true);
expect(pageErrors).toEqual([]);
await page.screenshot({ path: path.join(artifactDir, "coalesced-restart-banner.png") });
} finally {
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:31.154Z",
"generatedAt": "2026-07-11T07:17:58.537Z",
"locale": "ar",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -238,6 +238,7 @@
{"cache_key":"9ce80b7e5e8342aa6625f14c30985daf9367256485a8234d3524bf1e87e381e6","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.sortCreated","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Created","text_hash":"d70b9e24bca26b409b9458ceca6c9e5c2b5c3171c37ff050c6f6a0d7a4420d2a","tgt_lang":"ar","translated":"تاريخ الإنشاء","updated_at":"2026-07-06T15:07:02.499Z"}
{"cache_key":"9d21a4f3049d62d9376453d1cd7bc825fa508f73d2aaf191bcff38396f5fe319","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.newGroupPrompt","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"New group name","text_hash":"cee9e709525c90b1a97dff72c62082602b199b54036ed573926d1b11c6d54ec7","tgt_lang":"ar","translated":"اسم المجموعة الجديدة","updated_at":"2026-07-05T14:39:56.977Z"}
{"cache_key":"9d760b6f3a4062b9d47dce430073f483f2ef77205bc1d507afc42cca94e27f29","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.expand","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Expand session workspace","text_hash":"ac1d210db40c5026879774849ad74a9e1247523192a795ac33965b3ee72691c2","tgt_lang":"ar","translated":"توسيع مساحة عمل الجلسة","updated_at":"2026-06-16T14:15:34.787Z"}
{"cache_key":"9e3e7f0dd0096eac1e103abd787703e4971bc5bf2a4cda282c4264b2aa7dd80e","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"ar","translated":"يتوفر تحديث","updated_at":"2026-07-11T03:30:32.097Z"}
{"cache_key":"9ffc5d549e830a366c909b5f8b0d5c8c7e9ccb37abf90396662f7cdea62933e5","model":"gpt-5.5","provider":"openai","segment_id":"common.close","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Close","text_hash":"7d9eb7acb13e24625c404401d8e88b2350e32162455885f18276cf802f7701ed","tgt_lang":"ar","translated":"إغلاق","updated_at":"2026-07-10T15:21:17.822Z"}
{"cache_key":"a02637acc571f1e2f867f37f9ac97ef2f7133c3b5928dc78cddbeab639b4acef","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.talkModelAuto","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Auto","text_hash":"0286249762f7c94349cdc0ba3bb2255baf9a80036e2193ead1d77696f888582f","tgt_lang":"ar","translated":"تلقائي","updated_at":"2026-07-06T20:20:02.809Z"}
{"cache_key":"a03a21e088426345e53f61dc4f773d8cc3dee412c84b434d1a6fcdb4dffcf21c","model":"gpt-5.5","provider":"openai","segment_id":"chat.pullRequests.checksPassed","source_path":"ui/src/i18n/locales/ar.ts","src_lang":"en","text":"Passed","text_hash":"436fe71bb9561f0596161c4d50c7b23327b4189acaf63dc89f4f9205b67a7528","tgt_lang":"ar","translated":"نجح","updated_at":"2026-07-10T23:12:36.272Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:30.296Z",
"generatedAt": "2026-07-11T07:17:57.765Z",
"locale": "de",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -408,6 +408,7 @@
{"cache_key":"f6a4da531e49ab70c30ea50b25437a9bd8f6bbddad9d6b1e55d30f21b489387b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"de","translated":"Sitzung wiederherstellen","updated_at":"2026-07-02T14:30:04.036Z"}
{"cache_key":"f715083d23b1ef97ba627a3be48560a105a6f5d69f2d31ae4723665b31a0bb59","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentCurrentUnconfigured","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{agent} (not configured)","text_hash":"d138ab0079dea760c723d7c947d0c31178252e28e7dd70a40b9d3d85e5549b1d","tgt_lang":"de","translated":"{agent} (nicht konfiguriert)","updated_at":"2026-06-17T14:13:07.391Z"}
{"cache_key":"f77ed42a0f97e6ae7e6910b0089d3313f0965cbd17fc029d226ca41a381fea96","model":"gpt-5.5","provider":"openai","segment_id":"chat.pullRequests.checksPending","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"CI checks running","text_hash":"451411025de9b78bc4a991c45efb7131807d53f84d5a2aeaf516c802d9705401","tgt_lang":"de","translated":"CI-Prüfungen werden ausgeführt","updated_at":"2026-07-10T17:03:42.358Z"}
{"cache_key":"f80b0d34947512b1e28bd803546f27037e6753274aa5c9c8133737fb51360a07","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"de","translated":"Update verfügbar","updated_at":"2026-07-11T03:30:23.926Z"}
{"cache_key":"f8b9c85418169642438a234a9c66a06d4970c323c27ccdfc5a0fe57329b4bf69","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesReady","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count} ready","text_hash":"f5f5fd424d7c18f19a51ee147857efddc320a0ec6e1eeb4354be129425632f05","tgt_lang":"de","translated":"{count} bereit","updated_at":"2026-06-16T14:12:59.821Z"}
{"cache_key":"f9ec94bc46e4c0f48f6c5a2f33c176a7844d141a86719c562b3cbbea88c24815","model":"claude-opus-4-8","provider":"anthropic","segment_id":"codexSessions.host.sessionCount","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"{count} shown","text_hash":"e57b4adfe868fd74a183650103d820176d4960bd0bdb677d9985db09f9752867","tgt_lang":"de","translated":"{count} angezeigt","updated_at":"2026-06-16T14:13:06.672Z"}
{"cache_key":"fa934f472f72f3e8e1547695730b2ba14bf1caed6478a0b84bc4a749c4070199","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/de.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"de","translated":"Fehlt","updated_at":"2026-06-16T14:12:59.821Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:30.476Z",
"generatedAt": "2026-07-11T07:17:57.903Z",
"locale": "es",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -46,6 +46,7 @@
{"cache_key":"155ea7acf4ab1ca8bafbef77149c9d003908575f9c495c7031149db5a1359063","model":"gpt-5","provider":"openai","segment_id":"codexSessions.summary.onlineHosts","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"online","text_hash":"f6fc84c9f21c24907d6bee6eec38cabab5fa9a7be8c4a7827fe9e56f245bd2d5","tgt_lang":"es","translated":"en línea","updated_at":"2026-07-09T10:01:43.721Z"}
{"cache_key":"15e8f9aba31f108ff2a859e0f48fe1879ac2fdb22720d2ffd6faa2f040e618eb","model":"gpt-5.5","provider":"openai","segment_id":"pluginsPage.working","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Working…","text_hash":"5474eef8d0f179c707cf418e2bbb468c77cc24edc5e9f5f4e137e85e06a8eea0","tgt_lang":"es","translated":"Procesando…","updated_at":"2026-07-10T04:28:16.685Z"}
{"cache_key":"1607b905341a2f410b56987930eb556121bddd0fe25ebc52fbebaa708fed9fd9","model":"claude-opus-4-8","provider":"anthropic","segment_id":"cron.jobDetail.command","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Command","text_hash":"713166971d730f81fcf8b757f2ea239d1a0360d9f74e8f5afe60fba97105879c","tgt_lang":"es","translated":"Comando","updated_at":"2026-06-16T14:14:26.057Z"}
{"cache_key":"16085d7a4033aff6bf7ed9eb2d18ee1af6c29ceda3139fcf89122e9383ab39c9","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"es","translated":"Actualización disponible","updated_at":"2026-07-11T03:30:24.981Z"}
{"cache_key":"174cd7d2144b65b2b908a8d92c39b7b304fbd74dfcfbc018e3d9bfe039d35e9d","model":"gpt-5.5","provider":"openai","segment_id":"chat.splitView.splitRight","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Split right","text_hash":"aa9997bb1d8c23d0b88521c4093fc8c3ee01b187f78635ae4d3e16d27e8a8475","tgt_lang":"es","translated":"Dividir a la derecha","updated_at":"2026-07-06T07:23:22.549Z"}
{"cache_key":"176682877ecde3fb89aac59f0c17dfb38479d7f1a91256b9fb17aabe49307c65","model":"gpt-5","provider":"openai","segment_id":"codexSessions.empty.search","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"No sessions on this host match your search.","text_hash":"53e8e235da1a4490f8514580987af37d3693f5232882f414ce75de09cd4203f9","tgt_lang":"es","translated":"Ninguna sesión de este host coincide con tu búsqueda.","updated_at":"2026-07-09T10:01:43.721Z"}
{"cache_key":"176d49c8a5b920df5b74bf73bad328b3e3a6c4f5c8f3be0f51088b5958aaae4e","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.contextUsage.contextWindow","source_path":"ui/src/i18n/locales/es.ts","src_lang":"en","text":"Context window","text_hash":"7696d0855331622dc12438057f5509348f9d6f0ec2eb3580e18a99d31eba86db","tgt_lang":"es","translated":"Ventana de contexto","updated_at":"2026-07-05T10:16:02.806Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:32.849Z",
"generatedAt": "2026-07-11T07:17:59.667Z",
"locale": "fa",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:30.844Z",
"generatedAt": "2026-07-11T07:17:58.287Z",
"locale": "fr",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -126,6 +126,7 @@
{"cache_key":"3cb1081b70aa9b070a66d0853311d0abe28342fc8fe9b58d29477a04fa0f4929","model":"gpt-5.5","provider":"openai","segment_id":"aboutPage.built","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Built","text_hash":"cfe0e6cbcf5cdd1aab44a39ab4d39713561bafab51fa7ff4654c980d5578ee5f","tgt_lang":"fr","translated":"Build","updated_at":"2026-07-10T09:47:05.904Z"}
{"cache_key":"3ce8f87cb0f2e5fddad69908a017e1afe6c7567615537047f0b6cd6d651ec8fb","model":"gpt-5.5","provider":"openai","segment_id":"pluginsPage.searchClawHubBody","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Enter at least two characters to find code and bundle plugins.","text_hash":"7b88a5efe7893e8013832e739b3569a33f6e61a6e59a3f94389d7205af46702b","tgt_lang":"fr","translated":"Saisissez au moins deux caractères pour trouver du code et des plugins groupés.","updated_at":"2026-07-10T02:24:41.383Z"}
{"cache_key":"3ced3592ea6acebeac1c5b2e06684c1aabd1fdc1deed38906e62320a9fe51bd4","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.agentFilterConfiguredDefault","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"{agent} (default)","text_hash":"7e996234f0fa55605720f9dc954a58411795bd882e948c87c739d43bd02137c3","tgt_lang":"fr","translated":"{agent} (par défaut)","updated_at":"2026-06-17T14:14:32.183Z"}
{"cache_key":"3e54a907ac11ac8ae0b84cdc79b5a9626fdc5f5f985c4202860496f5c9f16f00","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"fr","translated":"Mise à jour disponible","updated_at":"2026-07-11T03:30:29.892Z"}
{"cache_key":"3edc00a38a3d13ced2578a684c2ae62a7bcd6cc73e235c78fb64d7a572b8b8fa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailAutomationSkills","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Skills: {skills}","text_hash":"4788d5f9db66e1421a762bbd942c64450c73d2145a6ef929ce32a919a0f2e3a1","tgt_lang":"fr","translated":"Skills : {skills}","updated_at":"2026-06-16T14:14:26.985Z"}
{"cache_key":"3f2a1680c4c8f175feb2765b734b9dfbeb633b1c333c3ed7d1d00945c8a7dae1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.emptyFilteredTitle","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"No cards match this view","text_hash":"a272617e183ba1dff3f7f140d0851b64baf95f4827ff729d23dfeb05c2069875","tgt_lang":"fr","translated":"Aucune carte ne correspond à cette vue","updated_at":"2026-06-17T14:14:37.466Z"}
{"cache_key":"3f3db4327cd5a4030192268e2c0d0168799248a137618e85896d5f4aaf3cfbaa","model":"gpt-5.5","provider":"openai","segment_id":"newSession.worktreeNameInvalid","source_path":"ui/src/i18n/locales/fr.ts","src_lang":"en","text":"Worktree names use lowercase letters, digits, and dashes.","text_hash":"6a34593b030e5dc2e8e8c55a2be4a5e9a6b415676d4d91e9f521b8586ea3c28f","tgt_lang":"fr","translated":"Les noms de worktree utilisent des lettres minuscules, des chiffres et des tirets.","updated_at":"2026-07-10T15:21:04.486Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:30.989Z",
"generatedAt": "2026-07-11T07:17:58.418Z",
"locale": "hi",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -399,6 +399,7 @@
{"cache_key":"357bbea88a37e9e8301589b223185cf9adddf150b6e49744facaeae8661923f8","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.cronOption","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Cron","text_hash":"dd9d24965dbedc026915308732b77c1af68dcf52d3c0ca2421b1fdb0d197aca1","tgt_lang":"hi","translated":"Cron","updated_at":"2026-06-26T21:37:38.564Z"}
{"cache_key":"3592d3f606bc6c6e93c290934ac9d2b9170c45d4c9d86af46edf99aea5225030","model":"gpt-5.5","provider":"openai","segment_id":"cron.form.timeoutHelp","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Optional. Leave blank to use the gateway default timeout behavior for this run.","text_hash":"f9e62144427ba2922056e13ac5249dfa4690787efa68d2fe18a6e579b7fc9f9c","tgt_lang":"hi","translated":"वैकल्पिक। इस run के लिए gateway के डिफ़ॉल्ट timeout व्यवहार का उपयोग करने हेतु खाली छोड़ें।","updated_at":"2026-06-26T21:37:46.380Z"}
{"cache_key":"35a7e8bdea3cfa05f91d669d69f51c927a9ed61611a413acd223e089cdf89257","model":"gpt-5.5","provider":"openai","segment_id":"usage.filters.model","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Model","text_hash":"5e2c614c23f02239bc03c6c04fcb681950f9e72bf8fdff6be79c79841cbb10c0","tgt_lang":"hi","translated":"Model","updated_at":"2026-06-26T21:34:43.709Z"}
{"cache_key":"35ceb21f5b10ee00190cf825b88282288c31e4d2bb462c04ed5a135bb4e6dc71","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"hi","translated":"अपडेट उपलब्ध है","updated_at":"2026-07-11T03:30:31.046Z"}
{"cache_key":"35d65a9705b9d127a766290ea9e70d1f4ced0048686225f6c08f26e88a5e1f91","model":"gpt-5.5","provider":"openai","segment_id":"cron.errors.nameRequiredShort","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Name required.","text_hash":"08cc53c62fae59721b64dec36d9966533a5f7ded7f93ee0391b21da263158aa1","tgt_lang":"hi","translated":"नाम आवश्यक।","updated_at":"2026-06-26T21:38:17.792Z"}
{"cache_key":"35fd5337467f6d0913473e38a853572fc8372174da0eb4be27f3ad7208a5ed1a","model":"gpt-5.5","provider":"openai","segment_id":"login.togglePasswordVisibility","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Toggle password visibility","text_hash":"1016c07b0f58d365790cc799fb215afd92fde1aeb5ac47cd17260e327465b2d6","tgt_lang":"hi","translated":"पासवर्ड दृश्यता टॉगल करें","updated_at":"2026-06-26T21:35:43.732Z"}
{"cache_key":"360547cc5c4715613c3b449589f2567765e6b493c5fbbfc28697e8a3c235c94f","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.realtimeTalkRequiresMicrophone","source_path":"ui/src/i18n/locales/hi.ts","src_lang":"en","text":"Realtime Talk requires browser microphone access.","text_hash":"e082e85327dc5d2905a34ca4ca60e76836f9b4d7e3b1834b821c81a9c456b39d","tgt_lang":"hi","translated":"Realtime Talk के लिए ब्राउज़र माइक्रोफ़ोन एक्सेस आवश्यक है।","updated_at":"2026-07-06T17:56:41.735Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:31.963Z",
"generatedAt": "2026-07-11T07:17:59.021Z",
"locale": "id",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:31.518Z",
"generatedAt": "2026-07-11T07:17:58.655Z",
"locale": "it",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -300,6 +300,7 @@
{"cache_key":"9f57809ac22c061c9ea221aa958ecf891dbde27a8a28bd5a1bbc8a082aeccdb3","model":"gpt-5.5","provider":"openai","segment_id":"tasksPage.activeSub","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Queued and running background work.","text_hash":"c5d1254fdacab64acf0c8203bf2f51758ec1c65fdf6c161d18c4fac92c4516a4","tgt_lang":"it","translated":"Lavori in background in coda e in esecuzione.","updated_at":"2026-07-09T21:53:25.511Z"}
{"cache_key":"9f8e84cb4fe101205ba11057fa60c524992d9b4273be5d4b3f64573acc71902c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailProof","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Proof","text_hash":"7fbb3ccf9640651f69af3626de6836fb302a0a088c7cd27721c367b8b530e502","tgt_lang":"it","translated":"Prova","updated_at":"2026-06-16T14:15:39.914Z"}
{"cache_key":"a1183ada5b2005e5858f6a6338e29b11c2109e057cf1db7227d7f4ff51eae38e","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.groupRowCountOne","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"{count} session","text_hash":"c0975b42c84d7da963f4c962d1e2c1ee78eb18efc7ebec75fb3ce761ce9a40db","tgt_lang":"it","translated":"{count} sessione","updated_at":"2026-07-05T14:39:59.771Z"}
{"cache_key":"a16270a2c50e50482ec149e0db63738d6e51f2e31c17558c5333b71f90a80858","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"it","translated":"Aggiornamento disponibile","updated_at":"2026-07-11T03:30:33.133Z"}
{"cache_key":"a16622f7711cc2288d9bdf37181a6db1f1be17b7dd5c08c416eb4fdcf8e457ea","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.deletePreservedWorktreeConfirm","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"The session's worktree has uncommitted or unpushed work, so it was kept ({branch}). Delete the checkout anyway?","text_hash":"de74effbbd8fdc00e12e89709ca3363bf7406830f51010302ca4fc70cd9c4ec9","tgt_lang":"it","translated":"Il worktree della sessione contiene lavoro non committato o non inviato, quindi è stato mantenuto ({branch}). Eliminare comunque il checkout?","updated_at":"2026-07-10T17:59:29.738Z"}
{"cache_key":"a18f7e947f8a5befc95d698ceb37e00eac74bdb973a554d19b3d6200845485fc","model":"gpt-5.5","provider":"openai","segment_id":"chat.pullRequests.showMore","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Show {count} more","text_hash":"e372f20a52883cff2eb03f97aba6383ccd70805457f928bba33da9129c405739","tgt_lang":"it","translated":"Mostra altri {count}","updated_at":"2026-07-10T23:12:38.055Z"}
{"cache_key":"a25663846ed52baa56a6aff1672aafbadecfb5750ab4a13193e10b386e30e8c1","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.preview","source_path":"ui/src/i18n/locales/it.ts","src_lang":"en","text":"Preview","text_hash":"324b134f57c70c729ae3dc4d298bb451656717d70523e942c1ce667b8024ea07","tgt_lang":"it","translated":"Anteprima","updated_at":"2026-06-16T14:15:54.477Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:30.577Z",
"generatedAt": "2026-07-11T07:17:58.034Z",
"locale": "ja-JP",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -273,6 +273,7 @@
{"cache_key":"a98d042f52a274515ae8b61ab23a7c03467d924f4e9d9e5399c7033b75ce6f38","model":"gpt-5.5","provider":"openai","segment_id":"browser.reload","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Reload","text_hash":"bdc090ec61e3fcfc65f469951dfe00f3f2ecfc6003c44deac8e05b7237092de6","tgt_lang":"ja-JP","translated":"再読み込み","updated_at":"2026-07-11T02:17:57.654Z"}
{"cache_key":"aa0327fedea0aa93474b9e26175e935b5f72a1cefaba66ab0f355e4260bc2fa3","model":"gpt-5.5","provider":"openai","segment_id":"worktrees.newWorktree","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"New worktree","text_hash":"4f210afedcabde192cf04e3c8c1ff21f79149bc696d1b8abaece56e8e3faa4dd","tgt_lang":"ja-JP","translated":"新しいワークツリー","updated_at":"2026-07-10T17:58:54.036Z"}
{"cache_key":"aa23af2127c19df2575f27252ef021839ac4c97b794f31025a22b1b042e0d3af","model":"gpt-5.5","provider":"openai","segment_id":"chat.runControls.newSessionDisconnected","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Connect to create a new session","text_hash":"8a024c2e0f9dafb9be5a92fb2050bab969c42847a1123de55d465deafb9282e4","tgt_lang":"ja-JP","translated":"新しいセッションを作成するには接続してください","updated_at":"2026-07-10T17:58:59.355Z"}
{"cache_key":"aaa97ec805b0312d991e9371a482f7e3bb9cbafb1dea8f144f2bffbf101139d5","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"ja-JP","translated":"アップデートがあります","updated_at":"2026-07-11T03:30:26.307Z"}
{"cache_key":"ac3b2e583623039a3990a982b7bf0d5a7afc0c7e44521518d466e6e530cde28f","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.healthBlocked","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"blocked","text_hash":"6973dddd3ef9cb6a2932702f31777faad9c9bf3124d147a84f31aadb6d139546","tgt_lang":"ja-JP","translated":"ブロック中","updated_at":"2026-06-17T14:14:09.255Z"}
{"cache_key":"ac9c5e386fd7b44b6220bc38a87ccd0584b5fd8b6a8841157cb25b0bccdc472b","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.renameGroupPrompt","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"Rename group","text_hash":"98d254a311d0e820bb8739eb28d4e1eb5fc1cfc34f755167759864cd15504b4c","tgt_lang":"ja-JP","translated":"グループ名を変更","updated_at":"2026-07-06T23:40:52.793Z"}
{"cache_key":"ad587aac57ecf89dc3440a75192ad536e55e91c91313dfde119fda5624a79f24","model":"gpt-5.5","provider":"openai","segment_id":"tabs.mcp","source_path":"ui/src/i18n/locales/ja-JP.ts","src_lang":"en","text":"MCP","text_hash":"53f13ae99ed53bd346eb8e1c8cefb7ef8260683b50401caf101360967ea052aa","tgt_lang":"ja-JP","translated":"MCP","updated_at":"2026-05-31T05:36:37.677Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:30.709Z",
"generatedAt": "2026-07-11T07:17:58.161Z",
"locale": "ko",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -383,6 +383,7 @@
{"cache_key":"ece1372bff4919019c00f1b63b01c63c11789645bf28c3d334319d82c6980e08","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.changedCount","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"{count} changed","text_hash":"db3cb1c116f0a410592fe8556a43513156ce84faa3b69de7e68635474b2f6a10","tgt_lang":"ko","translated":"{count}개 변경됨","updated_at":"2026-06-16T14:14:34.760Z"}
{"cache_key":"ed80e5e0203238f2e4dbbfb2056616f688ab7bcf15dfb3f928d910444ac69665","model":"gpt-5.5","provider":"openai","segment_id":"newSession.worktreeNamePlaceholder","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"auto","text_hash":"929260ad9b9ea9fe0f3553dd964f4ff3deb5792efd031a2b90f573fe91f012bb","tgt_lang":"ko","translated":"자동","updated_at":"2026-07-10T17:59:02.700Z"}
{"cache_key":"ee6e1c02afd54a9b199ebc559e105bc52e9e79d6ebe8dd463e8660c55a23f8f4","model":"gpt-5.5","provider":"openai","segment_id":"tabs.skillWorkshop","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Skill Workshop","text_hash":"3912c65bdd0a43563438762a43ecbd4b14637844a18decbf9249df73d21152a0","tgt_lang":"ko","translated":"Skill Workshop","updated_at":"2026-05-31T21:48:23.318Z"}
{"cache_key":"ee8c8d7004238c51003e4c4c632b4edb05b4b0050a914c51774dd174618fd9ff","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"ko","translated":"업데이트 가능","updated_at":"2026-07-11T03:30:28.704Z"}
{"cache_key":"ef5eb42974a839541ff4ac3aaefc5500f0b1fd6f75e1abdd98894a0b746ac36e","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependencyStatusMissing","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Missing","text_hash":"6be36ca49ee85210c5d1ad9c377d90a9859c66d889110a2a5b0dccd390d12e20","tgt_lang":"ko","translated":"없음","updated_at":"2026-06-16T14:14:28.718Z"}
{"cache_key":"f0b69589b55c6a48bb8c71e3b830f14f9ac167951a04965dc52c6f93cff37b92","model":"gpt-5.5","provider":"openai","segment_id":"newSession.hint","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Pick where this session works, then say what to do.","text_hash":"fcab2bf24a4a66dfbdf0589e5e1474d3b5cf952720f9f9f619d11f564283e698","tgt_lang":"ko","translated":"이 세션이 작업할 위치를 선택한 다음, 수행할 작업을 입력하세요.","updated_at":"2026-07-10T17:59:02.700Z"}
{"cache_key":"f17dd0291a15c9d7baee831991be5a462cdc646905f512a8ca2a6fb72908ad14","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.contextUsage.estimatedCost","source_path":"ui/src/i18n/locales/ko.ts","src_lang":"en","text":"Est. cost","text_hash":"3199f14286736527bfc6ad9165629415a5aa087517acd2c0e9b1ae8bb5d26766","tgt_lang":"ko","translated":"예상 비용","updated_at":"2026-07-05T16:00:12.544Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:32.680Z",
"generatedAt": "2026-07-11T07:17:59.535Z",
"locale": "nl",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -352,6 +352,7 @@
{"cache_key":"a51bffd1c45763093dc06b890d841b2f0405fc3da6a1e9dffcfbe509619fa843","model":"gpt-5.5","provider":"openai","segment_id":"terminal.detached","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"detached","text_hash":"88e34e4cdbb5c6066cb1b0d0abe74714bd72f86259433bc8287614e4ae052605","tgt_lang":"nl","translated":"losgekoppeld","updated_at":"2026-07-04T21:24:05.296Z"}
{"cache_key":"a6a225e46a8e0e7abe3f12e36e30633ce57783568661c6a5d3df73a245f085c0","model":"gpt-5.5","provider":"openai","segment_id":"pluginsPage.noDiscoverMatchTitle","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Nothing to discover matches","text_hash":"6becb932fc06a9b6bdfe92fb1964a1a20407fc4624d1687d8d53ee8e65c5320e","tgt_lang":"nl","translated":"Geen overeenkomende ontdekkingen","updated_at":"2026-07-10T02:28:48.783Z"}
{"cache_key":"a6bb2fd61138557db9a96fd5968bfe66aef070de5eca5445920c8262b3174ecc","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.contextUsage.open","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Open context usage details","text_hash":"8765adde14aebe600e7c9c69196c5b4a9ccc654802a94626d0391d9eaed725b1","tgt_lang":"nl","translated":"Details over contextgebruik openen","updated_at":"2026-07-05T10:16:33.214Z"}
{"cache_key":"a781827d2f817e516ec8cd7d70534523b5db7f31aecc75ce05835404f40c2710","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"nl","translated":"Update beschikbaar","updated_at":"2026-07-11T03:30:40.263Z"}
{"cache_key":"a787556688e712ff88edec985e1d4b00023c98af5b70391d7d47712324895f5c","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.contextUsageApprox","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"~{percent}% of context used ({used} / {context} tokens, approximate)","text_hash":"54007077673048ad26ff220971d2598fde53a34c33da9e1eaca0927ce80b2708","tgt_lang":"nl","translated":"~{percent}% van context gebruikt ({used} / {context} tokens, bij benadering)","updated_at":"2026-07-09T07:40:50.909Z"}
{"cache_key":"a79fe9d0dfd6822ded48b0aae70e53619f389e449455accf82038a46d5e907fd","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.viewDetails","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"View details","text_hash":"d1bf045bb524dae5b02c471c230958bcd1bf232d7a49367b1cdf977855a06b41","tgt_lang":"nl","translated":"Details bekijken","updated_at":"2026-06-16T14:18:09.152Z"}
{"cache_key":"a7f36aa9f5b99d9564a585058e86886e2d39c84148daaaaf550e1e155fe6e78b","model":"gpt-5.5","provider":"openai","segment_id":"chat.sessionDiff.empty","source_path":"ui/src/i18n/locales/nl.ts","src_lang":"en","text":"No changes in this session's checkout.","text_hash":"0345aa3b1a02eec8e7dbb6f8cf7fbb4c8b335ff6164c87b4ff250f753180d840","tgt_lang":"nl","translated":"Geen wijzigingen in de checkout van deze sessie.","updated_at":"2026-07-11T04:53:40.304Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:32.106Z",
"generatedAt": "2026-07-11T07:17:59.140Z",
"locale": "pl",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:30.155Z",
"generatedAt": "2026-07-11T07:17:57.618Z",
"locale": "pt-BR",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -93,6 +93,7 @@
{"cache_key":"2fd1b0311df8702e5bf88078211dfe1d867e780f15f2584a0363775a27951100","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.dependenciesBlocked","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"{count} blocked","text_hash":"fb39869b0fb3b8933126014e5c3739d7d67a620b8369781ca27e7395c595bde8","tgt_lang":"pt-BR","translated":"{count} bloqueadas","updated_at":"2026-06-16T14:13:17.394Z"}
{"cache_key":"2fdc485fae8361ee09e36a7ea6a471698c58f304b1dc4ada1a2830a5a17512f7","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.groupMenu","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Group options for {group}","text_hash":"29456bfd0f10cfa1b3b0c005e2202221ff66aafe6b72ee05f7cfc93ce9e79af7","tgt_lang":"pt-BR","translated":"Opções do grupo para {group}","updated_at":"2026-07-06T23:40:46.278Z"}
{"cache_key":"307241a63c30b4abea7b7d03ad20fbdeadf0901648426863ea2c252a4c4ab9d5","model":"gpt-5.5","provider":"openai","segment_id":"newSession.baseBranch","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Base branch","text_hash":"9acbb9ebea63701494639b7f2b27206b28628ab7994d45a8f41edf2f8e21efc7","tgt_lang":"pt-BR","translated":"Branch base","updated_at":"2026-07-10T17:58:42.698Z"}
{"cache_key":"318b52e388ac5399bc642a065d384d064f7b098e90c2bfa66b7e275572d3c7d5","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"pt-BR","translated":"Atualização disponível","updated_at":"2026-07-11T03:30:23.054Z"}
{"cache_key":"31e7c6612c04fc2e36b5d973a14def704d56b18af0c46fa96872cbe6d0679c16","model":"gpt-5.5","provider":"openai","segment_id":"worktrees.lastActive","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Last active","text_hash":"bcdf701c4dfbaee3a2162f9b9affd87a23a13426c391f95e964d983851b58a05","tgt_lang":"pt-BR","translated":"Última atividade","updated_at":"2026-07-05T21:00:34.411Z"}
{"cache_key":"322e3115a45abec088171ecf487f91e3f9d146428872caedfcaff50e560e9d1d","model":"gpt-5","provider":"openai","segment_id":"codexSessions.title","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Sessions across your computers","text_hash":"23b30c777a12daeb3e5471cd51530189b621db9fdec4e6ea976e5a97516fac22","tgt_lang":"pt-BR","translated":"Sessões em todos os seus computadores","updated_at":"2026-07-09T10:01:43.718Z"}
{"cache_key":"328b16b2532ada2bf594d3cc589637f1ec809902df1194b6f7644d4a2a78c033","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.session","source_path":"ui/src/i18n/locales/pt-BR.ts","src_lang":"en","text":"Session","text_hash":"6959b4159575d8dd76d9f3bbe2c6437904f861e7860c35abd18deffb1c3425a0","tgt_lang":"pt-BR","translated":"Sessão","updated_at":"2026-06-16T14:13:25.058Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:33.043Z",
"generatedAt": "2026-07-11T07:17:59.792Z",
"locale": "ru",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -1117,6 +1117,7 @@
{"cache_key":"996825e4f37800dde7b3f8600c9e395abbbd29b94791505f2434bae08a33f158","model":"gpt-5.5","provider":"openai","segment_id":"cron.jobs.emptyFilteredHint","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Clear or change filters to see scheduled jobs.","text_hash":"828abf818da2a558d07becfc4f045b7346227dc0538cd2fe386b7b6a1a482868","tgt_lang":"ru","translated":"Очистите или измените фильтры, чтобы увидеть запланированные задания.","updated_at":"2026-06-26T21:42:21.706Z"}
{"cache_key":"996f15db515f9d5d18d0ae2eec8a254004de0affc79e38bb2a8ef31cb7d213bf","model":"gpt-5.5","provider":"openai","segment_id":"cron.quickCreate.schedules.weekly.label","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Weekly","text_hash":"2975132481a7a6957cfa95055d04e706f21f1a613f448d0a17463f2eacca4636","tgt_lang":"ru","translated":"Еженедельно","updated_at":"2026-06-26T21:42:14.995Z"}
{"cache_key":"99a1e7c80d87e2741e6ec9f35969a17b7f95df09fa71fdf552d1b31c0b66ed0a","model":"gpt-5.5","provider":"openai","segment_id":"chat.backgroundTasks.show","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Show background tasks","text_hash":"7cce70ac350d780017a5a7f109b28cbe69f5a1c7bfc92d1c1d9479f402ab8bdf","tgt_lang":"ru","translated":"Показать фоновые задачи","updated_at":"2026-07-11T00:45:46.516Z"}
{"cache_key":"99c72a4b7ee2410c9092732b3eda3912c832d47a6ca234dd1b3f8af61280ac52","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"ru","translated":"Доступно обновление","updated_at":"2026-07-11T03:30:42.905Z"}
{"cache_key":"9a1f2b598f23d70f213c0c93c334be5c2eb81d78a6c1ffdd7895846feeb6c816","model":"gpt-5.5","provider":"openai","segment_id":"dreaming.diary.noDreamsYet","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"No dreams yet","text_hash":"56ee279116c32430a788602b1a13522e463b1ab0db6e6b559e02146342ab9d63","tgt_lang":"ru","translated":"Снов пока нет","updated_at":"2026-06-26T21:40:48.120Z"}
{"cache_key":"9a36dfd7ea6d8ccda1b05859c8d8ffbdfc46d98575e97a01fbfae1219943fa64","model":"gpt-5.5","provider":"openai","segment_id":"chat.queue.retrySend","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Retry send","text_hash":"6bc12f1358f60603697a3ee98f94fb63f271a24007b4c08ec655c805880aa577","tgt_lang":"ru","translated":"Повторить отправку","updated_at":"2026-06-26T21:41:59.944Z"}
{"cache_key":"9a72fb19e3be241371230b3b901b143b5c1af7f51278d03997c124294efc4214","model":"gpt-5.5","provider":"openai","segment_id":"workboard.openEngine","source_path":"ui/src/i18n/locales/ru.ts","src_lang":"en","text":"Open {engine}","text_hash":"511b6c9443f6317fbc6dbe356ca307966772177a3e0225060d38785ac17919e3","tgt_lang":"ru","translated":"Открыть {engine}","updated_at":"2026-06-26T21:39:54.162Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:32.328Z",
"generatedAt": "2026-07-11T07:17:59.281Z",
"locale": "th",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:31.648Z",
"generatedAt": "2026-07-11T07:17:58.777Z",
"locale": "tr",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:31.843Z",
"generatedAt": "2026-07-11T07:17:58.906Z",
"locale": "uk",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -427,6 +427,7 @@
{"cache_key":"f6a4b2bf2ef896fa97bc6db277ed290b1eb2c111e86072e6d13d6ecde36f1b63","model":"gpt-5.5","provider":"openai","segment_id":"sessionsView.restoreSession","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Restore session","text_hash":"35e4dc9befd3b3a29b50938839af0efb41b08051988dde53e1c1e73d1c2a039f","tgt_lang":"uk","translated":"Відновити сеанс","updated_at":"2026-07-02T14:30:30.852Z"}
{"cache_key":"f7334ab311c01cb3cde3c1fe8fe39172798f2fa31fbaa480d42bab06207f8e03","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.talkModel","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Model","text_hash":"5e2c614c23f02239bc03c6c04fcb681950f9e72bf8fdff6be79c79841cbb10c0","tgt_lang":"uk","translated":"Модель","updated_at":"2026-07-06T20:20:02.809Z"}
{"cache_key":"f77965589f28dd18306e5d1fc677c457fd41150ade10325d233a35cf96265328","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.microphoneInput","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Microphone input","text_hash":"5c8a6025b9d96fb0c090d33c9def15ee64aa520a83cf5d64c784b4f0699bb15e","tgt_lang":"uk","translated":"Вхід мікрофона","updated_at":"2026-07-06T17:33:56.873Z"}
{"cache_key":"f7b1337d7ec3f4695b7701714d4b95df2afa7d2923264b06cdfada71af82ea05","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"uk","translated":"Доступне оновлення","updated_at":"2026-07-11T03:30:35.072Z"}
{"cache_key":"f8475bf6c93645afe18727882c8db97dc8a035a0155bf5fef54253d13e4913ec","model":"gpt-5","provider":"openai","segment_id":"codexSessions.refresh","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Refresh","text_hash":"0e91610117029a62a478b7fa7df0b8598bebe3ab1e192d4b1882e310719c9671","tgt_lang":"uk","translated":"Оновити","updated_at":"2026-07-09T10:01:43.750Z"}
{"cache_key":"f88eabaed34ff69c3eace1298979cf7386fc4e3efe80ee9606f648c6739b646e","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.workSessions","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Work","text_hash":"104ab9213e28e4ff5ba0b4086457503e8a4656815cfec7bcf2799d76a3db081d","tgt_lang":"uk","translated":"Робота","updated_at":"2026-07-10T17:59:43.073Z"}
{"cache_key":"f8925fd043b9fe38914dd18cbf98b3987cb9c20274c45a1e7924b78ddd24cbb4","model":"gpt-5.5","provider":"openai","segment_id":"chat.composer.loadingMicrophones","source_path":"ui/src/i18n/locales/uk.ts","src_lang":"en","text":"Loading microphones…","text_hash":"042a481c407f31b97b0cc8ff4e6c8b8f3f6e85d798cba8549c348d6d33a9945c","tgt_lang":"uk","translated":"Завантаження мікрофонів…","updated_at":"2026-07-06T17:33:56.873Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:32.475Z",
"generatedAt": "2026-07-11T07:17:59.411Z",
"locale": "vi",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:29.786Z",
"generatedAt": "2026-07-11T07:17:57.131Z",
"locale": "zh-CN",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1
View File
@@ -347,6 +347,7 @@
{"cache_key":"d8d059a2a919eeb288a8748c5d2bf04ec0e36ca1a6504bcbcc9cfc559ee5385c","model":"claude-opus-4-8","provider":"anthropic","segment_id":"workboard.detailUpdated","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Updated","text_hash":"3a5ecca188c0579c00ee24cf3cab21bd02c15a06f7a70cc8e0a8ff2381dcbbfd","tgt_lang":"zh-CN","translated":"已更新","updated_at":"2026-06-16T14:12:56.348Z"}
{"cache_key":"d8d12564d50b423c2c2bd65f5d3534c9772715425066bffdcacfb682c0d20bd3","model":"gpt-5.5","provider":"openai","segment_id":"usage.providerUsage.today","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Today","text_hash":"2b065c7c9ce466e5ebcad757987d5d660ee4c9ea708bc62c43444b53334738ba","tgt_lang":"zh-CN","translated":"今天","updated_at":"2026-07-05T14:39:29.129Z"}
{"cache_key":"dad56b15cefe6ac6bd3c106fb098c076765e0046060edaba384bea10224f096f","model":"gpt-5.5","provider":"openai","segment_id":"worktrees.owner","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Owner","text_hash":"4b1b8aa3608a26da451ae0630d75b60ab1bc2dd229c41a80838fc7993e835c46","tgt_lang":"zh-CN","translated":"所有者","updated_at":"2026-07-10T17:58:34.409Z"}
{"cache_key":"dad80cc918d521d6eced0931fef9fe558667fe8c56eb4d9a80fefba365839f67","model":"gpt-5.5","provider":"openai","segment_id":"chat.sidebar.updateAvailable","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Update available","text_hash":"ff8b555d818f0b25bbdd1bec8b479660bac7fc276acbe993e759bf67fed32c34","tgt_lang":"zh-CN","translated":"有可用更新","updated_at":"2026-07-11T03:30:20.615Z"}
{"cache_key":"db50d06e695e55462ca1d66965f95e7c55ba5b7b48eae7a3845e4ec283130b2a","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.composer.talkAdvancedSettingsRequiresAdmin","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Advanced settings require admin","text_hash":"021f44198c7c7935a112e55150d2daaebe388ac9e529460d64c2f8f3ba3b9d82","tgt_lang":"zh-CN","translated":"高级设置需要管理员权限","updated_at":"2026-07-06T20:20:02.809Z"}
{"cache_key":"dc924c18bea888d8a915f542c2b759d5a6ff221ca12869798aede279629bedfa","model":"claude-opus-4-8","provider":"anthropic","segment_id":"chat.workspaceFiles.actions","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"Workspace file actions","text_hash":"461817d921bc7672e95fe4a3b23f4ac2a4a20e35b3d6eef3f02e8f5ba4201050","tgt_lang":"zh-CN","translated":"工作区文件操作","updated_at":"2026-06-16T14:13:07.632Z"}
{"cache_key":"dcbcde9aefceb81d5b5baaaaecb3011643afb5b9af2f45fa6dad4bfa817a1bc3","model":"gpt-5","provider":"openai","segment_id":"codexSessions.summary.onlineHosts","source_path":"ui/src/i18n/locales/zh-CN.ts","src_lang":"en","text":"online","text_hash":"f6fc84c9f21c24907d6bee6eec38cabab5fa9a7be8c4a7827fe9e56f245bd2d5","tgt_lang":"zh-CN","translated":"在线","updated_at":"2026-07-09T10:01:43.701Z"}
+4 -4
View File
@@ -1,11 +1,11 @@
{
"fallbackKeys": [],
"generatedAt": "2026-07-11T07:11:30.012Z",
"generatedAt": "2026-07-11T07:17:57.487Z",
"locale": "zh-TW",
"model": "gpt-5.5",
"provider": "openai",
"sourceHash": "347c235f241ad329d8a934851cdc2038b00dfa1cbbdd6fe46d07447910ffa3aa",
"totalKeys": 2108,
"translatedKeys": 2108,
"sourceHash": "d08ffa155bf05c9422dd9c831b05c4a18808a82e339863002a5bb7201d56be81",
"totalKeys": 2106,
"translatedKeys": 2106,
"workflow": 1
}
+1 -3
View File
@@ -1965,10 +1965,7 @@ export const ar: TranslationMap = {
commandPaletteTitle: "ابحث أو انتقل إلى… (⌘K)",
openCommandPalette: "فتح لوحة الأوامر",
docsOpensInNewTab: "{label} (يفتح في علامة تبويب جديدة)",
updateAvailable: "يتوفر تحديث:",
runningVersion: "الإصدار قيد التشغيل v{version}",
updating: "جارٍ التحديث…",
updateNow: "التحديث الآن",
dismissUpdateBanner: "إغلاق لافتة التحديث",
switchedSession: "تم التبديل إلى {session}",
splitView: {
@@ -1980,6 +1977,7 @@ export const ar: TranslationMap = {
dropOpenHere: "افتح هنا",
},
sidebar: {
updateAvailable: "يتوفر تحديث",
allSessions: "كل الجلسات",
chats: "الدردشات",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -2014,10 +2014,7 @@ export const de: TranslationMap = {
commandPaletteTitle: "Suchen oder springen zu… (⌘K)",
openCommandPalette: "Befehlspalette öffnen",
docsOpensInNewTab: "{label} (wird in neuem Tab geöffnet)",
updateAvailable: "Update verfügbar:",
runningVersion: "v{version} wird ausgeführt",
updating: "Wird aktualisiert…",
updateNow: "Jetzt aktualisieren",
dismissUpdateBanner: "Update-Banner ausblenden",
switchedSession: "Zu {session} gewechselt",
splitView: {
@@ -2029,6 +2026,7 @@ export const de: TranslationMap = {
dropOpenHere: "Hier öffnen",
},
sidebar: {
updateAvailable: "Update verfügbar",
allSessions: "Alle Sitzungen",
chats: "Chats",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1975,10 +1975,7 @@ export const en: TranslationMap = {
commandPaletteTitle: "Search or jump to… (⌘K)",
openCommandPalette: "Open command palette",
docsOpensInNewTab: "{label} (opens in new tab)",
updateAvailable: "Update available:",
runningVersion: "running v{version}",
updating: "Updating…",
updateNow: "Update now",
dismissUpdateBanner: "Dismiss update banner",
switchedSession: "Switched to {session}",
splitView: {
@@ -1990,6 +1987,7 @@ export const en: TranslationMap = {
dropOpenHere: "Open here",
},
sidebar: {
updateAvailable: "Update available",
allSessions: "All sessions",
chats: "Chats",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -2002,10 +2002,7 @@ export const es: TranslationMap = {
commandPaletteTitle: "Buscar o ir a… (⌘K)",
openCommandPalette: "Abrir paleta de comandos",
docsOpensInNewTab: "{label} (se abre en una pestaña nueva)",
updateAvailable: "Actualización disponible:",
runningVersion: "ejecutando v{version}",
updating: "Actualizando…",
updateNow: "Actualizar ahora",
dismissUpdateBanner: "Descartar banner de actualización",
switchedSession: "Se cambió a {session}",
splitView: {
@@ -2017,6 +2014,7 @@ export const es: TranslationMap = {
dropOpenHere: "Abrir aquí",
},
sidebar: {
updateAvailable: "Actualización disponible",
allSessions: "Todas las sesiones",
chats: "Chats",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1986,10 +1986,7 @@ export const fa: TranslationMap = {
commandPaletteTitle: "جست‌وجو یا رفتن به… (⌘K)",
openCommandPalette: "باز کردن پالت فرمان",
docsOpensInNewTab: "{label} (در زبانهٔ جدید باز می‌شود)",
updateAvailable: "به‌روزرسانی در دسترس است:",
runningVersion: "در حال اجرای v{version}",
updating: "در حال به‌روزرسانی…",
updateNow: "اکنون به‌روزرسانی کن",
dismissUpdateBanner: "بستن بنر به‌روزرسانی",
switchedSession: "به {session} جابه‌جا شد",
splitView: {
@@ -2001,6 +1998,7 @@ export const fa: TranslationMap = {
dropOpenHere: "اینجا باز شود",
},
sidebar: {
updateAvailable: "به‌روزرسانی در دسترس است",
allSessions: "همهٔ نشست‌ها",
chats: "گفت‌وگوها",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -2021,10 +2021,7 @@ export const fr: TranslationMap = {
commandPaletteTitle: "Rechercher ou accéder à… (⌘K)",
openCommandPalette: "Ouvrir la palette de commandes",
docsOpensInNewTab: "{label} (souvre dans un nouvel onglet)",
updateAvailable: "Mise à jour disponible :",
runningVersion: "version v{version} en cours dexécution",
updating: "Mise à jour…",
updateNow: "Mettre à jour maintenant",
dismissUpdateBanner: "Ignorer la bannière de mise à jour",
switchedSession: "Passage à {session}",
splitView: {
@@ -2036,6 +2033,7 @@ export const fr: TranslationMap = {
dropOpenHere: "Ouvrir ici",
},
sidebar: {
updateAvailable: "Mise à jour disponible",
allSessions: "Toutes les sessions",
chats: "Discussions",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1966,10 +1966,7 @@ export const hi: TranslationMap = {
commandPaletteTitle: "खोजें या यहां जाएं… (⌘K)",
openCommandPalette: "कमांड पैलेट खोलें",
docsOpensInNewTab: "{label} (नए टैब में खुलता है)",
updateAvailable: "अपडेट उपलब्ध है:",
runningVersion: "v{version} चल रहा है",
updating: "अपडेट हो रहा है…",
updateNow: "अभी अपडेट करें",
dismissUpdateBanner: "अपडेट बैनर हटाएँ",
switchedSession: "{session} पर स्विच किया गया",
splitView: {
@@ -1981,6 +1978,7 @@ export const hi: TranslationMap = {
dropOpenHere: "यहाँ खोलें",
},
sidebar: {
updateAvailable: "अपडेट उपलब्ध है",
allSessions: "सभी सत्र",
chats: "चैट",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1991,10 +1991,7 @@ export const id: TranslationMap = {
commandPaletteTitle: "Cari atau lompat ke… (⌘K)",
openCommandPalette: "Buka palet perintah",
docsOpensInNewTab: "{label} (terbuka di tab baru)",
updateAvailable: "Pembaruan tersedia:",
runningVersion: "menjalankan v{version}",
updating: "Memperbarui…",
updateNow: "Perbarui sekarang",
dismissUpdateBanner: "Tutup banner pembaruan",
switchedSession: "Beralih ke {session}",
splitView: {
@@ -2006,6 +2003,7 @@ export const id: TranslationMap = {
dropOpenHere: "Buka di sini",
},
sidebar: {
updateAvailable: "Pembaruan tersedia",
allSessions: "Semua sesi",
chats: "Chat",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -2008,10 +2008,7 @@ export const it: TranslationMap = {
commandPaletteTitle: "Cerca o passa a… (⌘K)",
openCommandPalette: "Apri tavolozza comandi",
docsOpensInNewTab: "{label} (si apre in una nuova scheda)",
updateAvailable: "Aggiornamento disponibile:",
runningVersion: "in esecuzione v{version}",
updating: "Aggiornamento…",
updateNow: "Aggiorna ora",
dismissUpdateBanner: "Ignora banner di aggiornamento",
switchedSession: "Passato a {session}",
splitView: {
@@ -2023,6 +2020,7 @@ export const it: TranslationMap = {
dropOpenHere: "Apri qui",
},
sidebar: {
updateAvailable: "Aggiornamento disponibile",
allSessions: "Tutte le sessioni",
chats: "Chat",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1997,10 +1997,7 @@ export const ja_JP: TranslationMap = {
commandPaletteTitle: "検索または移動… (⌘K)",
openCommandPalette: "コマンドパレットを開く",
docsOpensInNewTab: "{label} (新しいタブで開きます)",
updateAvailable: "アップデートがあります:",
runningVersion: "実行中 v{version}",
updating: "更新中…",
updateNow: "今すぐ更新",
dismissUpdateBanner: "更新バナーを閉じる",
switchedSession: "{session} に切り替えました",
splitView: {
@@ -2012,6 +2009,7 @@ export const ja_JP: TranslationMap = {
dropOpenHere: "ここで開く",
},
sidebar: {
updateAvailable: "アップデートがあります",
allSessions: "すべてのセッション",
chats: "チャット",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1980,10 +1980,7 @@ export const ko: TranslationMap = {
commandPaletteTitle: "검색하거나 이동… (⌘K)",
openCommandPalette: "명령 팔레트 열기",
docsOpensInNewTab: "{label}(새 탭에서 열림)",
updateAvailable: "업데이트 사용 가능:",
runningVersion: "실행 중 v{version}",
updating: "업데이트 중…",
updateNow: "지금 업데이트",
dismissUpdateBanner: "업데이트 배너 닫기",
switchedSession: "{session}(으)로 전환됨",
splitView: {
@@ -1995,6 +1992,7 @@ export const ko: TranslationMap = {
dropOpenHere: "여기에서 열기",
},
sidebar: {
updateAvailable: "업데이트 가능",
allSessions: "모든 세션",
chats: "채팅",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1998,10 +1998,7 @@ export const nl: TranslationMap = {
commandPaletteTitle: "Zoeken of springen naar… (⌘K)",
openCommandPalette: "Opdrachtenpalet openen",
docsOpensInNewTab: "{label} (opent in nieuw tabblad)",
updateAvailable: "Update beschikbaar:",
runningVersion: "actieve versie v{version}",
updating: "Bijwerken…",
updateNow: "Nu bijwerken",
dismissUpdateBanner: "Updatebanner sluiten",
switchedSession: "Overgeschakeld naar {session}",
splitView: {
@@ -2013,6 +2010,7 @@ export const nl: TranslationMap = {
dropOpenHere: "Hier openen",
},
sidebar: {
updateAvailable: "Update beschikbaar",
allSessions: "Alle sessies",
chats: "Chats",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -2000,10 +2000,7 @@ export const pl: TranslationMap = {
commandPaletteTitle: "Wyszukaj lub przejdź do… (⌘K)",
openCommandPalette: "Otwórz paletę poleceń",
docsOpensInNewTab: "{label} (otwiera się w nowej karcie)",
updateAvailable: "Dostępna aktualizacja:",
runningVersion: "uruchomiona wersja v{version}",
updating: "Aktualizowanie…",
updateNow: "Aktualizuj teraz",
dismissUpdateBanner: "Odrzuć baner aktualizacji",
switchedSession: "Przełączono na {session}",
splitView: {
@@ -2015,6 +2012,7 @@ export const pl: TranslationMap = {
dropOpenHere: "Otwórz tutaj",
},
sidebar: {
updateAvailable: "Dostępna aktualizacja",
allSessions: "Wszystkie sesje",
chats: "Czaty",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1992,10 +1992,7 @@ export const pt_BR: TranslationMap = {
commandPaletteTitle: "Pesquisar ou ir para… (⌘K)",
openCommandPalette: "Abrir paleta de comandos",
docsOpensInNewTab: "{label} (abre em nova aba)",
updateAvailable: "Atualização disponível:",
runningVersion: "executando v{version}",
updating: "Atualizando…",
updateNow: "Atualizar agora",
dismissUpdateBanner: "Dispensar banner de atualização",
switchedSession: "Mudou para {session}",
splitView: {
@@ -2007,6 +2004,7 @@ export const pt_BR: TranslationMap = {
dropOpenHere: "Abrir aqui",
},
sidebar: {
updateAvailable: "Atualização disponível",
allSessions: "Todas as sessões",
chats: "Conversas",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -2004,10 +2004,7 @@ export const ru: TranslationMap = {
commandPaletteTitle: "Поиск или переход к… (⌘K)",
openCommandPalette: "Открыть палитру команд",
docsOpensInNewTab: "{label} (откроется в новой вкладке)",
updateAvailable: "Доступно обновление:",
runningVersion: "запущена v{version}",
updating: "Обновление…",
updateNow: "Обновить сейчас",
dismissUpdateBanner: "Скрыть баннер обновления",
switchedSession: "Переключено на {session}",
splitView: {
@@ -2019,6 +2016,7 @@ export const ru: TranslationMap = {
dropOpenHere: "Открыть здесь",
},
sidebar: {
updateAvailable: "Доступно обновление",
allSessions: "Все сеансы",
chats: "Чаты",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1946,10 +1946,7 @@ export const th: TranslationMap = {
commandPaletteTitle: "ค้นหาหรือข้ามไปที่… (⌘K)",
openCommandPalette: "เปิดแถบคำสั่ง",
docsOpensInNewTab: "{label} (เปิดในแท็บใหม่)",
updateAvailable: "มีอัปเดตพร้อมใช้งาน:",
runningVersion: "กำลังใช้ v{version}",
updating: "กำลังอัปเดต…",
updateNow: "อัปเดตตอนนี้",
dismissUpdateBanner: "ปิดแบนเนอร์อัปเดต",
switchedSession: "สลับไปยัง {session} แล้ว",
splitView: {
@@ -1961,6 +1958,7 @@ export const th: TranslationMap = {
dropOpenHere: "เปิดที่นี่",
},
sidebar: {
updateAvailable: "มีการอัปเดตพร้อมใช้งาน",
allSessions: "เซสชันทั้งหมด",
chats: "แชท",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -2002,10 +2002,7 @@ export const tr: TranslationMap = {
commandPaletteTitle: "Ara veya şuraya git… (⌘K)",
openCommandPalette: "Komut paletini aç",
docsOpensInNewTab: "{label} (yeni sekmede açılır)",
updateAvailable: "Güncelleme mevcut:",
runningVersion: "çalışan v{version}",
updating: "Güncelleniyor…",
updateNow: "Şimdi güncelle",
dismissUpdateBanner: "Güncelleme başlığını kapat",
switchedSession: "{session} oturumuna geçildi",
splitView: {
@@ -2017,6 +2014,7 @@ export const tr: TranslationMap = {
dropOpenHere: "Burada aç",
},
sidebar: {
updateAvailable: "Güncelleme mevcut",
allSessions: "Tüm oturumlar",
chats: "Sohbetler",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1990,10 +1990,7 @@ export const uk: TranslationMap = {
commandPaletteTitle: "Пошук або перехід до… (⌘K)",
openCommandPalette: "Відкрити палітру команд",
docsOpensInNewTab: "{label} (відкривається в новій вкладці)",
updateAvailable: "Доступне оновлення:",
runningVersion: "працює v{version}",
updating: "Оновлення…",
updateNow: "Оновити зараз",
dismissUpdateBanner: "Закрити банер оновлення",
switchedSession: "Перемкнуто на {session}",
splitView: {
@@ -2005,6 +2002,7 @@ export const uk: TranslationMap = {
dropOpenHere: "Відкрити тут",
},
sidebar: {
updateAvailable: "Доступне оновлення",
allSessions: "Усі сеанси",
chats: "Чати",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1979,10 +1979,7 @@ export const vi: TranslationMap = {
commandPaletteTitle: "Tìm kiếm hoặc chuyển đến… (⌘K)",
openCommandPalette: "Mở bảng lệnh",
docsOpensInNewTab: "{label} (mở trong tab mới)",
updateAvailable: "Có bản cập nhật:",
runningVersion: "đang chạy v{version}",
updating: "Đang cập nhật…",
updateNow: "Cập nhật ngay",
dismissUpdateBanner: "Bỏ qua banner cập nhật",
switchedSession: "Đã chuyển sang {session}",
splitView: {
@@ -1994,6 +1991,7 @@ export const vi: TranslationMap = {
dropOpenHere: "Mở tại đây",
},
sidebar: {
updateAvailable: "Có bản cập nhật",
allSessions: "Tất cả phiên",
chats: "Cuộc trò chuyện",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1939,10 +1939,7 @@ export const zh_CN: TranslationMap = {
commandPaletteTitle: "搜索或跳转到… (⌘K)",
openCommandPalette: "打开命令面板",
docsOpensInNewTab: "{label}(在新标签页中打开)",
updateAvailable: "有可用更新:",
runningVersion: "正在运行 v{version}",
updating: "正在更新…",
updateNow: "立即更新",
dismissUpdateBanner: "关闭更新横幅",
switchedSession: "已切换到 {session}",
splitView: {
@@ -1954,6 +1951,7 @@ export const zh_CN: TranslationMap = {
dropOpenHere: "在此处打开",
},
sidebar: {
updateAvailable: "有可用更新",
allSessions: "所有会话",
chats: "聊天",
openSessionMenu: "Open session menu",
+1 -3
View File
@@ -1942,10 +1942,7 @@ export const zh_TW: TranslationMap = {
commandPaletteTitle: "搜尋或跳至… (⌘K)",
openCommandPalette: "開啟命令面板",
docsOpensInNewTab: "{label}(在新分頁開啟)",
updateAvailable: "有可用更新:",
runningVersion: "執行中版本 v{version}",
updating: "正在更新…",
updateNow: "立即更新",
dismissUpdateBanner: "關閉更新橫幅",
switchedSession: "已切換至 {session}",
splitView: {
@@ -1957,6 +1954,7 @@ export const zh_TW: TranslationMap = {
dropOpenHere: "在此開啟",
},
sidebar: {
updateAvailable: "有可用的更新",
allSessions: "所有工作階段",
chats: "聊天",
openSessionMenu: "Open session menu",
+114
View File
@@ -805,6 +805,120 @@ html.openclaw-native-macos .shell-nav-expand {
border-top: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
}
.sidebar-update-card {
position: relative;
margin: 0 8px 10px;
}
.sidebar-update-card__action {
width: 100%;
min-height: 62px;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 34px 10px 12px;
border: 1px solid color-mix(in srgb, var(--accent) 18%, var(--border));
border-radius: var(--radius-lg);
background: color-mix(in srgb, var(--accent-subtle) 48%, var(--bg-elevated));
color: var(--text);
cursor: pointer;
text-align: left;
transition:
background var(--duration-fast) ease,
border-color var(--duration-fast) ease;
}
.sidebar-update-card__action:hover:not(:disabled),
.sidebar-update-card__action:focus-visible {
border-color: color-mix(in srgb, var(--accent) 36%, var(--border));
background: color-mix(in srgb, var(--accent-subtle) 72%, var(--bg-elevated));
}
.sidebar-update-card__action:focus-visible,
.sidebar-update-card__dismiss:focus-visible {
outline: 2px solid color-mix(in srgb, var(--accent) 48%, transparent);
outline-offset: 1px;
}
.sidebar-update-card__action:disabled {
cursor: wait;
opacity: 0.66;
}
.sidebar-update-card__icon,
.sidebar-update-card__arrow,
.sidebar-update-card__dismiss {
display: inline-flex;
align-items: center;
justify-content: center;
}
.sidebar-update-card__icon {
width: 28px;
height: 28px;
flex: 0 0 auto;
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--accent) 14%, transparent);
color: var(--accent);
}
.sidebar-update-card__copy {
min-width: 0;
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: 2px;
}
.sidebar-update-card__title {
color: var(--text-strong);
font-size: 13px;
font-weight: 600;
line-height: 1.2;
}
.sidebar-update-card__subtitle {
color: var(--muted);
font-size: 12px;
line-height: 1.2;
}
.sidebar-update-card__arrow {
flex: 0 0 auto;
color: var(--muted);
}
.sidebar-update-card__dismiss {
position: absolute;
top: 4px;
right: 4px;
width: 24px;
height: 24px;
padding: 0;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--muted);
cursor: pointer;
}
.sidebar-update-card__dismiss:hover {
background: var(--bg-hover);
color: var(--text);
}
.sidebar-update-card__icon svg,
.sidebar-update-card__arrow svg,
.sidebar-update-card__dismiss svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 1.7px;
stroke-linecap: round;
stroke-linejoin: round;
}
.sidebar-nav {
/* Nav stays pinned above the session list; the cap keeps sessions reachable
when an expanded More section outgrows a short window (nav then scrolls). */