Files
openidea/src/features/run/RunView.tsx
T
l0ng-ai b03983847c fix(i18n): Translate UI text built from template literals
Template literals such as `Remove data source '${name}'?` reached props and
calls that look text up exactly (dialog messages, labels, aria-labels, menu
items, action titles), so they always showed in English. Some notification
titles also spliced lowercase English words into a sentence.

- `tx` also works as a tag: tx`Remove data source '${name}'?` looks up
  "Remove data source '{name}'?" by its literal parts, fills in the values
  (translated when the catalog has them) and remembers the result, so the
  primitives that pass strings through `tx` translate it again after a locale
  change or once the catalog has loaded (actions registered at startup).
- 269 template literals at exact-lookup sites now use the tag; text that
  spliced words or plural suffixes is written as one sentence per variant
  (safe delete, Docker delete/prune, rebase, conflicts, VCS "No … in
  progress", database copy errors, inspection descriptions, ...).
- zh: 280 new entries; stale and duplicate-shape entries removed.
- textCatalog guard: untagged template literals at exact-lookup sites fail
  (with an allowlist for code-like text), tagged text is checked against the
  catalog by shape, case-changing values and `c ? "a" : "b"` suffixes inside
  translated templates fail, and a catalog may hold one entry per template shape.

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
2026-10-27 18:43:53 +08:00

295 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from "react";
import { useWorkflowSettings } from "@/platform/workflowSettings";
import { EmptyState, IconButton, SplitGroup, SplitHandle, SplitPanel, Tabs, type TabItem, T } from "@/ui";
import { executeAction } from "@/platform/actions";
import { shortcutLabel } from "@/platform/keymap";
import { ConsoleModel } from "./console";
import { ConsoleView } from "./ConsoleView";
import { TerminalConsole } from "./TerminalConsole";
import { runDirectory } from "./navigate";
import {
acceptsInput,
clearConsole,
closeSessionTab,
closeStdin,
failedTests,
rerunFailed,
rerunSession,
resizeTerminal,
runSelectedTests,
runtime,
sendStdin,
setPendingInput,
stopSession,
useRun,
writeTerminal,
type RunSession,
} from "./store";
import { TestTreeView, formatDuration } from "./TestTreeView";
import { StackTracePanel, closeStackTrace, useStackTraces, type StackTraceTab } from "./stackTrace";
import styles from "./Run.module.css";
import { tx } from "@/platform/i18n";
function failed(s: RunSession): boolean {
return s.status === "failed" || (s.status === "finished" && s.exitCode !== 0 && !s.stopped);
}
function sessionIcon(s: RunSession): string {
if (s.status === "running" || s.status === "starting") return s.config.isTest ? "check" : "run";
if (failed(s)) return "error";
return s.config.isTest ? "check" : "run";
}
function SessionPanel({ session }: { session: RunSession }) {
const version = useRun((s) => s.versions[session.id] ?? 0);
const rt = runtime(session.id);
const [follow, setFollow] = useState(true);
// Settings › Console › Use soft wraps is the default; the toolbar toggle overrides it for this tab.
const defaultWrap = useWorkflowSettings((s) => s.console.softWrap);
const [wrapOverride, setWrapOverride] = useState<boolean | null>(null);
const softWrap = wrapOverride ?? defaultWrap;
const [selectedTest, setSelectedTest] = useState<string | null>(null);
const running = session.status === "running" || session.status === "starting";
const cwd = runDirectory(session.config.cwd);
// Selecting a test with captured output shows just that output (like IDEA).
const testModel = useMemo(() => {
const node = selectedTest ? rt?.tests?.get(selectedTest) : undefined;
if (!node || node.output.length === 0) return null;
const m = new ConsoleModel();
m.append({ kind: "stdout", text: node.output.join("\n") + "\n" });
return m;
}, [selectedTest, rt, version]); // eslint-disable-line react-hooks/exhaustive-deps
if (!rt) return null;
const toggleWrap = () => setWrapOverride(!softWrap);
const consoleColumn = (
<div className={styles.consoleColumn}>
{rt.console.dropped > 0 && !testModel && (
<div className={styles.notice}>{rt.console.dropped.toLocaleString()} earlier lines were discarded</div>
)}
{testModel && (
<div className={styles.notice}>
<button type="button" className={styles.noticeLink} onClick={() => setSelectedTest(null)}>
<T>Show all output</T>
</button>
</div>
)}
{rt.terminal && !testModel ? (
<TerminalConsole
output={rt.terminal}
cwd={cwd}
onData={(data) => writeTerminal(session.id, data)}
onResize={(cols, rows) => resizeTerminal(session.id, cols, rows)}
aria-label={tx`${session.config.name} terminal`}
/>
) : (
<ConsoleView
model={testModel ?? rt.console}
version={version}
softWrap={softWrap}
follow={testModel ? false : follow}
onFollowChange={setFollow}
cwd={cwd}
onClear={() => clearConsole(session.id)}
input={
!testModel && acceptsInput(session)
? {
pending: rt.input,
onChange: (text) => setPendingInput(session.id, text),
onSubmit: (line) => void sendStdin(session.id, line),
onEof: () => void closeStdin(session.id),
}
: undefined
}
aria-label={tx`${session.config.name} output`}
/>
)}
</div>
);
return (
<div className={styles.session}>
<div className={styles.toolbar} role="toolbar" aria-orientation="vertical" aria-label="Run actions">
<IconButton
icon="rerun"
iconSize={14}
title="Rerun"
shortcut={shortcutLabel("run.rerun")}
onClick={() => rerunSession(session.id)}
/>
{rt.tests && (
<IconButton
icon="rerun"
iconSize={14}
tone="danger"
title="Rerun Failed Tests"
shortcut={shortcutLabel("run.rerunFailed")}
disabled={running || failedTests(session.id).length === 0}
onClick={() => rerunFailed(session.id)}
/>
)}
<IconButton
icon="stop"
iconSize={13}
title="Stop"
tone={running ? "danger" : "default"}
disabled={!running}
onClick={() => void stopSession(session.id)}
/>
<div className={styles.toolbarSep} />
{!rt.terminal && (
<>
<IconButton
icon="arrow-down"
iconSize={14}
title="Scroll to End"
pressed={follow}
onClick={() => setFollow((v) => !v)}
/>
<IconButton icon="soft-wrap" iconSize={14} title="Soft-Wrap" pressed={softWrap} onClick={toggleWrap} />
</>
)}
<IconButton icon="trash" iconSize={14} title="Clear All" onClick={() => clearConsole(session.id)} />
<div className={styles.toolbarSep} />
<IconButton
icon="settings"
iconSize={14}
title="Edit Configurations…"
onClick={() => void executeAction("run.editConfigs", session.config.id)}
/>
</div>
<div className={styles.sessionBody}>
{rt.tests ? (
<SplitGroup orientation="horizontal" id={`run-tests-${session.id}`} className={styles.testSplit}>
<SplitPanel id="tests" defaultSize="32%" minSize="140px">
<TestTreeView
results={rt.tests}
version={version}
cwd={cwd}
running={running}
selected={selectedTest}
onSelect={setSelectedTest}
onRunTests={(ids, debug) => void runSelectedTests(session.id, ids, debug)}
/>
</SplitPanel>
<SplitHandle />
<SplitPanel id="console" minSize="200px">
{consoleColumn}
</SplitPanel>
</SplitGroup>
) : (
consoleColumn
)}
</div>
</div>
);
}
function sessionItems(sessions: readonly RunSession[]): TabItem[] {
return sessions.map((s) => ({
id: s.id,
label: s.config.name,
text: s.config.name,
icon: sessionIcon(s),
color: failed(s) ? "var(--danger)" : undefined,
closable: true,
tooltip:
s.status === "finished"
? tx`Finished in ${formatDuration((s.endedAt ?? s.startedAt) - s.startedAt)}, exit code ${s.exitCode ?? "?"}`
: undefined,
}));
}
/** The selected tab: a run session or an analyzed stack trace (IDEA shows both in Run). */
function currentTab(
sessions: readonly RunSession[],
traces: readonly StackTraceTab[],
active: string | null,
): { session: RunSession } | { trace: StackTraceTab } | null {
const session = sessions.find((s) => s.id === active);
if (session) return { session };
const trace = traces.find((t) => t.id === active);
if (trace) return { trace };
if (sessions.length) return { session: sessions[sessions.length - 1]! };
return traces.length ? { trace: traces[traces.length - 1]! } : null;
}
/** Session (and stack trace) tabs in the tool window header. */
export function RunTabs() {
const sessions = useRun((s) => s.sessions);
const traces = useStackTraces((s) => s.tabs);
const active = useRun((s) => s.activeSession);
const current = currentTab(sessions, traces, active);
if (!current) return null;
const items: TabItem[] = [
...sessionItems(sessions),
...traces.map((t) => ({ id: t.id, label: t.title, text: t.title, closable: true })),
];
return (
<Tabs
variant="compact"
className={styles.tabs}
aria-label="Run sessions"
items={items}
activeId={"session" in current ? current.session.id : current.trace.id}
onSelect={(id) => useRun.setState({ activeSession: id })}
onClose={(id) => (traces.some((t) => t.id === id) ? closeStackTrace(id) : void closeSessionTab(id))}
panelIdPrefix="openidea-run"
/>
);
}
export function RunView() {
const sessions = useRun((s) => s.sessions);
const traces = useStackTraces((s) => s.tabs);
const active = useRun((s) => s.activeSession);
const current = currentTab(sessions, traces, active);
if (!current) {
// IDEA's empty Run tool window: the three ways to run something.
const chooser = shortcutLabel("run.chooseConfig");
return (
<EmptyState>
<div className={styles.emptyHints}>
<T>To run your code, do one of the following:</T>
<ul>
<li>
<T>Click the Run icon in the editor gutter</T>
</li>
<li>
<T>{'Select "Run…" in the editor context menu'}</T>
</li>
<li>
<T>Launch a</T>{" "}
<button type="button" className={styles.noticeLink} onClick={() => void executeAction("run.editConfigs")}>
<T>run configuration</T>
</button>
{chooser && <span className={styles.chooserMuted}> ({chooser})</span>}
</li>
</ul>
</div>
</EmptyState>
);
}
const id = "session" in current ? current.session.id : current.trace.id;
return (
<div className={styles.root}>
<div
className={styles.panel}
role="tabpanel"
id={`openidea-run-panel-${id}`}
aria-labelledby={`openidea-run-tab-${id}`}
>
{"session" in current ? (
<SessionPanel key={id} session={current.session} />
) : (
<StackTracePanel key={id} tab={current.trace} />
)}
</div>
</div>
);
}