read the shell prompt again and stop the reconnect loop
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

The terminal was the last surface in the chat tool panel still wearing
someone else's design system, and it had two defects underneath that.

The font stack named no Nerd Font, so the private-use glyphs a default
zsh prompt draws its separators and branch marks from rendered as notdef
boxes. The ~ and the path after it drew fine, which is what made it read
as a font bug rather than a missing-glyph one.

Both components also carried their own copy of the palette -- four themes
in one, fifteen in the other, drifting, defaulting to a near-black that
appears nowhere in the token set. Changing --code-bg did nothing to the
terminal because the terminal never read a token. They now share one
module built from the tokens, so an error in the terminal is the same
red as an error anywhere else.

TerminalView reconnected on a bare setTimeout with no attempt cap and no
error surface, retrying invisibly every three seconds forever against an
unreachable backend. It now shares the panel's cap of three and renders
the same retry affordance, counting down again once a session is created.

Also drops a dead ws.onopen assignment and a 21px padding that was on no
spacing scale, and records what the chat page still owes the prototype.
This commit is contained in:
2026-10-19 17:03:38 +08:00
parent a623398fe8
commit 8052bf5781
9 changed files with 465 additions and 307 deletions
@@ -7,72 +7,18 @@ import "@xterm/xterm/css/xterm.css";
import { getApiKey, getBaseUrlValue } from "@/api/client";
import { useMessage } from "@/composables/useMessage";
import { useI18n } from "vue-i18n";
import type { ITheme } from "@xterm/xterm";
import {
DEFAULT_TERMINAL_THEME,
TERMINAL_FONT_FAMILY,
getTerminalTheme,
terminalThemeOptions,
} from "@/utils/hermes/terminal-theme";
const { t } = useI18n();
const message = useMessage();
const props = defineProps<{ visible?: boolean; initialCommand?: string }>();
// ─── Terminal themes ────────────────────────────────────────────
const TERMINAL_THEMES: Record<string, { label: string; theme: ITheme }> = {
default: {
label: "Default",
theme: {
background: "#1a1a2e",
foreground: "#e0e0e0",
cursor: "#4cc9f0",
cursorAccent: "#1a1a2e",
selectionBackground: "rgba(76, 201, 240, 0.3)",
black: "#000000", red: "#e06c75", green: "#98c379", yellow: "#e5c07b",
blue: "#61afef", magenta: "#c678dd", cyan: "#56b6c2", white: "#abb2bf",
brightBlack: "#5c6370", brightRed: "#e06c75", brightGreen: "#98c379",
brightYellow: "#e5c07b", brightBlue: "#61afef", brightMagenta: "#c678dd",
brightCyan: "#56b6c2", brightWhite: "#ffffff",
},
},
"solarized-dark": {
label: "Solarized Dark",
theme: {
background: "#002b36", foreground: "#839496",
cursor: "#93a1a1", cursorAccent: "#002b36",
selectionBackground: "rgba(147, 161, 161, 0.3)",
black: "#073642", red: "#dc322f", green: "#859900", yellow: "#b58900",
blue: "#268bd2", magenta: "#d33682", cyan: "#2aa198", white: "#eee8d5",
brightBlack: "#002b36", brightRed: "#cb4b16", brightGreen: "#586e75",
brightYellow: "#657b83", brightBlue: "#839496", brightMagenta: "#6c71c4",
brightCyan: "#93a1a1", brightWhite: "#fdf6e3",
},
},
"tokyo-night": {
label: "Tokyo Night",
theme: {
background: "#1a1b26", foreground: "#a9b1d6",
cursor: "#c0caf5", cursorAccent: "#1a1b26",
selectionBackground: "rgba(192, 202, 245, 0.2)",
black: "#15161e", red: "#f7768e", green: "#9ece6a", yellow: "#e0af68",
blue: "#7aa2f7", magenta: "#bb9af7", cyan: "#7dcfff", white: "#a9b1d6",
brightBlack: "#414868", brightRed: "#f7768e", brightGreen: "#9ece6a",
brightYellow: "#e0af68", brightBlue: "#7aa2f7", brightMagenta: "#bb9af7",
brightCyan: "#7dcfff", brightWhite: "#c0caf5",
},
},
"github-dark": {
label: "GitHub Dark",
theme: {
background: "#0d1117", foreground: "#c9d1d9",
cursor: "#58a6ff", cursorAccent: "#0d1117",
selectionBackground: "rgba(88, 166, 255, 0.25)",
black: "#484f58", red: "#ff7b72", green: "#7ee787", yellow: "#ffa657",
blue: "#79c0ff", magenta: "#d2a8ff", cyan: "#a5d6ff", white: "#c9d1d9",
brightBlack: "#6e7681", brightRed: "#ffa198", brightGreen: "#56d364",
brightYellow: "#e3b341", brightBlue: "#58a6ff", brightMagenta: "#bc8cff",
brightCyan: "#79c0ff", brightWhite: "#f0f6fc",
},
},
};
const STORAGE_KEY_THEME = "hermes_terminal_theme";
// ─── Types ──────────────────────────────────────────────────────
@@ -91,7 +37,9 @@ interface SessionInfo {
const terminalRef = ref<HTMLDivElement | null>(null);
const sessions = ref<SessionInfo[]>([]);
const activeSessionId = ref<string | null>(null);
const selectedTheme = ref(localStorage.getItem(STORAGE_KEY_THEME) || "default");
const selectedTheme = ref(
localStorage.getItem(STORAGE_KEY_THEME) || DEFAULT_TERMINAL_THEME,
);
const connectionError = ref<string | null>(null);
const isConnecting = ref(false);
const showSidebar = ref(false);
@@ -117,16 +65,13 @@ const activeSession = computed(
() => sessions.value.find((s) => s.id === activeSessionId.value) || null,
);
const themeOptions = computed(() =>
Object.entries(TERMINAL_THEMES).map(([key, val]) => ({
label: val.label,
value: key,
})),
);
const themeOptions = computed(() => terminalThemeOptions());
const terminalBg = computed(
() => TERMINAL_THEMES[selectedTheme.value]?.theme.background ?? "#1a1a2e",
);
// 背景由 CSS 负责上色:样式表把 .xterm-screen 强制成透明,容器必须自己有底色。
// 这里只把选中主题的色值交给 CSS 变量,绘制仍留在样式层,令牌值兜底。
const terminalBgStyle = computed(() => ({
"--terminal-bg": getTerminalTheme(selectedTheme.value).background,
}));
// ─── WebSocket ──────────────────────────────────────────────────
@@ -281,9 +226,9 @@ function getOrCreateTerm(id: string): { term: Terminal; fitAddon: FitAddon } {
if (!entry) {
const term = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
theme: { ...TERMINAL_THEMES[selectedTheme.value].theme },
fontSize: 13,
fontFamily: TERMINAL_FONT_FAMILY,
theme: { ...getTerminalTheme(selectedTheme.value) },
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
@@ -420,8 +365,7 @@ function applyTheme(value: unknown) {
const themeName = String(value);
selectedTheme.value = themeName;
localStorage.setItem(STORAGE_KEY_THEME, themeName);
const themeObj = TERMINAL_THEMES[themeName]?.theme;
if (!themeObj) return;
const themeObj = getTerminalTheme(themeName);
for (const entry of termMap.values()) {
entry.term.options.theme = { ...themeObj };
}
@@ -578,7 +522,7 @@ onUnmounted(() => {
<div
ref="terminalRef"
class="terminal-xterm"
:style="{ backgroundColor: terminalBg }"
:style="terminalBgStyle"
@touchstart="handleTerminalTouchStart"
@touchmove="handleTerminalTouchMove"
@touchend="handleTerminalTouchEnd"
@@ -846,6 +790,8 @@ $terminal-panel-header-height: 47px;
flex: 1;
min-height: 0;
min-width: 0;
// --terminal-bg 由脚本按选中主题写入;令牌值兜底,保证 JS 未就绪时也不是黑洞。
background: var(--terminal-bg, #{$bg-input});
border-radius: $radius-md;
overflow: hidden;
border: 1px solid $border-color;