From 9015d5dccb1e5fb6016d42442f01fc6f987c660d Mon Sep 17 00:00:00 2001 From: LukeGus Date: Thu, 1 Oct 2026 00:09:32 -0500 Subject: [PATCH] fix: redesign session log player controls, fix tooltip overlap and drop dot separators --- .../ai/src/frontend/AiProviderSettings.tsx | 16 +- .../src/frontend/AutomationsPanel.tsx | 30 +- .../src/frontend/HostMaintenance.tsx | 23 +- .../src/frontend/FileManagerSidebar.tsx | 12 +- .../src/frontend/components/AudioPreview.tsx | 5 +- .../frontend/components/CompressDialog.tsx | 2 +- .../components/LocalTransferProgressToast.tsx | 3 +- .../src/frontend/transferMetricsFormat.ts | 2 +- .../fleets/src/frontend/FleetInventoryTab.tsx | 21 +- .../src/frontend/MetricsChartWidget.tsx | 5 +- .../src/frontend/cards/FilesystemPicker.tsx | 12 +- .../src/frontend/cards/GpuCard.tsx | 2 +- .../src/frontend/cards/ProcessesCard.tsx | 2 +- .../cards/managers/HealthCheckCard.tsx | 10 +- .../cards/managers/ProcessInspectorCard.tsx | 2 +- .../cards/managers/WireGuardManagerCard.tsx | 10 +- .../tests/frontend/cards/GpuCard.test.tsx | 4 +- .../proxmox/src/frontend/HostProxmoxTab.tsx | 2 +- .../src/frontend/ProxmoxDiscoverDialog.tsx | 3 +- .../src/frontend/stats/NodeSummaryStrip.tsx | 8 +- plugins/session-recording/locales/en.json | 11 +- .../src/frontend/SessionLogsPanel.tsx | 279 ++++++++++-------- .../src/frontend/SessionRecordingPlayer.tsx | 142 ++++++--- .../src/frontend/ShareSessionModal.tsx | 19 +- plugins/ssh-terminal/locales/en.json | 7 +- .../command-history/CommandAutocomplete.tsx | 8 +- .../tmux-monitor/src/frontend/PanePreview.tsx | 5 +- .../src/frontend/SearchResults.tsx | 5 +- .../tmux-monitor/src/frontend/SessionTree.tsx | 12 +- src/ui/locales/en.json | 4 +- src/ui/sidebar/AdminApiKeysSection.tsx | 12 +- src/ui/sidebar/AdminUserManagePanel.tsx | 5 +- src/ui/sidebar/ConnectionsPanel.tsx | 4 +- src/ui/sidebar/HostExportDialog.tsx | 25 +- .../CredentialItem/CredentialItem.tsx | 5 +- 35 files changed, 434 insertions(+), 283 deletions(-) diff --git a/plugins/ai/src/frontend/AiProviderSettings.tsx b/plugins/ai/src/frontend/AiProviderSettings.tsx index f2db036f6..e89f9fab1 100644 --- a/plugins/ai/src/frontend/AiProviderSettings.tsx +++ b/plugins/ai/src/frontend/AiProviderSettings.tsx @@ -357,11 +357,17 @@ export function AiProviderSettings({
{provider.label}
-
- {provider.providerType} - {provider.defaultModel ? ` · ${provider.defaultModel}` : ""} - {provider.baseUrl ? ` · ${provider.baseUrl}` : ""} - {provider.apiKeyPrefix ? ` · ${provider.apiKeyPrefix}…` : ""} +
+ {provider.providerType} + {provider.defaultModel && ( + {provider.defaultModel} + )} + {provider.baseUrl && ( + {provider.baseUrl} + )} + {provider.apiKeyPrefix && ( + {provider.apiKeyPrefix}… + )}
diff --git a/plugins/automations/src/frontend/AutomationsPanel.tsx b/plugins/automations/src/frontend/AutomationsPanel.tsx index 54283b761..8ac57d886 100644 --- a/plugins/automations/src/frontend/AutomationsPanel.tsx +++ b/plugins/automations/src/frontend/AutomationsPanel.tsx @@ -428,15 +428,19 @@ export function AutomationsPanel({
- - {row.definition - ? t( + + {row.definition && ( + + {t( `${base}.triggerKinds.${row.definition.trigger.kind}`, - ) - : ""} - {row.last_run_status - ? ` · ${timeAgo(row.last_run_at, t)}` - : ""} + )} + + )} + {row.last_run_status && ( + + {timeAgo(row.last_run_at, t)} + + )}
-
- {timeAgo(entry.started_at, t)} - {entry.duration_ms !== null - ? ` · ${Math.round(entry.duration_ms / 100) / 10}s` - : ""} +
+ {timeAgo(entry.started_at, t)} + {entry.duration_ms !== null && ( + {Math.round(entry.duration_ms / 100) / 10}s + )}
{entry.error && (
diff --git a/plugins/automations/src/frontend/HostMaintenance.tsx b/plugins/automations/src/frontend/HostMaintenance.tsx index e790979e5..49f0fdd89 100644 --- a/plugins/automations/src/frontend/HostMaintenance.tsx +++ b/plugins/automations/src/frontend/HostMaintenance.tsx @@ -41,7 +41,7 @@ function VisibleBadge({ return ( {title} @@ -121,9 +121,8 @@ function MaintenanceEditor({ return (
-

- {t("maintenance.title")} · {host.name || host.ip} -

+

{t("maintenance.title")}

+

{host.name || host.ip}

{t("maintenance.description")}

@@ -308,12 +307,16 @@ function MaintenanceEditor({ >

{plan.reason}

-

- {t(`maintenance.${plan.recurrence}`)} ·{" "} - {plan.nextStart - ? `${plan.nextStart.replace("T", " ").slice(0, 16)} UTC` - : t("maintenance.completed")}{" "} - · {t("maintenance.minutes", { count: plan.durationMinutes })} +

+ {t(`maintenance.${plan.recurrence}`)} + + {plan.nextStart + ? `${plan.nextStart.replace("T", " ").slice(0, 16)} UTC` + : t("maintenance.completed")} + + + {t("maintenance.minutes", { count: plan.durationMinutes })} +

{canEdit && ( diff --git a/plugins/file-manager/src/frontend/FileManagerSidebar.tsx b/plugins/file-manager/src/frontend/FileManagerSidebar.tsx index a51095ddc..175486796 100644 --- a/plugins/file-manager/src/frontend/FileManagerSidebar.tsx +++ b/plugins/file-manager/src/frontend/FileManagerSidebar.tsx @@ -144,11 +144,13 @@ function StorageMeter({ {fs.mount} - - {fs.usedHuman && fs.totalHuman - ? `${fs.usedHuman}/${fs.totalHuman}` - : "N/A"} - {fs.percent != null ? ` · ${fs.percent}%` : ""} + + + {fs.usedHuman && fs.totalHuman + ? `${fs.usedHuman}/${fs.totalHuman}` + : "N/A"} + + {fs.percent != null && {fs.percent}%} ))} diff --git a/plugins/file-manager/src/frontend/components/AudioPreview.tsx b/plugins/file-manager/src/frontend/components/AudioPreview.tsx index 28110071b..ac48503ec 100644 --- a/plugins/file-manager/src/frontend/components/AudioPreview.tsx +++ b/plugins/file-manager/src/frontend/components/AudioPreview.tsx @@ -77,8 +77,9 @@ export function AudioPreview({

{file.name.replace(/\.[^/.]+$/, "")}

-

- {ext.toUpperCase()} • {formatFileSize(file.size, t)} +

+ {ext.toUpperCase()} + {formatFileSize(file.size, t)}

diff --git a/plugins/file-manager/src/frontend/components/CompressDialog.tsx b/plugins/file-manager/src/frontend/components/CompressDialog.tsx index 55ecb55c3..5177e814f 100644 --- a/plugins/file-manager/src/frontend/components/CompressDialog.tsx +++ b/plugins/file-manager/src/frontend/components/CompressDialog.tsx @@ -132,7 +132,7 @@ export function CompressDialog({ key={index} className="truncate text-foreground font-medium" > - • {name} + {name} ))} {fileNames.length > 5 && ( diff --git a/plugins/file-manager/src/frontend/components/LocalTransferProgressToast.tsx b/plugins/file-manager/src/frontend/components/LocalTransferProgressToast.tsx index fde2c77e5..9f5284859 100644 --- a/plugins/file-manager/src/frontend/components/LocalTransferProgressToast.tsx +++ b/plugins/file-manager/src/frontend/components/LocalTransferProgressToast.tsx @@ -58,11 +58,10 @@ export function LocalTransferProgressToast({ title={status.currentFileName} > {(status.activeFiles ?? 0) > 1 && ( - + {t("fileManager.localTransferInFlight", { count: status.activeFiles, })} - {" · "} )} {status.currentFileName} diff --git a/plugins/file-manager/src/frontend/transferMetricsFormat.ts b/plugins/file-manager/src/frontend/transferMetricsFormat.ts index d9ff1dba1..047da8d08 100644 --- a/plugins/file-manager/src/frontend/transferMetricsFormat.ts +++ b/plugins/file-manager/src/frontend/transferMetricsFormat.ts @@ -87,6 +87,6 @@ export function createFormatTransferMetrics(t: TFunction) { }), ); } - return parts.join(" · "); + return parts.join(", "); }; } diff --git a/plugins/fleets/src/frontend/FleetInventoryTab.tsx b/plugins/fleets/src/frontend/FleetInventoryTab.tsx index 6155ba0fb..9e4754dab 100644 --- a/plugins/fleets/src/frontend/FleetInventoryTab.tsx +++ b/plugins/fleets/src/frontend/FleetInventoryTab.tsx @@ -256,15 +256,18 @@ export function FleetInventoryTab({ {selectedFleet && ( - - {t("newUi.sidebar.fleets.memberCount", { - count: entries.length, - })}{" "} - ·{" "} - {t("newUi.sidebar.fleets.inventoryCollectedCount", { - count: collectedCount, - total: entries.length, - })} + + + {t("newUi.sidebar.fleets.memberCount", { + count: entries.length, + })} + + + {t("newUi.sidebar.fleets.inventoryCollectedCount", { + count: collectedCount, + total: entries.length, + })} + )} diff --git a/plugins/host-metrics/src/frontend/MetricsChartWidget.tsx b/plugins/host-metrics/src/frontend/MetricsChartWidget.tsx index 815c25dde..91beedfa5 100644 --- a/plugins/host-metrics/src/frontend/MetricsChartWidget.tsx +++ b/plugins/host-metrics/src/frontend/MetricsChartWidget.tsx @@ -178,8 +178,9 @@ function MetricsChartWidget({ {hostName ?? `Host #${hostId}`} - - {label} · {range} + + {label} + {range}
{showCurrentValue && current != null && ( diff --git a/plugins/host-metrics/src/frontend/cards/FilesystemPicker.tsx b/plugins/host-metrics/src/frontend/cards/FilesystemPicker.tsx index 70a2eeead..ccd2b3174 100644 --- a/plugins/host-metrics/src/frontend/cards/FilesystemPicker.tsx +++ b/plugins/host-metrics/src/frontend/cards/FilesystemPicker.tsx @@ -66,11 +66,13 @@ export function FilesystemPicker({ {fs.mount} )} - - {fs.usedHuman && fs.totalHuman - ? `${fs.usedHuman}/${fs.totalHuman}` - : "N/A"} - {fs.percent != null ? ` · ${fs.percent}%` : ""} + + + {fs.usedHuman && fs.totalHuman + ? `${fs.usedHuman}/${fs.totalHuman}` + : "N/A"} + + {fs.percent != null && {fs.percent}%} ))} diff --git a/plugins/host-metrics/src/frontend/cards/GpuCard.tsx b/plugins/host-metrics/src/frontend/cards/GpuCard.tsx index 4044b1091..1531365c2 100644 --- a/plugins/host-metrics/src/frontend/cards/GpuCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/GpuCard.tsx @@ -39,7 +39,7 @@ function processVram(process: GpuProcess): string { process.memoryUsedMiB === null ? NA : `${formatGiB(process.memoryUsedMiB)}G`; - return process.gpuIndex === null ? vram : `${vram} · GPU ${process.gpuIndex}`; + return process.gpuIndex === null ? vram : `${vram} (GPU ${process.gpuIndex})`; } function executableName(path: string): string { diff --git a/plugins/host-metrics/src/frontend/cards/ProcessesCard.tsx b/plugins/host-metrics/src/frontend/cards/ProcessesCard.tsx index 27a91bc5c..2d622cf4e 100644 --- a/plugins/host-metrics/src/frontend/cards/ProcessesCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/ProcessesCard.tsx @@ -42,7 +42,7 @@ export function ProcessesCard({ metrics }: { metrics: ServerMetrics | null }) { ))} diff --git a/plugins/host-metrics/src/frontend/cards/managers/HealthCheckCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/HealthCheckCard.tsx index 3cdaf0a69..450d012ec 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/HealthCheckCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/HealthCheckCard.tsx @@ -288,9 +288,13 @@ export function HealthCheckCard({ hostId }: { hostId: number | null }) { : check.target}
- - {result?.latencyMs != null ? `${result.latencyMs}ms` : "—"} - {upPct != null ? ` · ${upPct}%` : ""} + + + {result?.latencyMs != null + ? `${result.latencyMs}ms` + : "-"} + + {upPct != null && {upPct}%}
{hist.length > 1 && ( diff --git a/plugins/host-metrics/src/frontend/cards/managers/ProcessInspectorCard.tsx b/plugins/host-metrics/src/frontend/cards/managers/ProcessInspectorCard.tsx index 4723ee819..c99f3718a 100644 --- a/plugins/host-metrics/src/frontend/cards/managers/ProcessInspectorCard.tsx +++ b/plugins/host-metrics/src/frontend/cards/managers/ProcessInspectorCard.tsx @@ -160,7 +160,7 @@ export function ProcessInspectorCard({ hostId }: { hostId: number | null }) { {p.mem.toFixed(0)}% {iface.name} - - {iface.up ? "up" : "down"} - {iface.listenPort ? ` · :${iface.listenPort}` : ""} - {iface.peers.length > 0 ? ` · ${iface.peers.length}p` : ""} + + {iface.up ? "up" : "down"} + {iface.listenPort ? :{iface.listenPort} : null} + {iface.peers.length > 0 && ( + {iface.peers.length}p + )} diff --git a/plugins/host-metrics/tests/frontend/cards/GpuCard.test.tsx b/plugins/host-metrics/tests/frontend/cards/GpuCard.test.tsx index a3ef9b82a..40d0b22f4 100644 --- a/plugins/host-metrics/tests/frontend/cards/GpuCard.test.tsx +++ b/plugins/host-metrics/tests/frontend/cards/GpuCard.test.tsx @@ -108,8 +108,8 @@ describe("GpuCard", () => { const python = screen.getByText("python3"); expect(python).toHaveAttribute("title", "/usr/bin/python3"); - expect(screen.getByText("7.7G · GPU 0")).toBeInTheDocument(); - expect(screen.getByText("N/A · GPU 1")).toBeInTheDocument(); + expect(screen.getByText("7.7G (GPU 0)")).toBeInTheDocument(); + expect(screen.getByText("N/A (GPU 1)")).toBeInTheDocument(); }); it("says so when no process is using a GPU", () => { diff --git a/plugins/proxmox/src/frontend/HostProxmoxTab.tsx b/plugins/proxmox/src/frontend/HostProxmoxTab.tsx index 1b20da4c0..de154f55e 100644 --- a/plugins/proxmox/src/frontend/HostProxmoxTab.tsx +++ b/plugins/proxmox/src/frontend/HostProxmoxTab.tsx @@ -65,7 +65,7 @@ export function HostProxmoxTab({ }) : t("hosts.proxmoxLastSyncNoResult"); const lastSyncDescription = cfg.lastSyncAt - ? `${new Date(cfg.lastSyncAt).toLocaleString()} · ${lastSyncSummary}` + ? `${new Date(cfg.lastSyncAt).toLocaleString()}, ${lastSyncSummary}` : t("hosts.proxmoxLastSyncNever"); return ( diff --git a/plugins/proxmox/src/frontend/ProxmoxDiscoverDialog.tsx b/plugins/proxmox/src/frontend/ProxmoxDiscoverDialog.tsx index 5af77d32d..368feb16d 100644 --- a/plugins/proxmox/src/frontend/ProxmoxDiscoverDialog.tsx +++ b/plugins/proxmox/src/frontend/ProxmoxDiscoverDialog.tsx @@ -331,7 +331,7 @@ export function ProxmoxDiscoverDialog({ selected: selected.size, })} -
+
- ·
diff --git a/plugins/session-recording/locales/en.json b/plugins/session-recording/locales/en.json index 015542edd..09e0d60bb 100644 --- a/plugins/session-recording/locales/en.json +++ b/plugins/session-recording/locales/en.json @@ -44,10 +44,17 @@ "copied": "Copied!", "loadError": "Failed to load session logs", "deleteError": "Failed to delete session log", - "filterByHost": "Filter by host..." + "filterByHost": "Filter by host...", + "back": "Back", + "empty": "(empty)", + "noResults": "No results for \"{{query}}\"", + "hostFallback": "Host {{id}}", + "sessionFallback": "Session {{id}}" }, "player": { "timeline": "Recording timeline", - "speed": "Playback speed" + "speed": "Playback speed", + "play": "Play recording", + "pause": "Pause recording" } } diff --git a/plugins/session-recording/src/frontend/SessionLogsPanel.tsx b/plugins/session-recording/src/frontend/SessionLogsPanel.tsx index 44e25afb7..8e3d20f1b 100644 --- a/plugins/session-recording/src/frontend/SessionLogsPanel.tsx +++ b/plugins/session-recording/src/frontend/SessionLogsPanel.tsx @@ -1,4 +1,11 @@ -import { useState, useEffect, useCallback, useMemo, useRef } from "react"; +import { + useState, + useEffect, + useCallback, + useMemo, + useRef, + type ReactNode, +} from "react"; import { useTranslation, useToast, @@ -146,6 +153,63 @@ function SectionHeader({ label, count }: { label: string; count: number }) { ); } +function LogMeta({ + items, + className, +}: { + items: (string | null | undefined)[]; + className?: string; +}) { + return ( + + {items.filter(Boolean).map((item, i) => ( + + {item} + + ))} + + ); +} + +const ACTION_BUTTON = + "size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"; + +function ActionButton({ + label, + onClick, + destructive, + children, +}: { + label: string; + onClick: () => void; + destructive?: boolean; + children: ReactNode; +}) { + return ( + + + + + + {label} + + + ); +} + function LogRow({ log, onView, @@ -160,7 +224,10 @@ function LogRow({ onDelete: () => void; }) { const { t } = useTranslation(); - const hostLabel = log.hostName ?? log.hostIp ?? `Host ${log.hostId}`; + const hostLabel = + log.hostName ?? + log.hostIp ?? + t("sessionLogs.hostFallback", { id: log.hostId }); return (
@@ -172,68 +239,44 @@ function LogRow({ {hostLabel} - - {formatDate(log.startedAt)} - {" · "} - {(log.protocol ?? "ssh").toUpperCase()} - {log.username ? ` · ${log.username}` : ""} - {" · "} - {formatDuration(log.duration)} - {" · "} - {formatBytes(log.sizeBytes)} - +
- +
- - - - - View log - + + + {log.format === "asciicast" && ( - - - - - - {t("sessionLogs.downloadAsText")} - - + + + )} - - - - - Download - - - - - - Delete - + + + + + +
@@ -406,14 +449,18 @@ export function SessionLogsPanel() { // Inline log viewer if (viewLog) { const hostLabel = - viewLog.hostName ?? viewLog.hostIp ?? `Host ${viewLog.hostId}`; + viewLog.hostName ?? + viewLog.hostIp ?? + t("sessionLogs.hostFallback", { id: viewLog.hostId }); return (
{/* Viewer header */}
@@ -421,67 +468,50 @@ export function SessionLogsPanel() { {hostLabel} - - {formatDate(viewLog.startedAt)} - {` · ${viewLog.protocol.toUpperCase()}`} - {viewLog.duration != null - ? ` · ${formatDuration(viewLog.duration)}` - : ""} - {" · "} - {formatBytes(viewLog.sizeBytes)} - +
- +
{viewText != null && ( - - - - - - {copied + - + : t("sessionLogs.copyContent") + } + onClick={handleCopy} + > + {copied ? ( + + ) : ( + + )} + )} {viewLog.format === "asciicast" && ( - - - - - - {t("sessionLogs.downloadAsText")} - - + handleDownloadText(viewLog)} + > + + )} - - - - - - {t("sessionLogs.downloadLog")} - - + handleDownload(viewLog)} + > + +
@@ -496,7 +526,7 @@ export function SessionLogsPanel() { ) : (
-              {viewContent || "(empty)"}
+              {viewContent || t("sessionLogs.empty")}
             
)} @@ -536,7 +566,7 @@ export function SessionLogsPanel() { {filtered.length === 0 ? (
- No results for "{filter}" + {t("sessionLogs.noResults", { query: filter })}
) : ( @@ -568,13 +598,14 @@ export function SessionLogsPanel() {

{t("sessionLogs.confirmDelete")}

-

- {deleteTarget.hostName ?? - deleteTarget.hostIp ?? - `Session ${deleteTarget.id}`} - {" · "} - {formatDate(deleteTarget.startedAt)} -

+
+ + {deleteTarget.hostName ?? + deleteTarget.hostIp ?? + t("sessionLogs.sessionFallback", { id: deleteTarget.id })} + + {formatDate(deleteTarget.startedAt)} +
- - {formatPosition(position)} - - onSeek(Number(event.target.value))} - className="min-w-0 flex-1 accent-primary" - aria-label={t("player.timeline")} - /> - - {formatPosition(duration)} - - onSpeed(Number(event.target.value))} - className="h-7 border border-border bg-background px-1 text-[10px]" - aria-label={t("player.speed")} - > - {SPEEDS.map((value) => ( - - ))} - +
+ +
+ + + {formatPosition(Math.min(position, duration))} /{" "} + {formatPosition(duration)} + +
+ {SPEEDS.map((value) => ( + + ))} +
+
); } diff --git a/plugins/session-sharing/src/frontend/ShareSessionModal.tsx b/plugins/session-sharing/src/frontend/ShareSessionModal.tsx index 2f80e7f8b..08d204b67 100644 --- a/plugins/session-sharing/src/frontend/ShareSessionModal.tsx +++ b/plugins/session-sharing/src/frontend/ShareSessionModal.tsx @@ -416,14 +416,17 @@ export function ShareSessionModal({ "?", })} - - {share.permissionLevel === "read-write" - ? t("sessionSharing.permissionLevel.readWrite") - : t("sessionSharing.permissionLevel.readOnly")} - {" · "} - {t("sessionSharing.expiresAt", { - date: new Date(share.expiresAt).toLocaleString(), - })} + + + {share.permissionLevel === "read-write" + ? t("sessionSharing.permissionLevel.readWrite") + : t("sessionSharing.permissionLevel.readOnly")} + + + {t("sessionSharing.expiresAt", { + date: new Date(share.expiresAt).toLocaleString(), + })} +
diff --git a/plugins/ssh-terminal/locales/en.json b/plugins/ssh-terminal/locales/en.json index b122f9ec5..33f4814a6 100644 --- a/plugins/ssh-terminal/locales/en.json +++ b/plugins/ssh-terminal/locales/en.json @@ -86,7 +86,10 @@ "imageUploaded": "Image uploaded: {{path}}", "imageClipboardUnavailable": "This browser cannot read images from the clipboard", "imageClipboardEmpty": "No image found in the clipboard", - "localShell": "Local terminal shell" + "localShell": "Local terminal shell", + "autocompleteHintComplete": "Tab or Enter to complete", + "autocompleteHintNavigate": "↑↓ to navigate", + "autocompleteHintClose": "Esc to close" }, "terminalToolbar": { "detachTmux": "Detach tmux", @@ -163,7 +166,7 @@ }, "sessionTimeoutMinutes": { "label": "Terminal Session Persistence", - "description": "How long a disconnected terminal is kept alive on the server before it is closed. Min 1 · Max 1440 minutes. For sessions that must outlive this, enable Auto-Tmux on the host." + "description": "How long a disconnected terminal is kept alive on the server before it is closed. Min 1, max 1440 minutes. For sessions that must outlive this, enable Auto-Tmux on the host." }, "commandHistoryEnabled": { "label": "Command History", diff --git a/plugins/ssh-terminal/src/frontend/terminal/command-history/CommandAutocomplete.tsx b/plugins/ssh-terminal/src/frontend/terminal/command-history/CommandAutocomplete.tsx index 3f8578eee..b7b0da935 100644 --- a/plugins/ssh-terminal/src/frontend/terminal/command-history/CommandAutocomplete.tsx +++ b/plugins/ssh-terminal/src/frontend/terminal/command-history/CommandAutocomplete.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useRef } from "react"; import { cn } from "@termix/plugin-sdk/ui"; +import { useTranslation } from "@termix/plugin-sdk/frontend"; interface CommandAutocompleteProps { suggestions: string[]; @@ -53,6 +54,7 @@ export function CommandAutocomplete({ position, visible, }: CommandAutocompleteProps) { + const { t } = useTranslation(); const containerRef = useRef(null); const selectedRef = useRef(null); @@ -103,7 +105,11 @@ export function CommandAutocomplete({ ))}
- Tab/Enter to complete • ↑↓ to navigate • Esc to close + + {t("terminal.autocompleteHintComplete")} + {t("terminal.autocompleteHintNavigate")} + {t("terminal.autocompleteHintClose")} +
); diff --git a/plugins/tmux-monitor/src/frontend/PanePreview.tsx b/plugins/tmux-monitor/src/frontend/PanePreview.tsx index 189b02adc..e44c44753 100644 --- a/plugins/tmux-monitor/src/frontend/PanePreview.tsx +++ b/plugins/tmux-monitor/src/frontend/PanePreview.tsx @@ -79,8 +79,9 @@ export function PanePreview({ return ( <>
- - {pane.sessionName} · {pane.paneId} + + {pane.sessionName} + {pane.paneId} {metrics && ( <> diff --git a/plugins/tmux-monitor/src/frontend/SearchResults.tsx b/plugins/tmux-monitor/src/frontend/SearchResults.tsx index dd30e100b..73ea2aa83 100644 --- a/plugins/tmux-monitor/src/frontend/SearchResults.tsx +++ b/plugins/tmux-monitor/src/frontend/SearchResults.tsx @@ -72,8 +72,9 @@ export function SearchResults({ className="flex cursor-pointer items-baseline gap-2 px-3 py-1 text-xs hover:bg-muted/40" onClick={() => onSelect(match)} > - - {match.sessionName} · {match.paneId} + + {match.sessionName} + {match.paneId} {highlightMatch(match.text, query)} diff --git a/plugins/tmux-monitor/src/frontend/SessionTree.tsx b/plugins/tmux-monitor/src/frontend/SessionTree.tsx index 5ebb69da4..6a8148e2a 100644 --- a/plugins/tmux-monitor/src/frontend/SessionTree.tsx +++ b/plugins/tmux-monitor/src/frontend/SessionTree.tsx @@ -1,6 +1,6 @@ // Session/window/pane tree styled after the VSCode "tmux manager" view. // Each session is a single row: status dot, name, inline tag badges, and a -// right-aligned meta label (relative time · cpu · mem) that is overlaid by +// right-aligned meta label (relative time, cpu, mem) that is overlaid by // the hover actions (attach, new window, …-menu). The overlay uses a grid // stack with opacity so rows never shift and the actions stay reachable via // keyboard focus. @@ -119,11 +119,17 @@ export function SessionTree({ }); } - function metaLabel(session: TmuxSessionOverview): string { + function metaLabel(session: TmuxSessionOverview) { const time = formatRelativeTime(session.lastActivity, now, t); const agg = metricsBySession.get(session.name); if (!agg) return time; - return `${time} · ${agg.cpu.toFixed(0)}% · ${formatMem(agg.memKb)}`; + return ( + + {time} + {agg.cpu.toFixed(0)}% + {formatMem(agg.memKb)} + + ); } return ( diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index a3f6e6418..641d5e198 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -1184,7 +1184,7 @@ "add": "Add:", "commandPalette": "Command Palette", "done": "Done", - "editModeInstructions": "Drag cards to reorder · Drag the column divider to resize columns · Drag the bottom edge of a card to resize its height · Trash to remove", + "editModeInstructions": "Drag cards to reorder. Drag the column divider to resize columns. Drag the bottom edge of a card to change its height. Use the trash button to remove a card.", "empty": "Empty", "clear": "Clear", "viewServerDetails": "View server details", @@ -1264,7 +1264,7 @@ "auditForwardingSaveError": "Failed to save audit forwarding settings", "sessionTimeout": "Session Timeout", "hours": "hours", - "sessionTimeoutRange": "Min 1h · Max 720h", + "sessionTimeoutRange": "Min 1h, max 720h", "monitoringDefaults": "Monitoring Defaults", "statusCheck": "Status Check", "sec": "sec", diff --git a/src/ui/sidebar/AdminApiKeysSection.tsx b/src/ui/sidebar/AdminApiKeysSection.tsx index 5c52fbbf0..f94cf6ecf 100644 --- a/src/ui/sidebar/AdminApiKeysSection.tsx +++ b/src/ui/sidebar/AdminApiKeysSection.tsx @@ -230,11 +230,13 @@ export function AdminApiKeysSection({ {key.tokenPrefix}… - - {key.createdAt.split("T")[0]} ·{" "} - {key.expiresAt - ? key.expiresAt.split("T")[0] - : t("admin.apiKeyNoExpiry")} + + {key.createdAt.split("T")[0]} + + {key.expiresAt + ? key.expiresAt.split("T")[0] + : t("admin.apiKeyNoExpiry")} +