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,
})}
-
+
@@ -340,7 +340,6 @@ export function ProxmoxDiscoverDialog({
>
{t("hosts.proxmoxSelectAll")}
-
·
setSelected(new Set())}
diff --git a/plugins/proxmox/src/frontend/stats/NodeSummaryStrip.tsx b/plugins/proxmox/src/frontend/stats/NodeSummaryStrip.tsx
index a1d6ff52c..eaa5b73f5 100644
--- a/plugins/proxmox/src/frontend/stats/NodeSummaryStrip.tsx
+++ b/plugins/proxmox/src/frontend/stats/NodeSummaryStrip.tsx
@@ -136,10 +136,10 @@ export function NodeSummaryStrip({
-
- {node?.system.hostname ?? "-"}
- {node?.system.pveVersion ? ` · ${node.system.pveVersion}` : ""}
-
+ {node?.system.hostname ?? "-"}
+ {node?.system.pveVersion && (
+ {node.system.pveVersion}
+ )}
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 (
+
+
+
+ {children}
+
+
+
+ {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 */}
setViewLog(null)}
- className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"
+ aria-label={t("sessionLogs.back")}
+ className={ACTION_BUTTON}
>
@@ -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 ? (
-
- ) : (
-
- )}
-
-
-
- {copied
+
-
+ : t("sessionLogs.copyContent")
+ }
+ onClick={handleCopy}
+ >
+ {copied ? (
+
+ ) : (
+
+ )}
+
)}
{viewLog.format === "asciicast" && (
-
-
- handleDownloadText(viewLog)}
- className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"
- >
-
-
-
-
- {t("sessionLogs.downloadAsText")}
-
-
+
handleDownloadText(viewLog)}
+ >
+
+
)}
-
-
- handleDownload(viewLog)}
- className="size-6 flex items-center justify-center text-muted-foreground/50 hover:text-foreground hover:bg-muted/60 transition-colors"
- >
-
-
-
-
- {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)}
+
setDeleteTarget(null)}
diff --git a/plugins/session-recording/src/frontend/SessionRecordingPlayer.tsx b/plugins/session-recording/src/frontend/SessionRecordingPlayer.tsx
index 899100d0c..51f07666e 100644
--- a/plugins/session-recording/src/frontend/SessionRecordingPlayer.tsx
+++ b/plugins/session-recording/src/frontend/SessionRecordingPlayer.tsx
@@ -4,7 +4,6 @@ import { Pause, Play } from "lucide-react";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import Guacamole from "guacamole-common-js";
-import { Select2 } from "@termix/plugin-sdk/ui";
import "@xterm/xterm/css/xterm.css";
import { parseAsciicast, type Asciicast } from "./asciicast";
import type { SessionLogRecord } from "./session-recording-api";
@@ -20,6 +19,65 @@ function getErrorMessage(error: unknown, fallback: string): string {
return error instanceof Error ? error.message : fallback;
}
+function SeekBar({
+ position,
+ duration,
+ onSeek,
+}: {
+ position: number;
+ duration: number;
+ onSeek: (position: number) => void;
+}) {
+ const { t } = useTranslation();
+ const barRef = useRef(null);
+ const [dragging, setDragging] = useState(false);
+ const progress = duration > 0 ? Math.min(position / duration, 1) : 0;
+
+ const seekFromPointer = (clientX: number) => {
+ const rect = barRef.current?.getBoundingClientRect();
+ if (!rect || rect.width === 0 || duration <= 0) return;
+ const ratio = Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1);
+ onSeek(ratio * duration);
+ };
+
+ return (
+ {
+ event.currentTarget.setPointerCapture(event.pointerId);
+ setDragging(true);
+ seekFromPointer(event.clientX);
+ }}
+ onPointerMove={(event) => {
+ if (dragging) seekFromPointer(event.clientX);
+ }}
+ onPointerUp={(event) => {
+ event.currentTarget.releasePointerCapture(event.pointerId);
+ setDragging(false);
+ }}
+ onKeyDown={(event) => {
+ if (event.key === "ArrowLeft") onSeek(Math.max(position - 5, 0));
+ if (event.key === "ArrowRight")
+ onSeek(Math.min(position + 5, duration));
+ }}
+ >
+
+
+ );
+}
+
function PlaybackControls({
playing,
position,
@@ -39,47 +97,47 @@ function PlaybackControls({
}) {
const { t } = useTranslation();
return (
-
-
- {playing ? (
-
- ) : (
-
- )}
-
-
- {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) => (
-
- {value}×
-
- ))}
-
+
+
+
+
+ {playing ? (
+
+ ) : (
+
+ )}
+
+
+ {formatPosition(Math.min(position, duration))} /{" "}
+ {formatPosition(duration)}
+
+
+ {SPEEDS.map((value) => (
+ onSpeed(value)}
+ aria-pressed={speed === value}
+ className={
+ speed === value
+ ? "h-5 px-1.5 text-[10px] tabular-nums bg-muted text-foreground"
+ : "h-5 px-1.5 text-[10px] tabular-nums text-muted-foreground/60 hover:text-foreground"
+ }
+ >
+ {value}x
+
+ ))}
+
+
);
}
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")}
+
{cred.name}
-
- {cred.username || "-"} · {cred.type}
+
+ {cred.username || "-"}
+ {cred.type}
diff --git a/src/ui/sidebar/ConnectionsPanel.tsx b/src/ui/sidebar/ConnectionsPanel.tsx
index 90cc1bb18..58c8498fa 100644
--- a/src/ui/sidebar/ConnectionsPanel.tsx
+++ b/src/ui/sidebar/ConnectionsPanel.tsx
@@ -189,9 +189,7 @@ function ConnectionRow({
{hostName && hostName !== name ? (
-
- {hostName} ·{" "}
-
+ {hostName}
) : null}
{subLabel}
diff --git a/src/ui/sidebar/HostExportDialog.tsx b/src/ui/sidebar/HostExportDialog.tsx
index 1fc26ca49..21020d345 100644
--- a/src/ui/sidebar/HostExportDialog.tsx
+++ b/src/ui/sidebar/HostExportDialog.tsx
@@ -320,16 +320,23 @@ export function HostExportDialog({
- {count === 0
- ? t("hosts.export.noneSelected")
- : `${t("hosts.export.summary", {
- selected: count,
- total: exportableHosts.length,
- })} · ${
- withCredentials
+ {count === 0 ? (
+ t("hosts.export.noneSelected")
+ ) : (
+
+
+ {t("hosts.export.summary", {
+ selected: count,
+ total: exportableHosts.length,
+ })}
+
+
+ {withCredentials
? t("hosts.export.credentialsIncluded")
- : t("hosts.export.credentialsExcluded")
- }`}
+ : t("hosts.export.credentialsExcluded")}
+
+
+ )}
diff --git a/src/ui/sidebar/credential-tree/CredentialItem/CredentialItem.tsx b/src/ui/sidebar/credential-tree/CredentialItem/CredentialItem.tsx
index 067b9863b..978959e74 100644
--- a/src/ui/sidebar/credential-tree/CredentialItem/CredentialItem.tsx
+++ b/src/ui/sidebar/credential-tree/CredentialItem/CredentialItem.tsx
@@ -335,8 +335,9 @@ export function CredentialItem({
{cred.username}
{usedByCount > 0 && (
-
- {cred.username ? " · " : ""}
+
{usedByCount}h
)}