Files
Termix/src/ui/shell/CommandPalette.tsx
T
Luke Gustafsondependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>sashLukeGusDivByZerodevdanetraAleksandr FominykhZacharyZcR
9de904dd4c release-2.5.0 (#994)
* feat(sshid) - sshid.io equivalent for termix (#919)

* feat(ssh-id): database schema, migrations and field encryption

Adds ssh_identities, ssh_identity_keys and ssh_identity_ca tables (public keys
stored plaintext for the unauthenticated resolver; CA private key registered
for per-user field encryption), with UNIQUE(user_id), an index on
ssh_identity_keys(identity_id), and idempotent CREATE TABLE migrations.

* feat(ssh-id): backend API — resolver, key management, CA and certificates

Mounts /sshid (nginx route added). Public text/plain authorized_keys resolver
(+ exact /:algo filter, HTML viewer) and CA public-key endpoint; no-store +
noindex headers on every resolver response including early 404s. Authenticated
management: claim/rename/delete handle, add/import/generate/enable/delete keys,
and a per-user CA (create/rotate/delete) with pure-Node OpenSSH certificate
issuance. Audit logging on all mutations; UNIQUE races map to a precise 409.
Unit tests for key parsing and certificate signing (ssh-keygen-validated).

* feat(ssh-id): frontend panel, API client and i18n

SSH ID panel wired into the app rail and AppShell: claim handle, resolver URL +
curl one-liner, key list, generate, paste/import, CA enable/rotate/remove with
server trust command, and per-key certificate issuance. API client re-exported
through main-axios.ts; all strings i18n'd.

* style(ssh-id): align panel and resolver page with Termix theme

- Rebuild the SSH ID sidebar panel with the theme's square components
  (SectionCard / SettingRow / FakeSwitch) instead of rounded ad-hoc cards;
  use accent-brand and destructive tokens rather than raw red/green.
- Fix panel scrolling: move overflow to a block scroll container so the
  cards keep their natural height instead of being clipped.
- Restyle the public resolver HTML page (/sshid/u/:handle) to the Termix
  dark theme: square corners, #18181b/#303032 palette, #f59145 accent,
  uppercase section labels.
- Tidy copy: 'Save To Credentials' label, drop the redundant generate intro,
  and correct the generate tooltip (the key is stored when saving to vault).

* feat: rename to Termix ID, improve UI, backend inconsistencies, and general bug fixes

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* ci(deps): bump actions/checkout from 6 to 7 in the github-actions group (#922)

Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump the dev-patch-updates group with 11 updates (#923)

Bumps the dev-patch-updates group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@codemirror/search](https://github.com/codemirror/search) | `6.7.0` | `6.7.1` |
| [@codemirror/view](https://github.com/codemirror/view) | `6.43.0` | `6.43.1` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.0` | `4.3.1` |
| [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `4.1.8` | `4.1.9` |
| [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) | `4.1.8` | `4.1.9` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.2` | `0.5.3` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `17.0.7` | `17.0.8` |
| [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.8.4` |
| [sharp](https://github.com/lovell/sharp) | `0.35.1` | `0.35.2` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.0` | `4.3.1` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` |


Updates `@codemirror/search` from 6.7.0 to 6.7.1
- [Changelog](https://github.com/codemirror/search/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/search/commits)

Updates `@codemirror/view` from 6.43.0 to 6.43.1
- [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/view/commits)

Updates `@tailwindcss/vite` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-vite)

Updates `@vitest/coverage-v8` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/coverage-v8)

Updates `@vitest/ui` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/ui)

Updates `eslint-plugin-react-refresh` from 0.5.2 to 0.5.3
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.2...v0.5.3)

Updates `lint-staged` from 17.0.7 to 17.0.8
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v17.0.7...v17.0.8)

Updates `prettier` from 3.8.3 to 3.8.4
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.8.4)

Updates `sharp` from 0.35.1 to 0.35.2
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.1...v0.35.2)

Updates `tailwindcss` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/vitest)

---
updated-dependencies:
- dependency-name: "@codemirror/search"
  dependency-version: 6.7.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@codemirror/view"
  dependency-version: 6.43.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitest/ui"
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: lint-staged
  dependency-version: 17.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: prettier
  dependency-version: 3.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: sharp
  dependency-version: 0.35.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tailwindcss
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump nanoid in the prod-patch-updates group (#925)

Bumps the prod-patch-updates group with 1 update: [nanoid](https://github.com/ai/nanoid).


Updates `nanoid` from 5.1.11 to 5.1.15
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/5.1.11...5.1.15)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-version: 5.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump the major-updates group with 5 updates (#926)

Bumps the major-updates group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.2.0` | `5.0.0` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.4` | `10.0.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.2` | `26.0.0` |
| [concurrently](https://github.com/open-cli-tools/concurrently) | `9.2.1` | `10.0.3` |
| [eslint](https://github.com/eslint/eslint) | `9.39.4` | `10.5.0` |


Updates `js-yaml` from 4.2.0 to 5.0.0
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...5.0.0)

Updates `@eslint/js` from 9.39.4 to 10.0.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

Updates `@types/node` from 25.9.2 to 26.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `concurrently` from 9.2.1 to 10.0.3
- [Release notes](https://github.com/open-cli-tools/concurrently/releases)
- [Commits](https://github.com/open-cli-tools/concurrently/compare/v9.2.1...v10.0.3)

Updates `eslint` from 9.39.4 to 10.5.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.4...v10.5.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: concurrently
  dependency-version: 10.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: eslint
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(ssh): add HashiCorp Vault SSH signer authentication

* fix: small fixes to vault feature to align with Termix codebase

* chore: add view docs links for vault/termix id

* fix: file upload fails with 400 and missing schema migrations on upgrade (#929)

Two bugs introduced in v2.4.1:

1. uploadFileStream uses fileManagerApi.post() which triggers axios's
   transformRequest to JSON-serialize the FormData because the instance
   default Content-Type is application/json. Change to postForm() which
   sets Content-Type: multipart/form-data so the browser XHR sends the
   correct multipart body with boundary.

2. Two schema items added to schema.ts were not included in migrateSchema()
   in db/index.ts, causing 500 errors on existing installations upgrading
   from v2.4.0:
   - user_preferences.status_color_scheme (no such column)
   - dashboard_service_links table (no such table)

Fixes #928

Co-authored-by: sash <sash@fominykh.io>

* fix: support PuTTY PPK ssh keys (#930)

* fix: chunk large file manager uploads (#932)

* fix: route dashboard hosts by protocol (#934)

* fix: resolve tunnel endpoints reliably (#935)

* Fix Electron OIDC browser auth failures (#936)

* Allow RDP connections without stored credentials (#937)

* Sync role credential shares for OIDC users (#938)

* Fix terminal link dialog layering (#940)

* Confirm large files before opening editor (#942)

* Confirm closing active host connections (#943)

* Preserve file path case in file manager UI (#941)

* fix: preserve unicode guacamole tokens (#933)

* Persist VNC authentication settings (#944)

* Fix Guacamole websocket base path (#946)

* Promote file manager terminals to tabs (#939)

* Guard Guacamole disconnect during startup (#945)

* chore: increment ver

* feat: bitwarden ssh agent integration

* feat: serial connections support

* fix: various small bug fixes

* feat: open all sessions in a folder and terminal custom theme color support

* feat: cross host file manager clipboard and several small bug fixes

* feat: tailscale/wireguard support and added a new status state for when backend is checking status

* feat: grafana like server stats history, new alert system, ntfy/webhook support

* feat: new grid and widget based homepage function

* feat: new donate button in dashboard

* fix: alert ui incorrectly using termix css and fixed issue with alert system not loading

* chore: fix ci checks (#966)

* Fix dashboard service link creation (#950)

* Fix jump host SOCKS5 proxy selection (#951)

* Fix jump host SOCKS5 proxy selection

* fix: type jump host socks proxy config

* Fix tmux detection path handling (#949)

* Support GUACD_URL environment config (#952)

* Retry autostart tunnel host fetches (#953)

* Retry autostart tunnel host fetches

* chore: format tunnel route

* Fix PUID html ownership in Docker entrypoint (#954)

* feat: allow custom tunnel endpoints (#977)

* fix: skip metrics start for non-ssh hosts (#976)

* Fix Proxmox import auth fallback (#956)

* Fix Proxmox import auth fallback

* chore: format proxmox import auth

* Fix SSH heading syntax highlighting (#955)

* Fix SSH heading syntax highlighting

* chore: format terminal highlighter

* Initialize auth before fullscreen terminal routes (#957)

* Add WebAuthn passkey authentication (#959)

* chore: add Biome tooling (#965)

* chore: add biome tooling

* chore: support tailwind syntax in biome

* Fix VNC required argument handshake (#968)

* Prioritize host results in command palette search (#969)

* Prevent sidebar host hover layout shift (#970)

* Add terminal font zoom with mouse wheel (#971)

* Add host temperature metrics card (#972)

* Make file downloads reliable in desktop app (#973)

* Add app rail hover expansion setting (#974)

* fix: use correct translation key for nav.close (#964)

* fix(tunnel): skip endpoint credential validation for direct tunnels (#963)

* fix: SSH port connection bug (#975)

* fix: chunked upload for files >=1.5GB to bypass browser ArrayBuffer limit (#948)

* feat: support SSH agent auth across SSH features (#960)

* feat: add Podman container runtime support (#958)

* chore: update readme and release notes

* fix: stabilize Windows app icon (#978)

* Add safe host sharing export (#979)

* Add external editor support for file manager (#985)

* Rework SSH credential password handling (#984)

* Support password fallback for SSH key credentials

* Complete SSH credential password fallback

* Fix runtime base path for auth callbacks (#982)

* Fix TUI terminal output highlighting (#983)

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* Add app fullscreen mode (#993)

* Fix host metrics startup polling (#986)

* chore: update release notes

* fix: line chart text overlap

* chore: update RELEASE_NOTES.md

* chore: add donation goal to readme

* chore: update readme

* fix: hide full-screen button in electron app

* fix: failed unit tests

* chore: lint, format, and bump version to 2.5.0

* chore: remove timeout from crowdin translate

* chore: sync Crowdin translations for 2.5.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: DivByZero <mr.oplus@yahoo.fr>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: devdanetra <46488477+devdanetra@users.noreply.github.com>
Co-authored-by: Aleksandr Fominykh <neoformalex@users.noreply.github.com>
Co-authored-by: sash <sash@fominykh.io>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
2026-06-29 13:28:26 -05:00

579 lines
24 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import { Kbd } from "@/components/kbd";
import {
Command,
CommandItem,
CommandList,
CommandGroup,
CommandSeparator,
} from "@/components/command";
import {
Server,
Settings,
Terminal,
FolderOpen,
FolderSearch,
Box,
Globe,
Plus,
MessagesSquare,
LifeBuoy,
Search,
Activity,
Network,
User,
KeyRound,
Layers, // --- tmux-monitor ---
Monitor,
MousePointerClick,
Clock,
Folder,
Pencil,
} from "lucide-react";
import { getRecentActivity, type RecentActivityItem } from "@/main-axios";
import type { Host, TabType } from "@/types/ui-types";
interface CommandPaletteProps {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
hosts: Host[];
onOpenTab: (type: TabType, label?: string, pendingEvent?: string) => void;
}
const ACTIVITY_ICONS: Record<string, React.ReactNode> = {
terminal: <Terminal className="size-3.5" />,
file_manager: <FolderOpen className="size-3.5" />,
server_stats: <Activity className="size-3.5" />,
tunnel: <Network className="size-3.5" />,
docker: <Box className="size-3.5" />,
telnet: <MessagesSquare className="size-3.5" />,
vnc: <MousePointerClick className="size-3.5" />,
rdp: <Monitor className="size-3.5" />,
};
const ACTIVITY_TAB_TYPE: Record<string, TabType> = {
terminal: "terminal",
file_manager: "files",
server_stats: "host-metrics",
tunnel: "tunnel",
docker: "docker",
telnet: "telnet",
vnc: "vnc",
rdp: "rdp",
};
function getSshActions(host: Host): {
type: TabType;
icon: React.ElementType;
label: string;
}[] {
const metricsEnabled = host.statsConfig?.metricsEnabled !== false;
return [
host.enableTerminal !== false && {
type: "terminal",
icon: Terminal,
label: "Terminal",
},
host.enableFileManager && {
type: "files",
icon: FolderSearch,
label: "Files",
},
host.enableDocker && { type: "docker", icon: Box, label: "Docker" },
// --- tmux-monitor --- opt-in per host, off by default
host.enableTerminal !== false &&
host.enableTmuxMonitor && {
type: "tmux_monitor",
icon: Layers,
label: "Tmux Monitor",
},
host.enableTunnel && { type: "tunnel", icon: Network, label: "Tunnels" },
metricsEnabled && {
type: "host-metrics",
icon: Activity,
label: "Host Metrics",
},
].filter(Boolean) as {
type: TabType;
icon: React.ElementType;
label: string;
}[];
}
export function CommandPalette({
isOpen,
setIsOpen,
hosts,
onOpenTab,
}: CommandPaletteProps) {
const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null);
const [search, setSearch] = useState("");
const [recentActivity, setRecentActivity] = useState<RecentActivityItem[]>(
[],
);
useEffect(() => {
if (isOpen) {
setTimeout(() => inputRef.current?.focus(), 50);
setSearch("");
getRecentActivity(5)
.then(setRecentActivity)
.catch(() => {});
}
}, [isOpen]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setIsOpen(false);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [setIsOpen]);
const filteredHosts = hosts.filter(
(h) =>
h.name.toLowerCase().includes(search.toLowerCase()) ||
h.ip.toLowerCase().includes(search.toLowerCase()) ||
h.username.toLowerCase().includes(search.toLowerCase()),
);
// Group hosts by folder; ungrouped hosts appear first under an implicit root group
const groupedHosts: { folder: string | null; hosts: Host[] }[] = [];
const folderMap = new Map<string, Host[]>();
const ungrouped: Host[] = [];
for (const h of filteredHosts) {
if (h.folder) {
if (!folderMap.has(h.folder)) folderMap.set(h.folder, []);
folderMap.get(h.folder)!.push(h);
} else {
ungrouped.push(h);
}
}
if (ungrouped.length > 0)
groupedHosts.push({ folder: null, hosts: ungrouped });
for (const [folder, fhosts] of folderMap) {
groupedHosts.push({ folder, hosts: fhosts });
}
const handleAction = (action: () => void) => {
action();
setIsOpen(false);
};
const showHostResultsFirst = search.trim().length > 0;
if (!isOpen) return null;
return (
<div
className={cn(
"fixed inset-0 z-[100] flex items-start justify-center pt-[15vh] bg-background/40 backdrop-blur-sm transition-all duration-200 animate-in fade-in",
)}
onClick={() => setIsOpen(false)}
>
<div
className={cn(
"w-full max-w-2xl mx-4 overflow-hidden rounded-none border border-border bg-card shadow-2xl animate-in zoom-in-95 duration-200",
)}
onClick={(e) => e.stopPropagation()}
>
<Command className="rounded-none">
<div className="flex items-center border-b border-border px-4 py-1">
<Search className="size-4 text-muted-foreground mr-3" />
<input
ref={inputRef}
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t("commandPalette.searchPlaceholder")}
className="flex-1 h-12 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
<div className="flex items-center gap-1.5 ml-2">
<Kbd className="bg-muted/50 border-none h-6 px-2 text-[11px] rounded-none">
ESC
</Kbd>
</div>
</div>
<CommandList className="max-h-[60vh] thin-scrollbar flex flex-col">
<CommandGroup
heading={t("commandPalette.quickActions")}
className="px-2"
>
<CommandItem
onSelect={() =>
handleAction(() =>
onOpenTab(
"host-manager",
undefined,
"host-manager:add-host",
),
)
}
className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<div className="size-8 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors">
<Plus className="size-4 text-accent-brand" />
</div>
<div className="flex flex-col flex-1">
<span className="text-sm font-semibold">
{t("commandPalette.addNewHost")}
</span>
<span className="text-xs text-muted-foreground">
{t("commandPalette.addNewHostDesc")}
</span>
</div>
</CommandItem>
<CommandItem
onSelect={() => handleAction(() => onOpenTab("admin-settings"))}
className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<div className="size-8 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors">
<Settings className="size-4 text-accent-brand" />
</div>
<div className="flex flex-col flex-1">
<span className="text-sm font-semibold">
{t("commandPalette.adminSettings")}
</span>
<span className="text-xs text-muted-foreground">
{t("commandPalette.adminSettingsDesc")}
</span>
</div>
</CommandItem>
<CommandItem
onSelect={() => handleAction(() => onOpenTab("user-profile"))}
className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<div className="size-8 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors">
<User className="size-4 text-accent-brand" />
</div>
<div className="flex flex-col flex-1">
<span className="text-sm font-semibold">
{t("commandPalette.userProfile")}
</span>
<span className="text-xs text-muted-foreground">
{t("commandPalette.userProfileDesc")}
</span>
</div>
</CommandItem>
{/* --- tmux-monitor --- */}
<CommandItem
onSelect={() => handleAction(() => onOpenTab("tmux_monitor"))}
className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<div className="size-8 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors">
<Layers className="size-4 text-accent-brand" />
</div>
<div className="flex flex-col flex-1">
<span className="text-sm font-semibold">
{t("commandPalette.tmuxMonitor")}
</span>
<span className="text-xs text-muted-foreground">
{t("commandPalette.tmuxMonitorDesc")}
</span>
</div>
</CommandItem>
<CommandItem
onSelect={() =>
handleAction(() =>
onOpenTab(
"host-manager",
undefined,
"host-manager:add-credential",
),
)
}
className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<div className="size-8 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors">
<KeyRound className="size-4 text-accent-brand" />
</div>
<div className="flex flex-col flex-1">
<span className="text-sm font-semibold">
{t("commandPalette.addCredential")}
</span>
<span className="text-xs text-muted-foreground">
{t("commandPalette.addCredentialDesc")}
</span>
</div>
</CommandItem>
</CommandGroup>
{recentActivity.length > 0 && (
<>
<CommandSeparator className="my-2" />
<CommandGroup
heading={t("commandPalette.recentActivity")}
className="px-2"
>
{recentActivity.map((item) => (
<CommandItem
key={item.id}
onSelect={() =>
handleAction(() =>
onOpenTab(
ACTIVITY_TAB_TYPE[item.type],
item.hostName,
),
)
}
className="group flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<div className="size-7 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors text-muted-foreground group-hover:text-accent-brand">
{ACTIVITY_ICONS[item.type]}
</div>
<div className="flex flex-col flex-1 min-w-0">
<span className="text-sm font-semibold truncate">
{item.hostName}
</span>
<span className="text-xs text-muted-foreground capitalize">
{item.type.replace("_", " ")}
</span>
</div>
<div className="flex items-center gap-1 text-muted-foreground/50">
<Clock className="size-3" />
<span className="text-[10px]">
{new Date(item.timestamp).toLocaleDateString()}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</>
)}
<div className={cn(showHostResultsFirst && "order-first")}>
{!showHostResultsFirst && <CommandSeparator className="my-2" />}
<CommandGroup
heading={t("commandPalette.serversAndHosts")}
className="px-2"
>
{filteredHosts.length > 0 ? (
groupedHosts.map(({ folder, hosts: groupHosts }) => (
<div key={folder ?? "__root__"}>
{folder && (
<div className="flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-semibold text-muted-foreground/60 uppercase tracking-wide">
<Folder className="size-3" />
{folder}
</div>
)}
{groupHosts.map((host, i) => (
<CommandItem
key={i}
onSelect={() =>
handleAction(() => {
const type = host.enableSsh
? "terminal"
: host.enableRdp
? "rdp"
: host.enableVnc
? "vnc"
: host.enableTelnet
? "telnet"
: "terminal";
onOpenTab(type, host.name);
})
}
className="group flex items-center gap-3 px-3 py-2.5 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<div className="size-8 rounded-none bg-muted flex items-center justify-center group-hover:bg-accent-brand/20 transition-colors shrink-0">
<Server
className={cn(
"size-4",
host.online
? "text-accent-brand"
: "text-muted-foreground",
)}
/>
</div>
<div className="flex flex-col flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold truncate">
{host.name}
</span>
{host.online && (
<span className="size-1.5 rounded-full bg-accent-brand animate-pulse shrink-0" />
)}
</div>
<span className="text-xs text-muted-foreground font-mono">
{host.username}@{host.ip}
</span>
</div>
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
{host.enableSsh &&
getSshActions(host).map(
({ type, icon: Icon, label }) => (
<button
key={type}
title={label}
onClick={(e) => {
e.stopPropagation();
handleAction(() =>
onOpenTab(type, host.name),
);
}}
className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors"
>
<Icon className="size-3.5" />
</button>
),
)}
{host.enableSsh &&
(host.enableRdp ||
host.enableVnc ||
host.enableTelnet) && (
<div className="w-px h-3.5 bg-border/60 mx-0.5 shrink-0" />
)}
{host.enableRdp && (
<button
title="RDP"
onClick={(e) => {
e.stopPropagation();
handleAction(() =>
onOpenTab("rdp", host.name),
);
}}
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
>
<Monitor className="size-3" />
RDP
</button>
)}
{host.enableVnc && (
<button
title="VNC"
onClick={(e) => {
e.stopPropagation();
handleAction(() =>
onOpenTab("vnc", host.name),
);
}}
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
>
<MousePointerClick className="size-3" />
VNC
</button>
)}
{host.enableTelnet && (
<button
title="Telnet"
onClick={(e) => {
e.stopPropagation();
handleAction(() =>
onOpenTab("telnet", host.name),
);
}}
className="flex items-center gap-1 px-2 h-6 rounded text-xs font-medium text-muted-foreground/70 hover:text-foreground hover:bg-muted-foreground/10 transition-colors border border-border/40"
>
<Terminal className="size-3" />
Telnet
</button>
)}
<div className="w-px h-3.5 bg-border/60 mx-0.5 shrink-0" />
<button
title="Edit Host"
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
onOpenTab("host-manager");
setTimeout(() => {
window.dispatchEvent(
new CustomEvent("host-manager:edit-host", {
detail: host.id,
}),
);
}, 100);
}}
className="flex items-center justify-center size-7 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted-foreground/10 transition-colors"
>
<Pencil className="size-3.5" />
</button>
</div>
</CommandItem>
))}
</div>
))
) : (
<div className="py-6 text-center text-sm text-muted-foreground">
{t("commandPalette.noHostsFound", { search })}
</div>
)}
</CommandGroup>
</div>
<CommandSeparator className="my-2" />
<CommandGroup heading={t("commandPalette.links")} className="px-2">
<div className="grid grid-cols-3 gap-1">
<CommandItem
onSelect={() =>
window.open(
"https://github.com/Termix-SSH/Termix",
"_blank",
)
}
className="flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<Globe className="size-4 text-muted-foreground" />
<span className="text-sm font-medium">GitHub</span>
</CommandItem>
<CommandItem
onSelect={() =>
window.open(
"https://discord.com/invite/jVQGdvHDrf",
"_blank",
)
}
className="flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<MessagesSquare className="size-4 text-muted-foreground" />
<span className="text-sm font-medium">Discord</span>
</CommandItem>
<CommandItem
onSelect={() =>
window.open(
"https://github.com/Termix-SSH/Support/issues/new",
"_blank",
)
}
className="flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer"
>
<LifeBuoy className="size-4 text-muted-foreground" />
<span className="text-sm font-medium">Support</span>
</CommandItem>
</div>
</CommandGroup>
</CommandList>
<div className="border-t border-border px-4 py-3 bg-muted/30 flex items-center justify-between text-[11px] text-muted-foreground">
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<Kbd className="h-5 px-1 bg-background rounded-none">↑↓</Kbd>
<span>{t("commandPalette.navigate")}</span>
</div>
<div className="flex items-center gap-1">
<Kbd className="h-5 px-1 bg-background rounded-none">ENTER</Kbd>
<span>{t("commandPalette.select")}</span>
</div>
</div>
<div className="flex items-center gap-1">
<span>{t("commandPalette.toggleWith")}</span>
<Kbd className="h-5 px-1.5 bg-background rounded-none">Shift</Kbd>
<span>+</span>
<Kbd className="h-5 px-1.5 bg-background rounded-none">Shift</Kbd>
</div>
</div>
</Command>
</div>
</div>
);
}