mirror of
https://github.com/alphaXiv/OpenResearch.git
synced 2026-10-02 01:34:34 +08:00
OR-225: localize dashboard in Chinese and Persian (#258)
* feat(ui): localize dashboard in Chinese and Persian * fix(ui): refine localized directionality
This commit is contained in:
@@ -24,9 +24,28 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Localization catalogs
|
||||
run: node ui/scripts/check-i18n.mjs
|
||||
|
||||
- name: Dev-slot helper tests
|
||||
run: node --test scripts/dev-slot.test.mjs
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install UI dependencies
|
||||
working-directory: ui
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate localized messages
|
||||
working-directory: ui
|
||||
run: pnpm exec paraglide-js compile --silent --emit-ts-declarations
|
||||
|
||||
- name: UI typecheck
|
||||
working-directory: ui
|
||||
run: pnpm exec tsc --noEmit
|
||||
|
||||
- name: UI unit tests
|
||||
run: node --test --experimental-strip-types ui/tests/*.test.mjs
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -1047,7 +1047,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openresearch-cli"
|
||||
version = "0.1.116"
|
||||
version = "0.1.117"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "openresearch-cli"
|
||||
version = "0.1.116"
|
||||
version = "0.1.117"
|
||||
edition = "2021"
|
||||
description = "OpenResearch CLI (orx) — Rust port"
|
||||
repository = "https://github.com/alphaXiv/openresearch-cli"
|
||||
|
||||
Vendored
+1051
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1041
File diff suppressed because one or more lines are too long
Vendored
+26
-3
@@ -20,19 +20,42 @@
|
||||
document.documentElement.dataset.theme = "light";
|
||||
}
|
||||
})();
|
||||
// Resolve language before first paint; keep supported in sync with project.inlang/settings.json.
|
||||
(function () {
|
||||
var saved = null;
|
||||
try { saved = localStorage.getItem("orx:locale"); } catch (e) {}
|
||||
var browserPreferences = navigator.languages || [navigator.language];
|
||||
var supported = ["en", "zh-CN", "fa"];
|
||||
var match = function (tag) {
|
||||
if (!tag) return null;
|
||||
var normalized = String(tag).toLowerCase();
|
||||
for (var j = 0; j < supported.length; j += 1) {
|
||||
if (supported[j].toLowerCase() === normalized) return supported[j];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
var locale = match(saved);
|
||||
for (var i = 0; !locale && i < browserPreferences.length; i += 1) {
|
||||
var preference = browserPreferences[i];
|
||||
locale = match(preference) || match(String(preference).split("-")[0]);
|
||||
}
|
||||
locale = locale || "en";
|
||||
document.documentElement.lang = locale;
|
||||
document.documentElement.dir = "ltr";
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
/* Pre-CSS background; keep in sync with --base in src/tailwind.css. */
|
||||
html { background: #ffffff; }
|
||||
html[data-theme="dark"] { background: #0e0c0c; }
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-DfoIjp19.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dc0I88Kl.css">
|
||||
<script type="module" crossorigin src="/assets/index-3pBlNq7W.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CECCiDmL.css">
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
class="[&_:where([data-tip])]:relative [&_:where([data-tip])::after]:content-[attr(data-tip)] [&_:where([data-tip])::after]:absolute [&_:where([data-tip])::after]:top-[calc(100%_+_6px)] [&_:where([data-tip])::after]:left-1/2 [&_:where([data-tip])::after]:-translate-x-1/2 [&_:where([data-tip])::after]:w-max [&_:where([data-tip])::after]:max-w-none [&_:where([data-tip])::after]:bg-text [&_:where([data-tip])::after]:text-background [&_:where([data-tip])::after]:text-xs [&_:where([data-tip])::after]:font-medium [&_:where([data-tip])::after]:leading-none [&_:where([data-tip])::after]:py-[5px] [&_:where([data-tip])::after]:px-2 [&_:where([data-tip])::after]:rounded-sm [&_:where([data-tip])::after]:whitespace-nowrap [&_:where([data-tip])::after]:opacity-0 [&_:where([data-tip])::after]:invisible [&_:where([data-tip])::after]:pointer-events-none [&_:where([data-tip])::after]:z-[9999] [&_:where([data-tip]):is(:hover,:focus-visible)::after]:opacity-100 [&_:where([data-tip]):is(:hover,:focus-visible)::after]:visible [&_:where([data-tip-align='start'])::after]:left-0 [&_:where([data-tip-align='start'])::after]:translate-none [&_:where([data-tip-align='end'])::after]:right-0 [&_:where([data-tip-align='end'])::after]:left-auto [&_:where([data-tip-align='end'])::after]:translate-none"
|
||||
class="[&_:where([data-tip])]:relative [&_:where([data-tip])::after]:content-[attr(data-tip)] [&_:where([data-tip])::after]:absolute [&_:where([data-tip])::after]:top-[calc(100%_+_6px)] [&_:where([data-tip])::after]:left-1/2 [&_:where([data-tip])::after]:-translate-x-1/2 [&_:where([data-tip])::after]:w-max [&_:where([data-tip])::after]:max-w-none [&_:where([data-tip])::after]:bg-text [&_:where([data-tip])::after]:text-background [&_:where([data-tip])::after]:text-xs [&_:where([data-tip])::after]:font-medium [&_:where([data-tip])::after]:leading-none [&_:where([data-tip])::after]:py-[5px] [&_:where([data-tip])::after]:px-2 [&_:where([data-tip])::after]:rounded-sm [&_:where([data-tip])::after]:whitespace-nowrap [&_:where([data-tip])::after]:opacity-0 [&_:where([data-tip])::after]:invisible [&_:where([data-tip])::after]:pointer-events-none [&_:where([data-tip])::after]:z-[9999] [&_:where([data-tip]):is(:hover,:focus-visible)::after]:opacity-100 [&_:where([data-tip]):is(:hover,:focus-visible)::after]:visible [&_[data-tip-align='start']::after]:start-0 [&_[data-tip-align='start']::after]:translate-none [&_[data-tip-align='end']::after]:end-0 [&_[data-tip-align='end']::after]:start-auto [&_[data-tip-align='end']::after]:translate-none"
|
||||
></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+24
-1
@@ -20,6 +20,29 @@
|
||||
document.documentElement.dataset.theme = "light";
|
||||
}
|
||||
})();
|
||||
// Resolve language before first paint; keep supported in sync with project.inlang/settings.json.
|
||||
(function () {
|
||||
var saved = null;
|
||||
try { saved = localStorage.getItem("orx:locale"); } catch (e) {}
|
||||
var browserPreferences = navigator.languages || [navigator.language];
|
||||
var supported = ["en", "zh-CN", "fa"];
|
||||
var match = function (tag) {
|
||||
if (!tag) return null;
|
||||
var normalized = String(tag).toLowerCase();
|
||||
for (var j = 0; j < supported.length; j += 1) {
|
||||
if (supported[j].toLowerCase() === normalized) return supported[j];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
var locale = match(saved);
|
||||
for (var i = 0; !locale && i < browserPreferences.length; i += 1) {
|
||||
var preference = browserPreferences[i];
|
||||
locale = match(preference) || match(String(preference).split("-")[0]);
|
||||
}
|
||||
locale = locale || "en";
|
||||
document.documentElement.lang = locale;
|
||||
document.documentElement.dir = "ltr";
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
/* Pre-CSS background; keep in sync with --base in src/tailwind.css. */
|
||||
@@ -30,7 +53,7 @@
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
class="[&_:where([data-tip])]:relative [&_:where([data-tip])::after]:content-[attr(data-tip)] [&_:where([data-tip])::after]:absolute [&_:where([data-tip])::after]:top-[calc(100%_+_6px)] [&_:where([data-tip])::after]:left-1/2 [&_:where([data-tip])::after]:-translate-x-1/2 [&_:where([data-tip])::after]:w-max [&_:where([data-tip])::after]:max-w-none [&_:where([data-tip])::after]:bg-text [&_:where([data-tip])::after]:text-background [&_:where([data-tip])::after]:text-xs [&_:where([data-tip])::after]:font-medium [&_:where([data-tip])::after]:leading-none [&_:where([data-tip])::after]:py-[5px] [&_:where([data-tip])::after]:px-2 [&_:where([data-tip])::after]:rounded-sm [&_:where([data-tip])::after]:whitespace-nowrap [&_:where([data-tip])::after]:opacity-0 [&_:where([data-tip])::after]:invisible [&_:where([data-tip])::after]:pointer-events-none [&_:where([data-tip])::after]:z-[9999] [&_:where([data-tip]):is(:hover,:focus-visible)::after]:opacity-100 [&_:where([data-tip]):is(:hover,:focus-visible)::after]:visible [&_:where([data-tip-align='start'])::after]:left-0 [&_:where([data-tip-align='start'])::after]:translate-none [&_:where([data-tip-align='end'])::after]:right-0 [&_:where([data-tip-align='end'])::after]:left-auto [&_:where([data-tip-align='end'])::after]:translate-none"
|
||||
class="[&_:where([data-tip])]:relative [&_:where([data-tip])::after]:content-[attr(data-tip)] [&_:where([data-tip])::after]:absolute [&_:where([data-tip])::after]:top-[calc(100%_+_6px)] [&_:where([data-tip])::after]:left-1/2 [&_:where([data-tip])::after]:-translate-x-1/2 [&_:where([data-tip])::after]:w-max [&_:where([data-tip])::after]:max-w-none [&_:where([data-tip])::after]:bg-text [&_:where([data-tip])::after]:text-background [&_:where([data-tip])::after]:text-xs [&_:where([data-tip])::after]:font-medium [&_:where([data-tip])::after]:leading-none [&_:where([data-tip])::after]:py-[5px] [&_:where([data-tip])::after]:px-2 [&_:where([data-tip])::after]:rounded-sm [&_:where([data-tip])::after]:whitespace-nowrap [&_:where([data-tip])::after]:opacity-0 [&_:where([data-tip])::after]:invisible [&_:where([data-tip])::after]:pointer-events-none [&_:where([data-tip])::after]:z-[9999] [&_:where([data-tip]):is(:hover,:focus-visible)::after]:opacity-100 [&_:where([data-tip]):is(:hover,:focus-visible)::after]:visible [&_[data-tip-align='start']::after]:start-0 [&_[data-tip-align='start']::after]:translate-none [&_[data-tip-align='end']::after]:end-0 [&_[data-tip-align='end']::after]:start-auto [&_[data-tip-align='end']::after]:translate-none"
|
||||
></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
+1053
File diff suppressed because it is too large
Load Diff
+1053
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -5,12 +5,14 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"build": "pnpm lint:i18n && paraglide-js compile --silent --emit-ts-declarations && tsc --noEmit && vite build",
|
||||
"lint:i18n": "node scripts/check-i18n.mjs",
|
||||
"test": "node --test --experimental-strip-types tests/*.test.mjs",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clo/react-markdown": "jsr:1.1.1",
|
||||
"@inlang/paraglide-js": "^2.25.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@xyflow/react": "^12.6.0",
|
||||
|
||||
Generated
+204
@@ -11,6 +11,9 @@ importers:
|
||||
'@clo/react-markdown':
|
||||
specifier: jsr:1.1.1
|
||||
version: '@jsr/clo__react-markdown@1.1.1'
|
||||
'@inlang/paraglide-js':
|
||||
specifier: ^2.25.0
|
||||
version: 2.25.0(typescript@5.9.3)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))
|
||||
'@xterm/addon-fit':
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0(@xterm/xterm@5.5.0)
|
||||
@@ -323,6 +326,25 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@inlang/paraglide-js@2.25.0':
|
||||
resolution: {integrity: sha512-a3Gw3md1S3WyJK1rFAP/FwDCsFKnu1b98rJ3k5bUl6nc3cmbglaSYpuK9qq9aLLlUlZhuCtB/Pb1LeKDUaLycg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>=5.6'
|
||||
vite: '>=5.0.0'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@inlang/recommend-sherlock@0.2.1':
|
||||
resolution: {integrity: sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==}
|
||||
|
||||
'@inlang/sdk@3.0.2':
|
||||
resolution: {integrity: sha512-qBAzkOLRQ42hZBl2/AZFZD+/JkdjBAvwxWphdTs9UbikMsuLathOBOIl+rC/mzaq48ylD3zE8Je2nrx7tbm0pw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
@@ -345,6 +367,29 @@ packages:
|
||||
'@jsr/clo__react-markdown@1.1.1':
|
||||
resolution: {integrity: sha512-no2p+01aol5OiHGUwzYGkAKDWWPGpNAlumZezytP+zidXMGSTFChHs7k7j3ILM8SYaTK0HyVg1to4yumyuX97A==, tarball: https://npm.jsr.io/~/11/@jsr/clo__react-markdown/1.1.1.tgz}
|
||||
|
||||
'@lix-js/sdk-darwin-arm64@0.12.3':
|
||||
resolution: {integrity: sha512-YeMJyYGk+QwvQe8O4AAcBMgfNm68IjAGDvuvZHr1U1rIb3GC2kYFStajqf4mT4BGc+cN65/P/2kkqEo+Igu/sA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@lix-js/sdk-linux-arm64@0.12.3':
|
||||
resolution: {integrity: sha512-jtmfNg6jLW61l1QvsU3W184l3hGtYUyPGzZrQyekk0vsMF1hxsP++7ONEGNI19PE7ldwSnGCB8Q6OzRKX87abQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@lix-js/sdk-linux-x64@0.12.3':
|
||||
resolution: {integrity: sha512-PiQL4h1FJXbxnZx/PgenI5grgpFtYlS/ZtPtTYh638YHVu+6J0cqGY/pLVqP8CKv1dWAKsZ2YgftLd+1j2+WKA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@lix-js/sdk-win32-x64@0.12.3':
|
||||
resolution: {integrity: sha512-hnxMI7kPS4ngXs0ZVnOve5CmtC4+vaNIyNDi/yWU0rSvxj7mqw/1RCegTrCB7LjxlX4DG+eg9HruXurk9NDjGg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@lix-js/sdk@0.12.3':
|
||||
resolution: {integrity: sha512-T2194yoZsM2S9kyRmI8UtEJL0fgXYAkeY9jZ/PVyC0K78/wmBvrqOSt9SdvgYxM5qnZ57re3NLOjVlV+X5hWxQ==}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||
|
||||
@@ -473,6 +518,13 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@sinclair/typebox@0.31.30':
|
||||
resolution: {integrity: sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow==}
|
||||
|
||||
'@sqlite.org/sqlite-wasm@3.48.0-build4':
|
||||
resolution: {integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==}
|
||||
hasBin: true
|
||||
|
||||
'@tailwindcss/node@4.3.3':
|
||||
resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==}
|
||||
|
||||
@@ -664,6 +716,14 @@ packages:
|
||||
'@xyflow/system@0.0.78':
|
||||
resolution: {integrity: sha512-lY0z2qP33fUhTva9Vaxrk0lqZta2pkbxB1trHAx1omnJqRtPvDlAQYV2r5fhS6AdpkulYmbNW0svy+A4/t4B/g==}
|
||||
|
||||
acorn@8.18.0:
|
||||
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
array-timsort@1.0.3:
|
||||
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
|
||||
|
||||
bail@2.0.2:
|
||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||
|
||||
@@ -704,10 +764,22 @@ packages:
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
commander@11.1.0:
|
||||
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
commander@8.3.0:
|
||||
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
comment-json@4.6.2:
|
||||
resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
consola@3.4.0:
|
||||
resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==}
|
||||
engines: {node: ^14.18.0 || >=16.10.0}
|
||||
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
@@ -802,6 +874,11 @@ packages:
|
||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
esprima@4.0.1:
|
||||
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
estree-util-is-identifier-name@3.0.0:
|
||||
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
|
||||
|
||||
@@ -817,6 +894,9 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fflate@0.8.3:
|
||||
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -905,6 +985,10 @@ packages:
|
||||
resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==}
|
||||
hasBin: true
|
||||
|
||||
kysely@0.28.17:
|
||||
resolution: {integrity: sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -1236,6 +1320,11 @@ packages:
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
sqlite-wasm-kysely@0.3.0:
|
||||
resolution: {integrity: sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==}
|
||||
peerDependencies:
|
||||
kysely: '*'
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
@@ -1291,17 +1380,36 @@ packages:
|
||||
unist-util-visit@5.1.0:
|
||||
resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
|
||||
|
||||
unplugin@2.3.11:
|
||||
resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
urlpattern-polyfill@10.1.0:
|
||||
resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==}
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
uuid@14.0.2:
|
||||
resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==}
|
||||
hasBin: true
|
||||
|
||||
valibot@1.4.2:
|
||||
resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==}
|
||||
peerDependencies:
|
||||
typescript: '>=5'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
vfile-location@5.0.3:
|
||||
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
||||
|
||||
@@ -1357,6 +1465,9 @@ packages:
|
||||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
@@ -1570,6 +1681,33 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@inlang/paraglide-js@2.25.0(typescript@5.9.3)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))':
|
||||
dependencies:
|
||||
'@inlang/recommend-sherlock': 0.2.1
|
||||
'@inlang/sdk': 3.0.2
|
||||
commander: 11.1.0
|
||||
consola: 3.4.0
|
||||
jiti: 2.7.0
|
||||
json5: 2.2.3
|
||||
unplugin: 2.3.11
|
||||
urlpattern-polyfill: 10.1.0
|
||||
valibot: 1.4.2(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)
|
||||
|
||||
'@inlang/recommend-sherlock@0.2.1':
|
||||
dependencies:
|
||||
comment-json: 4.6.2
|
||||
|
||||
'@inlang/sdk@3.0.2':
|
||||
dependencies:
|
||||
'@lix-js/sdk': 0.12.3
|
||||
'@sinclair/typebox': 0.31.30
|
||||
kysely: 0.28.17
|
||||
sqlite-wasm-kysely: 0.3.0(kysely@0.28.17)
|
||||
uuid: 14.0.2
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -1605,6 +1743,27 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@lix-js/sdk-darwin-arm64@0.12.3':
|
||||
optional: true
|
||||
|
||||
'@lix-js/sdk-linux-arm64@0.12.3':
|
||||
optional: true
|
||||
|
||||
'@lix-js/sdk-linux-x64@0.12.3':
|
||||
optional: true
|
||||
|
||||
'@lix-js/sdk-win32-x64@0.12.3':
|
||||
optional: true
|
||||
|
||||
'@lix-js/sdk@0.12.3':
|
||||
dependencies:
|
||||
fflate: 0.8.3
|
||||
optionalDependencies:
|
||||
'@lix-js/sdk-darwin-arm64': 0.12.3
|
||||
'@lix-js/sdk-linux-arm64': 0.12.3
|
||||
'@lix-js/sdk-linux-x64': 0.12.3
|
||||
'@lix-js/sdk-win32-x64': 0.12.3
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.62.2':
|
||||
@@ -1682,6 +1841,10 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.62.2':
|
||||
optional: true
|
||||
|
||||
'@sinclair/typebox@0.31.30': {}
|
||||
|
||||
'@sqlite.org/sqlite-wasm@3.48.0-build4': {}
|
||||
|
||||
'@tailwindcss/node@4.3.3':
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
@@ -1873,6 +2036,10 @@ snapshots:
|
||||
d3-selection: 3.0.0
|
||||
d3-zoom: 3.0.0
|
||||
|
||||
acorn@8.18.0: {}
|
||||
|
||||
array-timsort@1.0.3: {}
|
||||
|
||||
bail@2.0.2: {}
|
||||
|
||||
baseline-browser-mapping@2.10.42: {}
|
||||
@@ -1903,8 +2070,17 @@ snapshots:
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@11.1.0: {}
|
||||
|
||||
commander@8.3.0: {}
|
||||
|
||||
comment-json@4.6.2:
|
||||
dependencies:
|
||||
array-timsort: 1.0.3
|
||||
esprima: 4.0.1
|
||||
|
||||
consola@3.4.0: {}
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
@@ -2005,6 +2181,8 @@ snapshots:
|
||||
|
||||
escape-string-regexp@5.0.0: {}
|
||||
|
||||
esprima@4.0.1: {}
|
||||
|
||||
estree-util-is-identifier-name@3.0.0: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
@@ -2013,6 +2191,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.5
|
||||
|
||||
fflate@0.8.3: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -2131,6 +2311,8 @@ snapshots:
|
||||
dependencies:
|
||||
commander: 8.3.0
|
||||
|
||||
kysely@0.28.17: {}
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
optional: true
|
||||
|
||||
@@ -2746,6 +2928,11 @@ snapshots:
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
sqlite-wasm-kysely@0.3.0(kysely@0.28.17):
|
||||
dependencies:
|
||||
'@sqlite.org/sqlite-wasm': 3.48.0-build4
|
||||
kysely: 0.28.17
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
dependencies:
|
||||
character-entities-html4: 2.1.0
|
||||
@@ -2817,16 +3004,31 @@ snapshots:
|
||||
unist-util-is: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
unplugin@2.3.11:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
acorn: 8.18.0
|
||||
picomatch: 4.0.5
|
||||
webpack-virtual-modules: 0.6.2
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.4):
|
||||
dependencies:
|
||||
browserslist: 4.28.4
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
urlpattern-polyfill@10.1.0: {}
|
||||
|
||||
use-sync-external-store@1.6.0(react@19.2.7):
|
||||
dependencies:
|
||||
react: 19.2.7
|
||||
|
||||
uuid@14.0.2: {}
|
||||
|
||||
valibot@1.4.2(typescript@5.9.3):
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
vfile-location@5.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -2861,6 +3063,8 @@ snapshots:
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
zustand@4.5.7(@types/react@19.2.17)(react@19.2.7):
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"baseLocale": "en",
|
||||
"locales": ["en", "zh-CN", "fa"],
|
||||
"modules": [
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
|
||||
],
|
||||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{locale}.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const uiDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const settings = await readJson(join(uiDir, "project.inlang/settings.json"));
|
||||
const catalogs = new Map();
|
||||
const errors = [];
|
||||
|
||||
for (const locale of settings.locales) {
|
||||
const file = join(uiDir, "messages", `${locale}.json`);
|
||||
try {
|
||||
catalogs.set(locale, await readJson(file));
|
||||
} catch (error) {
|
||||
errors.push(`${locale}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const base = catalogs.get(settings.baseLocale);
|
||||
if (!base) {
|
||||
errors.push(`Base locale catalog is unavailable: ${settings.baseLocale}`);
|
||||
} else {
|
||||
const baseKeys = messageKeys(base);
|
||||
for (const [locale, catalog] of catalogs) {
|
||||
const keys = messageKeys(catalog);
|
||||
const missing = baseKeys.filter((key) => !keys.includes(key));
|
||||
const unexpected = keys.filter((key) => !baseKeys.includes(key));
|
||||
const invalid = keys.filter(
|
||||
(key) => typeof catalog[key] !== "string" || catalog[key].trim() === "",
|
||||
);
|
||||
const placeholderMismatch = keys.filter(
|
||||
(key) =>
|
||||
typeof catalog[key] === "string" &&
|
||||
placeholders(catalog[key]).join(",") !== placeholders(base[key]).join(","),
|
||||
);
|
||||
const entities = keys.filter(
|
||||
(key) => typeof catalog[key] === "string" && /&(?:apos|quot|amp|lt|gt);/.test(catalog[key]),
|
||||
);
|
||||
if (missing.length) errors.push(`${locale}: missing keys: ${missing.join(", ")}`);
|
||||
if (unexpected.length) errors.push(`${locale}: unexpected keys: ${unexpected.join(", ")}`);
|
||||
if (invalid.length) errors.push(`${locale}: empty or non-string values: ${invalid.join(", ")}`);
|
||||
if (placeholderMismatch.length) errors.push(`${locale}: placeholder mismatch: ${placeholderMismatch.join(", ")}`);
|
||||
if (entities.length) errors.push(`${locale}: HTML entities must be decoded: ${entities.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
console.error(`Localization catalog lint failed:\n${errors.map((error) => `- ${error}`).join("\n")}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
async function readJson(file) {
|
||||
try {
|
||||
return JSON.parse(await readFile(file, "utf8"));
|
||||
} catch (error) {
|
||||
throw new Error(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function messageKeys(catalog) {
|
||||
return Object.keys(catalog)
|
||||
.filter((key) => !key.startsWith("$"))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function placeholders(message) {
|
||||
if (typeof message !== "string") return [];
|
||||
return [...message.matchAll(/\{([A-Za-z][A-Za-z0-9_]*)\}/g)]
|
||||
.map((match) => match[1])
|
||||
.sort();
|
||||
}
|
||||
+34
-32
@@ -1,3 +1,5 @@
|
||||
import { m } from "./paraglide/messages.js";
|
||||
import { getLocale } from "./paraglide/runtime.js";
|
||||
import {
|
||||
ChartSpline,
|
||||
Check,
|
||||
@@ -408,7 +410,7 @@ const PANEL_MIN_WIDTH = 360;
|
||||
const PANEL_MARGIN = 10;
|
||||
// Space the rest of the layout needs beside the panel: the 272px rail, the
|
||||
// chat column's minimum, and the gutters/margins between the three columns
|
||||
// (app-body padding 14×2, rail inner margin 14, right-pane inner margin 14).
|
||||
// (app-body padding 14×2, rail inner margin 14, end-pane inner margin 14).
|
||||
const RAIL_WIDTH = 272;
|
||||
const CHAT_MIN_SPACE = 380;
|
||||
const LAYOUT_CHROME = RAIL_WIDTH + 14 * 4;
|
||||
@@ -830,16 +832,16 @@ export default function App() {
|
||||
: projectsResult.value[0]?.id ?? null,
|
||||
);
|
||||
} else {
|
||||
errors.push("projects");
|
||||
errors.push(m.app_projects());
|
||||
}
|
||||
if (uiStateResult.status === "fulfilled") {
|
||||
persistedPreferredAgent.current = uiStateResult.value.preferredAgent;
|
||||
setUiState(uiStateResult.value);
|
||||
} else {
|
||||
errors.push("settings");
|
||||
errors.push(m.app_settings());
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
setStartupError(`Couldn't load OpenResearch ${errors.join(" and ")}.`);
|
||||
setStartupError(m.app_startup_load_failed({ items: new Intl.ListFormat(getLocale()).format(errors) }));
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
@@ -1060,14 +1062,14 @@ export default function App() {
|
||||
);
|
||||
|
||||
const nextExperimentNames = useMemo(
|
||||
() => new Map(experiments.map((experiment) => [experiment.id, experiment.title?.trim() || experiment.slug || "Experiment"])),
|
||||
() => new Map(experiments.map((experiment) => [experiment.id, experiment.title?.trim() || experiment.slug || m.tree_experiment()])),
|
||||
[experiments],
|
||||
);
|
||||
const experimentNames = useStableStringMap(nextExperimentNames);
|
||||
const nextRunExperimentNames = useMemo(() => {
|
||||
const names = new Map<string, string>();
|
||||
for (const run of runs) {
|
||||
names.set(run.id, experimentNames.get(run.experimentId) ?? "Experiment");
|
||||
names.set(run.id, experimentNames.get(run.experimentId) ?? m.tree_experiment());
|
||||
}
|
||||
return names;
|
||||
}, [experimentNames, runs]);
|
||||
@@ -1609,7 +1611,7 @@ export default function App() {
|
||||
<ClosableTab
|
||||
key={rightTabKey(tab)}
|
||||
active={planTab !== null && planTab.promptId === tab.promptId}
|
||||
label="Plan"
|
||||
label={m.chat_plan()}
|
||||
icon={<ScrollText size={12} style={{ flexShrink: 0 }} />}
|
||||
preview={isPreviewTab(tab)}
|
||||
onSelect={() => selectRightTab(tab)}
|
||||
@@ -1623,7 +1625,7 @@ export default function App() {
|
||||
<ClosableTab
|
||||
key={rightTabKey(tab)}
|
||||
active={subagentTab !== null && subagentTab.spawnPartId === tab.spawnPartId}
|
||||
label={spawnMeta[tab.spawnPartId]?.label ?? tab.label ?? "Sub-agent"}
|
||||
label={spawnMeta[tab.spawnPartId]?.label ?? tab.label ?? m.app_subagent()}
|
||||
shimmer={spawnMeta[tab.spawnPartId]?.running ?? false}
|
||||
icon={<Users size={12} style={{ flexShrink: 0 }} />}
|
||||
preview={isPreviewTab(tab)}
|
||||
@@ -1653,7 +1655,7 @@ export default function App() {
|
||||
<div className="app flex flex-col h-full">
|
||||
<div className={EMPTY_STATE_CLASS_NAME}>
|
||||
<p>{startupError}</p>
|
||||
<button className={PRIMARY_BUTTON_CLASS_NAME} onClick={loadInitialState}>Retry</button>
|
||||
<button className={PRIMARY_BUTTON_CLASS_NAME} onClick={loadInitialState}>{m.app_retry()}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1778,22 +1780,22 @@ export default function App() {
|
||||
)}
|
||||
{mainView === "chat" && panelOpen && (
|
||||
<aside
|
||||
className={`right-pane relative shrink-0 min-w-0 flex flex-col mt-5 mr-0 mb-5 ml-3.5 bg-canvas [&.max]:fixed [&.max]:inset-2.5 [&.max]:m-0 [&.max]:z-60 [&.max]:shadow-[0_12px_40px_color-mix(in_oklab,_var(--text)_22%,_transparent)] floating-panel border border-border rounded-lg overflow-hidden ${ELEVATED_SURFACE_SHADOW_CLASS_NAME} ${panelMax ? "max" : ""}`}
|
||||
className={`right-pane relative shrink-0 min-w-0 flex flex-col mt-5 me-0 mb-5 ms-3.5 bg-canvas [&.max]:fixed [&.max]:inset-2.5 [&.max]:m-0 [&.max]:z-60 [&.max]:shadow-[0_12px_40px_color-mix(in_oklab,_var(--text)_22%,_transparent)] floating-panel border border-border rounded-lg overflow-hidden ${ELEVATED_SURFACE_SHADOW_CLASS_NAME} ${panelMax ? "max" : ""}`}
|
||||
style={panelMax ? undefined : { width: panelWidth }}
|
||||
data-onboarding="experiments"
|
||||
>
|
||||
<div
|
||||
className={`panel-resizer absolute left-0 top-0 bottom-0 w-1.5 z-30 [&:hover]:bg-[color-mix(in_oklab,_var(--text)_12%,_transparent)] [&:active]:bg-[color-mix(in_oklab,_var(--text)_12%,_transparent)] ${panelMax ? "cursor-e-resize" : "cursor-col-resize"}`}
|
||||
title={panelMax ? "Drag right to restore panel" : "Drag to resize panel"}
|
||||
className={`panel-resizer absolute start-0 top-0 bottom-0 w-1.5 z-30 [&:hover]:bg-[color-mix(in_oklab,_var(--text)_12%,_transparent)] [&:active]:bg-[color-mix(in_oklab,_var(--text)_12%,_transparent)] ${panelMax ? "cursor-e-resize" : "cursor-col-resize"}`}
|
||||
title={panelMax ? m.app_drag_to_restore_panel() : m.app_drag_to_resize_panel()}
|
||||
onPointerDown={resizePanel}
|
||||
/>
|
||||
<div className="tabs flex items-end gap-0 pt-1 pr-1.5 pb-0 pl-2 h-10 border-b border-b-border bg-background shrink-0">
|
||||
<div className="tabs flex items-end gap-0 pt-1 pe-1.5 pb-0 ps-2 h-10 border-b border-b-border bg-background shrink-0">
|
||||
<div className="tab-strip flex items-end gap-0.5 flex-1 min-w-0 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{leadingFileTabs.map(renderFileTab)}
|
||||
{filesTabOpen && (
|
||||
<ClosableTab
|
||||
active={rightTab === "files"}
|
||||
label="Files"
|
||||
label={m.app_files()}
|
||||
icon={<FolderOpen size={12} style={{ flexShrink: 0 }} />}
|
||||
onSelect={() => selectRightTab("files")}
|
||||
onClose={() => closeHomeTab("files")}
|
||||
@@ -1802,7 +1804,7 @@ export default function App() {
|
||||
{artifactsTabOpen && (
|
||||
<ClosableTab
|
||||
active={rightTab === "artifacts"}
|
||||
label="Artifacts"
|
||||
label={m.app_artifacts()}
|
||||
icon={<Package size={12} style={{ flexShrink: 0 }} />}
|
||||
onSelect={() => selectRightTab("artifacts")}
|
||||
onClose={() => closeHomeTab("artifacts")}
|
||||
@@ -1811,7 +1813,7 @@ export default function App() {
|
||||
{experimentsTabOpen && (
|
||||
<ClosableTab
|
||||
active={rightTab === "experiments"}
|
||||
label="Experiments"
|
||||
label={m.app_experiments()}
|
||||
icon={<FlaskConical size={12} style={{ flexShrink: 0 }} />}
|
||||
onSelect={() => selectRightTab("experiments")}
|
||||
onClose={() => closeHomeTab("experiments")}
|
||||
@@ -1822,16 +1824,16 @@ export default function App() {
|
||||
<div className="panel-controls inline-flex items-center gap-0.5 self-center py-0 px-1.5 shrink-0">
|
||||
<button
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
title={panelMax ? "Restore panel" : "Expand panel"}
|
||||
aria-label={panelMax ? "Restore panel" : "Expand panel"}
|
||||
title={panelMax ? m.app_restore_panel() : m.app_expand_panel()}
|
||||
aria-label={panelMax ? m.app_restore_panel() : m.app_expand_panel()}
|
||||
onClick={() => setPanelMax((m) => !m)}
|
||||
>
|
||||
{panelMax ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
|
||||
</button>
|
||||
<button
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
title="Close panel"
|
||||
aria-label="Close panel"
|
||||
title={m.app_close_panel()}
|
||||
aria-label={m.app_close_panel()}
|
||||
onClick={() => {
|
||||
pendingExperimentsAutoOpenRef.current = false;
|
||||
setPanelOpen(false);
|
||||
@@ -1864,24 +1866,24 @@ export default function App() {
|
||||
<button
|
||||
ref={scopeTriggerRef}
|
||||
className={`${ICON_BUTTON_BASE_CLASS_NAME} experiment-scope-trigger w-6.5 h-6.5 rounded-sm${effectiveScope === "agent" ? " active" : ""}`}
|
||||
title={`Experiment filter: ${effectiveScope === "agent" ? "Current task" : "Entire project"}`}
|
||||
aria-label="Filter experiments"
|
||||
title={m.app_experiment_filter({ scope: effectiveScope === "agent" ? m.app_current_task() : m.app_entire_project() })}
|
||||
aria-label={m.app_filter_experiments()}
|
||||
aria-expanded={scopeMenuOpen}
|
||||
onClick={() => setScopeMenuOpen((open) => !open)}
|
||||
>
|
||||
<Filter size={16} strokeWidth={2.5} />
|
||||
</button>
|
||||
{scopeMenuOpen && (
|
||||
<div className="option-menu absolute bottom-[calc(100%_+_8px)] left-0 max-h-95 flex flex-col bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] z-50 overflow-hidden min-w-47.5 p-1.5 [&.align-right]:left-auto [&.align-right]:right-0 [&.drop-down]:bottom-auto [&.drop-down]:top-[calc(100%_+_4px)] [&.session-menu]:left-auto [&.session-menu]:right-1.5 [&.session-menu]:top-[calc(100%_-_2px)] [&.session-menu]:min-w-35 drop-down align-right experiment-scope-menu [&_.model-item]:whitespace-nowrap [&_.model-item:disabled]:text-muted [&_.model-item:disabled]:cursor-default [&_.model-item:disabled:hover]:bg-transparent">
|
||||
<div className="option-menu absolute bottom-[calc(100%_+_8px)] start-0 max-h-95 flex flex-col bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] z-50 overflow-hidden min-w-47.5 p-1.5 [&.align-right]:start-auto [&.align-right]:end-0 [&.drop-down]:bottom-auto [&.drop-down]:top-[calc(100%_+_4px)] [&.session-menu]:start-auto [&.session-menu]:end-1.5 [&.session-menu]:top-[calc(100%_-_2px)] [&.session-menu]:min-w-35 drop-down align-right experiment-scope-menu [&_.model-item]:whitespace-nowrap [&_.model-item:disabled]:text-muted [&_.model-item:disabled]:cursor-default [&_.model-item:disabled:hover]:bg-transparent">
|
||||
<button
|
||||
className={MODEL_ITEM_CLASS_NAME}
|
||||
aria-pressed={effectiveScope === "agent"}
|
||||
disabled={!activeSessionId || !allExperimentsAttributed}
|
||||
title={
|
||||
!activeSessionId
|
||||
? "Open a task to filter to its experiments"
|
||||
? m.app_open_task_to_filter()
|
||||
: !allExperimentsAttributed
|
||||
? "Current task filtering is unavailable for unattributed experiments"
|
||||
? m.app_filter_unavailable_unattributed()
|
||||
: undefined
|
||||
}
|
||||
onClick={() => {
|
||||
@@ -1889,7 +1891,7 @@ export default function App() {
|
||||
setScopeMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<span>Current task</span>
|
||||
<span>{m.app_current_task()}</span>
|
||||
{effectiveScope === "agent" && <Check size={13} />}
|
||||
</button>
|
||||
<button
|
||||
@@ -1900,7 +1902,7 @@ export default function App() {
|
||||
setScopeMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<span>Entire project</span>
|
||||
<span>{m.app_entire_project()}</span>
|
||||
{effectiveScope === "project" && <Check size={13} />}
|
||||
</button>
|
||||
</div>
|
||||
@@ -1909,21 +1911,21 @@ export default function App() {
|
||||
<div
|
||||
className="seg inline-flex items-center gap-0.5 rounded-md bg-[color-mix(in_oklab,_var(--text)_10%,_transparent)] [&_button]:font-semibold [&_button]:text-text [&_button]:rounded-sm [&_button:not(:disabled):hover]:text-text [&_button.active]:bg-background [&_button.active]:shadow-[0_1px_3px_color-mix(in_oklab,_var(--text)_25%,_transparent)] [&_button:disabled]:text-muted [&_button:disabled]:cursor-default experiments-view-toggle p-0.5 [&_button]:py-0.5 [&_button]:px-2 [&_button]:text-sm"
|
||||
role="group"
|
||||
aria-label="Experiment view"
|
||||
aria-label={m.app_experiment_view()}
|
||||
>
|
||||
<button
|
||||
className={view === "table" ? "active" : ""}
|
||||
aria-pressed={view === "table"}
|
||||
onClick={() => setView("table")}
|
||||
>
|
||||
Table
|
||||
{m.app_table()}
|
||||
</button>
|
||||
<button
|
||||
className={view === "tree" ? "active" : ""}
|
||||
aria-pressed={view === "tree"}
|
||||
onClick={() => setView("tree")}
|
||||
>
|
||||
Tree
|
||||
{m.app_tree()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1946,7 +1948,7 @@ export default function App() {
|
||||
runs={scopedRuns}
|
||||
emptyHint={
|
||||
effectiveScope === "agent" && experiments.length > 0
|
||||
? "No experiments from the current task yet. Switch to Entire project to see all experiments."
|
||||
? m.app_no_task_experiments()
|
||||
: undefined
|
||||
}
|
||||
experiments={scopedExperiments}
|
||||
@@ -2000,7 +2002,7 @@ export default function App() {
|
||||
<div className={CODE_TAB_BODY_CLASS_NAME}>
|
||||
<div className="wt-empty flex flex-col items-center gap-2.5 py-12 px-6 text-center text-muted [&_>_svg]:text-subtext [&_p]:m-0 [&_p]:max-w-80 [&_p]:text-sm">
|
||||
<FolderGit2 size={22} />
|
||||
<p>Select a project to browse its files.</p>
|
||||
<p>{m.app_select_a_project_to_browse_its_files()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+26
-23
@@ -1,5 +1,11 @@
|
||||
// Typed client for the orx up local HTTP API (/api/*). All wire JSON is camelCase.
|
||||
|
||||
import { m } from "./paraglide/messages.js";
|
||||
import { getLocale } from "./paraglide/runtime.js";
|
||||
import { fmtNumber } from "./i18n";
|
||||
|
||||
export { fmtNumber } from "./i18n";
|
||||
|
||||
export const DEMO_PROJECT_ID = "demo_nanochat_v1";
|
||||
// Bundled demo snapshots reserve this prefix so future demos inherit demo-only UI.
|
||||
export const isDemoProjectId = (id: string) => id.startsWith("demo_");
|
||||
@@ -1213,7 +1219,7 @@ export function serviceTiersFor(
|
||||
const tiers = harness.models.find((model) => model.id === modelId)?.serviceTiers;
|
||||
if (!tiers?.length) return [];
|
||||
return [
|
||||
{ id: SERVICE_TIER_DEFAULT_ID, label: "Standard", description: "Default speed" },
|
||||
{ id: SERVICE_TIER_DEFAULT_ID, label: m.service_tier_standard(), description: m.service_tier_default_speed() },
|
||||
...tiers,
|
||||
];
|
||||
}
|
||||
@@ -1721,23 +1727,24 @@ export function statusColor(status: string): string {
|
||||
|
||||
export function timeAgo(ms: number): string {
|
||||
const s = Math.max(0, Math.floor((Date.now() - ms) / 1000));
|
||||
if (s < 60) return `${s}s ago`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m ago`;
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return `${h}h ago`;
|
||||
return `${Math.floor(h / 24)}d ago`;
|
||||
const format = new Intl.RelativeTimeFormat(getLocale(), { numeric: "always", style: "narrow" });
|
||||
if (s < 60) return format.format(-s, "second");
|
||||
const minutes = Math.floor(s / 60);
|
||||
if (minutes < 60) return format.format(-minutes, "minute");
|
||||
const h = Math.floor(minutes / 60);
|
||||
if (h < 24) return format.format(-h, "hour");
|
||||
return format.format(-Math.floor(h / 24), "day");
|
||||
}
|
||||
|
||||
/** "42s" / "18m" / "2h 28m" / "1d 4h" — an elapsed duration, not a timestamp. */
|
||||
export function fmtDuration(ms: number): string {
|
||||
const s = Math.max(0, Math.floor(ms / 1000));
|
||||
if (s < 60) return `${s}s`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m`;
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 24) return `${h}h ${m % 60}m`;
|
||||
return `${Math.floor(h / 24)}d ${h % 24}h`;
|
||||
if (s < 60) return m.duration_seconds({ value: fmtNumber(s) });
|
||||
const minutes = Math.floor(s / 60);
|
||||
if (minutes < 60) return m.duration_minutes({ value: fmtNumber(minutes) });
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return m.duration_hours_minutes({ hours: fmtNumber(hours), minutes: fmtNumber(minutes % 60) });
|
||||
return m.duration_days_hours({ days: fmtNumber(Math.floor(hours / 24)), hours: fmtNumber(hours % 24) });
|
||||
}
|
||||
|
||||
/** Compact byte size, e.g. "512 B", "2.0 KB", "5.3 MB". Mirrors the backend's
|
||||
@@ -1750,20 +1757,16 @@ export function fmtBytes(n: number): string {
|
||||
v /= 1024;
|
||||
u += 1;
|
||||
}
|
||||
return u === 0 ? `${n} B` : `${v.toFixed(1)} ${units[u]}`;
|
||||
const number = new Intl.NumberFormat(getLocale(), { maximumFractionDigits: u === 0 ? 0 : 1, minimumFractionDigits: u === 0 ? 0 : 1 });
|
||||
return `${number.format(u === 0 ? n : v)} ${units[u]}`;
|
||||
}
|
||||
|
||||
/** Compact token count, e.g. 62300 → "62k", 1_200_000 → "1.2M", 940 → "940". */
|
||||
export function fmtTokens(n: number): string {
|
||||
if (n < 1000) return `${Math.round(n)}`;
|
||||
// One decimal, dropped when it's .0 ("31.4k", "200k", "1M"). The k branch
|
||||
// stops where toFixed(1) would round to "1000.0" (e.g. 999_960 → "1M").
|
||||
if (n < 999_950) return `${trimZero((n / 1000).toFixed(1))}k`;
|
||||
return `${trimZero((n / 1_000_000).toFixed(1))}M`;
|
||||
}
|
||||
|
||||
function trimZero(s: string): string {
|
||||
return s.endsWith(".0") ? s.slice(0, -2) : s;
|
||||
const number = new Intl.NumberFormat(getLocale(), { maximumFractionDigits: 1 });
|
||||
if (n < 1000) return number.format(Math.round(n));
|
||||
if (n < 999_950) return `${number.format(n / 1000)}k`;
|
||||
return `${number.format(n / 1_000_000)}M`;
|
||||
}
|
||||
|
||||
export function shortId(id: string): string {
|
||||
|
||||
+19
-12
@@ -1,4 +1,8 @@
|
||||
import type { TurnOptions } from "./api";
|
||||
import { m } from "./paraglide/messages.js";
|
||||
import { getLocale } from "./paraglide/runtime.js";
|
||||
|
||||
const fmtNumber = (value: number) => new Intl.NumberFormat(getLocale()).format(value);
|
||||
|
||||
export interface RetryMetadata {
|
||||
retryOwner?: "native" | "orx";
|
||||
@@ -12,23 +16,26 @@ export function retryStatusLabel(input: RetryMetadata, now: number): string {
|
||||
? Math.max(0, Math.ceil((input.nextRetryAt - now) / 1000))
|
||||
: null;
|
||||
if (input.retryOwner === "native" && input.maximum == null && seconds == null) {
|
||||
return "CLI is retrying…";
|
||||
return m.retry_cli();
|
||||
}
|
||||
return [
|
||||
"Retrying",
|
||||
typeof input.attempt === "number"
|
||||
? typeof input.maximum === "number"
|
||||
? `attempt ${input.attempt}/${input.maximum}`
|
||||
: `attempt ${input.attempt}`
|
||||
: null,
|
||||
seconds == null ? null : `next attempt in ${seconds}s`,
|
||||
].filter(Boolean).join(" · ");
|
||||
if (typeof input.attempt === "number" && typeof input.maximum === "number" && seconds != null) {
|
||||
return m.retry_attempt_max_seconds({ attempt: fmtNumber(input.attempt), maximum: fmtNumber(input.maximum), seconds: fmtNumber(seconds) });
|
||||
}
|
||||
if (typeof input.attempt === "number" && typeof input.maximum === "number") {
|
||||
return m.retry_attempt_max({ attempt: fmtNumber(input.attempt), maximum: fmtNumber(input.maximum) });
|
||||
}
|
||||
if (typeof input.attempt === "number" && seconds != null) {
|
||||
return m.retry_attempt_seconds({ attempt: fmtNumber(input.attempt), seconds: fmtNumber(seconds) });
|
||||
}
|
||||
if (typeof input.attempt === "number") return m.retry_attempt({ attempt: fmtNumber(input.attempt) });
|
||||
if (seconds != null) return m.retry_seconds({ seconds: fmtNumber(seconds) });
|
||||
return m.retrying();
|
||||
}
|
||||
|
||||
export function queuedRetryLabel(nextRetryAt: number | null | undefined, now: number): string {
|
||||
if (typeof nextRetryAt !== "number") return "Sending again…";
|
||||
if (typeof nextRetryAt !== "number") return m.retry_sending_again();
|
||||
const seconds = Math.max(0, Math.ceil((nextRetryAt - now) / 1000));
|
||||
return `Sending again in ${seconds}s…`;
|
||||
return m.retry_sending_again_seconds({ seconds: fmtNumber(seconds) });
|
||||
}
|
||||
|
||||
export function recoveryAction(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ import { backendDetail, backendKind, type Run } from "../api";
|
||||
import slurmLogo from "../assets/slurm-logo.svg";
|
||||
import thinkingMachinesLogo from "../assets/thinking-machines.png";
|
||||
import { MONO_CLASS_NAME } from "../styleClasses";
|
||||
import { m } from "../paraglide/messages.js";
|
||||
|
||||
/** Human name for a backend kind, used as the logo's alt/label. */
|
||||
function backendName(kind: string): string {
|
||||
@@ -25,7 +26,7 @@ function backendName(kind: string): string {
|
||||
case "openresearch_job":
|
||||
return "OpenResearch";
|
||||
case "local_job":
|
||||
return "This machine";
|
||||
return m.compute_target_local();
|
||||
case "tinker_job":
|
||||
return "Tinker";
|
||||
default:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getExperimentDiff, type DiffPayload, type Experiment } from "../api";
|
||||
import { GitDiffExplorer, TruncatedDiffNotice } from "./GitDiff";
|
||||
@@ -38,14 +40,14 @@ export function BranchChanges({
|
||||
return (
|
||||
<div className={`${CODE_TAB_BODY_CLASS_NAME} branch-changes [&_>_.changes-note]:my-3.5 [&_>_.changes-note]:mx-4 [&_>_.openresearch-diff]:mt-3.5 [&_>_.openresearch-diff]:mx-4 [&_>_.openresearch-diff]:mb-0 [&_>_.truncated-notice]:mt-3.5 [&_>_.truncated-notice]:mx-4 [&_>_.truncated-notice]:mb-0 [&_>_.diff-explorer]:mt-3.5 [&_>_.diff-explorer]:mx-4 [&_>_.diff-explorer]:mb-0`}>
|
||||
{error ? (
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>Failed to load changes: {error}</div>
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>{m.branch_changes_failed_to_load_changes()} {ltr(error)}</div>
|
||||
) : !diff ? (
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>Loading changes…</div>
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>{m.branch_changes_loading_changes()}</div>
|
||||
) : !diff.diff.trim() ? (
|
||||
<div className="changes-note text-sm text-muted">
|
||||
{experiment.parentExperimentId
|
||||
? "No committed changes from the parent branch."
|
||||
: "This is the baseline branch, so there is no parent comparison."}
|
||||
? m.branch_no_committed_changes()
|
||||
: m.branch_baseline_no_parent()}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { githubBranchUrl } from "../api";
|
||||
import { ltr } from "../i18n";
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { GitHubMark } from "./BackendLogos";
|
||||
|
||||
const FILES_PILL_CLASS_NAME = [
|
||||
@@ -29,7 +31,7 @@ export function BranchPill({
|
||||
href={githubBranchUrl(owner, repo, branch)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={`Open ${branch} on GitHub`}
|
||||
title={m.a11y_open_on_github({ name: ltr(branch) })}
|
||||
>
|
||||
<code>{branch}</code>
|
||||
<GitHubMark size={12} />
|
||||
|
||||
+324
-406
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
// Italic glyphs lean past the label's clip box, so pad the visible span and the
|
||||
// invisible width reserve alike — `italic` itself inherits into the ::after.
|
||||
const PREVIEW_CLASS_NAME = "italic [&_.tab-label_>_span]:pr-1 [&_.tab-label::after]:pr-1";
|
||||
const PREVIEW_CLASS_NAME = "italic [&_.tab-label_>_span]:pe-1 [&_.tab-label::after]:pe-1";
|
||||
|
||||
/** A closable tab in the right panel's tab strip (open experiments / files).
|
||||
* The close "x" is a span, not a button — it can't nest inside the tab button. */
|
||||
@@ -31,13 +32,13 @@ export function ClosableTab({
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className={`tab [&.closable]:max-w-60 [&.closable]:pr-0.5 [&_.tab-label]:grid [&_.tab-label]:grid-cols-[minmax(0,_1fr)] [&_.tab-label]:min-w-0 [&_.tab-label]:overflow-hidden [&_.tab-label_>_span]:[grid-area:1_/_1] [&_.tab-label_>_span]:overflow-hidden [&_.tab-label_>_span]:text-ellipsis [&_.tab-label_>_span]:whitespace-nowrap [&_.tab-label::after]:[grid-area:1_/_1] [&_.tab-label::after]:overflow-hidden [&_.tab-label::after]:text-ellipsis [&_.tab-label::after]:whitespace-nowrap [&_.tab-label::after]:content-[attr(data-label)] [&_.tab-label::after]:invisible [&_.tab-label::after]:font-medium [&_.tab-close]:inline-flex [&_.tab-close]:items-center [&_.tab-close]:justify-center [&_.tab-close]:w-3.5 [&_.tab-close]:h-3.5 [&_.tab-close]:rounded-xs [&_.tab-close]:text-muted [&_.tab-close]:shrink-0 [&_.tab-close:hover]:bg-[color-mix(in_oklab,_var(--text)_15%,_transparent)] [&_.tab-close:hover]:text-text relative inline-flex items-center gap-[5px] h-8 py-0 px-2 border border-transparent border-b-0 rounded-[var(--radius-md)_var(--radius-md)_0_0] text-sm font-normal text-subtext whitespace-nowrap select-none min-w-24 [&:hover]:bg-surface [&:hover]:text-text [&:not(.active)_+_.tab:not(.active)::before]:content-[''] [&:not(.active)_+_.tab:not(.active)::before]:absolute [&:not(.active)_+_.tab:not(.active)::before]:top-2.5 [&:not(.active)_+_.tab:not(.active)::before]:bottom-2.5 [&:not(.active)_+_.tab:not(.active)::before]:-left-px [&:not(.active)_+_.tab:not(.active)::before]:w-px [&:not(.active)_+_.tab:not(.active)::before]:bg-border [&.active]:border-border [&.active]:bg-background [&.active]:text-text [&.active]:font-medium [&.active::after]:content-[''] [&.active::after]:absolute [&.active::after]:right-0 [&.active::after]:-bottom-px [&.active::after]:left-0 [&.active::after]:h-px [&.active::after]:bg-background closable ${active ? "active" : ""} ${preview ? PREVIEW_CLASS_NAME : ""}`}
|
||||
className={`tab [&.closable]:max-w-60 [&.closable]:pe-0.5 [&_.tab-label]:grid [&_.tab-label]:grid-cols-[minmax(0,_1fr)] [&_.tab-label]:min-w-0 [&_.tab-label]:overflow-hidden [&_.tab-label_>_span]:[grid-area:1_/_1] [&_.tab-label_>_span]:overflow-hidden [&_.tab-label_>_span]:text-ellipsis [&_.tab-label_>_span]:whitespace-nowrap [&_.tab-label::after]:[grid-area:1_/_1] [&_.tab-label::after]:overflow-hidden [&_.tab-label::after]:text-ellipsis [&_.tab-label::after]:whitespace-nowrap [&_.tab-label::after]:content-[attr(data-label)] [&_.tab-label::after]:invisible [&_.tab-label::after]:font-medium [&_.tab-close]:inline-flex [&_.tab-close]:items-center [&_.tab-close]:justify-center [&_.tab-close]:w-3.5 [&_.tab-close]:h-3.5 [&_.tab-close]:rounded-xs [&_.tab-close]:text-muted [&_.tab-close]:shrink-0 [&_.tab-close:hover]:bg-[color-mix(in_oklab,_var(--text)_15%,_transparent)] [&_.tab-close:hover]:text-text relative inline-flex items-center gap-[5px] h-8 py-0 px-2 border border-transparent border-b-0 rounded-[var(--radius-md)_var(--radius-md)_0_0] text-sm font-normal text-subtext whitespace-nowrap select-none min-w-24 [&:hover]:bg-surface [&:hover]:text-text [&:not(.active)_+_.tab:not(.active)::before]:content-[''] [&:not(.active)_+_.tab:not(.active)::before]:absolute [&:not(.active)_+_.tab:not(.active)::before]:top-2.5 [&:not(.active)_+_.tab:not(.active)::before]:bottom-2.5 [&:not(.active)_+_.tab:not(.active)::before]:-start-px [&:not(.active)_+_.tab:not(.active)::before]:w-px [&:not(.active)_+_.tab:not(.active)::before]:bg-border [&.active]:border-border [&.active]:bg-background [&.active]:text-text [&.active]:font-medium [&.active::after]:content-[''] [&.active::after]:absolute [&.active::after]:end-0 [&.active::after]:-bottom-px [&.active::after]:start-0 [&.active::after]:h-px [&.active::after]:bg-background closable ${active ? "active" : ""} ${preview ? PREVIEW_CLASS_NAME : ""}`}
|
||||
onClick={onSelect}
|
||||
onDoubleClick={onPromote}
|
||||
title={preview ? `${label} (double-click or ⌘/Ctrl+K Enter to keep open)` : label}
|
||||
title={preview ? m.tab_preview_title({ label }) : label}
|
||||
aria-label={
|
||||
preview
|
||||
? `${label} (preview; double-click or Command/Control K, then Enter to keep open)`
|
||||
? m.tab_preview_aria({ label })
|
||||
: label
|
||||
}
|
||||
>
|
||||
@@ -48,7 +49,7 @@ export function ClosableTab({
|
||||
<span
|
||||
role="button"
|
||||
className="tab-close"
|
||||
title="Close tab"
|
||||
title={m.closable_tab_close_tab()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { GitBranch, RotateCw } from "lucide-react";
|
||||
import { GitHubMark } from "./BackendLogos";
|
||||
import { ICON_BUTTON_CLASS_NAME, SPINNER_CLASS_NAME } from "../styleClasses";
|
||||
@@ -28,14 +29,14 @@ export function CodeBrowserHeader({
|
||||
return (
|
||||
<div className="code-tab-header flex items-center gap-2 py-1.5 px-3 border-b border-b-border-variant shrink-0 [&_>_.seg]:p-0.5 [&_>_.seg]:rounded-sm [&_>_.seg_button]:py-0.5 [&_>_.seg_button]:px-2 [&_>_.seg_button]:text-sm [&_>_.seg_button]:font-medium">
|
||||
{showViewToggle && (
|
||||
<div className="seg inline-flex items-center gap-0.5 p-[3px] rounded-md bg-[color-mix(in_oklab,_var(--text)_10%,_transparent)] [&_button]:py-[3px] [&_button]:px-3 [&_button]:text-md [&_button]:font-semibold [&_button]:text-text [&_button]:rounded-sm [&_button:not(:disabled):hover]:text-text [&_button.active]:bg-background [&_button.active]:shadow-[0_1px_3px_color-mix(in_oklab,_var(--text)_25%,_transparent)] [&_button:disabled]:text-muted [&_button:disabled]:cursor-default" role="group" aria-label="Code browser view">
|
||||
<div className="seg inline-flex items-center gap-0.5 p-[3px] rounded-md bg-[color-mix(in_oklab,_var(--text)_10%,_transparent)] [&_button]:py-[3px] [&_button]:px-3 [&_button]:text-md [&_button]:font-semibold [&_button]:text-text [&_button]:rounded-sm [&_button:not(:disabled):hover]:text-text [&_button.active]:bg-background [&_button.active]:shadow-[0_1px_3px_color-mix(in_oklab,_var(--text)_25%,_transparent)] [&_button:disabled]:text-muted [&_button:disabled]:cursor-default" role="group" aria-label={m.code_browser_header_code_browser_view()}>
|
||||
<button
|
||||
type="button"
|
||||
className={view === "files" ? "active" : ""}
|
||||
aria-pressed={view === "files"}
|
||||
onClick={() => onViewChange("files")}
|
||||
>
|
||||
Files
|
||||
{m.code_browser_header_files()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -43,7 +44,7 @@ export function CodeBrowserHeader({
|
||||
aria-pressed={view === "changes"}
|
||||
onClick={() => onViewChange("changes")}
|
||||
>
|
||||
Changes
|
||||
{m.code_browser_header_changes()}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -66,7 +67,7 @@ export function CodeBrowserHeader({
|
||||
</a>
|
||||
)}
|
||||
<span style={{ flex: 1 }} />
|
||||
<button className={ICON_BUTTON_CLASS_NAME} title="Refresh" aria-label="Refresh" onClick={onRefresh}>
|
||||
<button className={ICON_BUTTON_CLASS_NAME} title={m.code_browser_header_refresh()} aria-label={m.code_browser_header_refresh()} onClick={onRefresh}>
|
||||
{refreshing ? <span className={SPINNER_CLASS_NAME} /> : <RotateCw size={13} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -102,12 +102,12 @@ export function CodeEditor({
|
||||
|
||||
// Both layers must reserve the scrollbar, or the textarea is narrower than the
|
||||
// overlay on classic-scrollbar platforms and the two wrap at different columns.
|
||||
const layerClassName = `absolute inset-0 m-0 py-3.5 pr-4 ${CODE_TEXT_CLASS_NAME} ${CODE_WRAP_CLASS_NAME} [scrollbar-gutter:stable]`;
|
||||
const layerClassName = `absolute inset-0 m-0 py-3.5 pe-4 ${CODE_TEXT_CLASS_NAME} ${CODE_WRAP_CLASS_NAME} [scrollbar-gutter:stable]`;
|
||||
|
||||
return (
|
||||
<div className={`file-view-editwrap relative h-full min-h-0 ${CODE_TEXT_CLASS_NAME}`}>
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 border-r border-r-border-variant pointer-events-none"
|
||||
className="absolute start-0 top-0 bottom-0 border-e border-e-border-variant pointer-events-none"
|
||||
style={{ width: `${ruleCh}ch` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -122,10 +122,10 @@ export function CodeEditor({
|
||||
/* The scroll target for `file:line` — measured, so it's on the row. */
|
||||
data-line={i + 1}
|
||||
className="relative"
|
||||
style={{ paddingLeft: `${codeCh}ch` }}
|
||||
style={{ paddingInlineStart: `${codeCh}ch` }}
|
||||
>
|
||||
<span
|
||||
className={`${CODE_GUTTER_CLASS_NAME} absolute left-0 pr-[1ch]`}
|
||||
className={`${CODE_GUTTER_CLASS_NAME} absolute start-0 pe-[1ch]`}
|
||||
style={{ width: `${ruleCh}ch` }}
|
||||
>
|
||||
{i + 1}
|
||||
@@ -138,7 +138,7 @@ export function CodeEditor({
|
||||
<textarea
|
||||
ref={taRef}
|
||||
className={`file-view-editarea ${layerClassName} overflow-y-auto overflow-x-hidden resize-none border-0 bg-transparent text-transparent caret-[var(--text)] outline-none`}
|
||||
style={{ paddingLeft: `${codeCh}ch` }}
|
||||
style={{ paddingInlineStart: `${codeCh}ch` }}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onScroll={syncScroll}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
// Files and committed changes for one experiment branch. The opening
|
||||
// experiment fixes the Git source; users only switch between Files/Changes.
|
||||
|
||||
@@ -143,7 +145,7 @@ export function CodeTab({
|
||||
? githubBranchUrl(project.githubOwner, project.githubRepo, branch)
|
||||
: undefined
|
||||
}
|
||||
githubTitle={`Open ${branch} on GitHub`}
|
||||
githubTitle={m.a11y_open_branch_github({ branch: ltr(branch) })}
|
||||
refreshing={refreshing}
|
||||
onRefresh={() =>
|
||||
view === "files" ? load() : setChangesRefreshKey((current) => current + 1)
|
||||
@@ -158,15 +160,15 @@ export function CodeTab({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{data?.truncated && <div className={CODE_TAB_NOTE_CLASS_NAME}>listing truncated</div>}
|
||||
{error && tree && <div className={CODE_TAB_NOTE_CLASS_NAME}>Refresh failed: {error}</div>}
|
||||
{data?.truncated && <div className={CODE_TAB_NOTE_CLASS_NAME}>{m.code_tab_listing_truncated()}</div>}
|
||||
{error && tree && <div className={CODE_TAB_NOTE_CLASS_NAME}>{m.code_tab_refresh_failed()} {ltr(error)}</div>}
|
||||
<div className={CODE_TAB_BODY_CLASS_NAME}>
|
||||
{!tree ? (
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>
|
||||
{error ? `Failed to load: ${error}` : "Loading…"}
|
||||
{error ? m.common_failed_to_load({ error: ltr(error) }) : m.common_loading()}
|
||||
</div>
|
||||
) : tree.dirs.size === 0 && tree.files.length === 0 ? (
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>No files.</div>
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>{m.code_tab_no_files()}</div>
|
||||
) : (
|
||||
<div className="file-tree py-1.5 px-0 text-md">
|
||||
<TreeLevel
|
||||
|
||||
@@ -75,12 +75,12 @@ export function CodeView({
|
||||
code never picks it up. */}
|
||||
<span
|
||||
data-line={i + 1}
|
||||
className={`${CODE_GUTTER_CLASS_NAME} before:content-[attr(data-line)] shrink-0 pr-[1ch]`}
|
||||
className={`${CODE_GUTTER_CLASS_NAME} before:content-[attr(data-line)] shrink-0 pe-[1ch]`}
|
||||
style={{ width: `${ruleCh}ch` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<code
|
||||
className={`file-view-code flex-1 min-w-0 pl-[2ch] pr-4 ${CODE_TEXT_CLASS_NAME} ${CODE_WRAP_CLASS_NAME}`}
|
||||
className={`file-view-code flex-1 min-w-0 ps-[2ch] pe-4 ${CODE_TEXT_CLASS_NAME} ${CODE_WRAP_CLASS_NAME}`}
|
||||
>
|
||||
{/* An empty <code> serializes to nothing, dropping blank lines
|
||||
from a copied selection. */}
|
||||
@@ -95,7 +95,7 @@ export function CodeView({
|
||||
<div className={`file-view-codewrap relative py-3.5 ${CODE_TEXT_CLASS_NAME}`}>
|
||||
{lines.length > 0 && (
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 border-r border-r-border-variant pointer-events-none"
|
||||
className="absolute start-0 top-0 bottom-0 border-e border-e-border-variant pointer-events-none"
|
||||
style={{ width: `${ruleCh}ch` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
import { getLocale } from "../paraglide/runtime.js";
|
||||
import { fmtTokens, type ContextUsage } from "../api";
|
||||
import {
|
||||
COMPOSER_CONTROL_CLASS_NAME,
|
||||
@@ -33,13 +36,16 @@ function VisibleContextMeter({ usage }: { usage: ContextUsage }) {
|
||||
? Math.min(100, Math.round((usedTokens / contextWindow) * 100))
|
||||
: null;
|
||||
const fill = pct === null ? "var(--accent)" : tone(pct);
|
||||
const percent = pct === null
|
||||
? ""
|
||||
: new Intl.NumberFormat(getLocale(), { style: "percent" }).format(pct / 100);
|
||||
|
||||
return (
|
||||
<div className="option-picker relative inline-flex shrink-0" ref={ref}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${pct === null ? `${COMPOSER_CONTROL_CLASS_NAME} px-1` : COMPOSER_ICON_CONTROL_CLASS_NAME} composer-bare context-ring text-md text-text`}
|
||||
title="Context window used"
|
||||
title={m.context_meter_context_window_used()}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
{pct === null ? (
|
||||
@@ -69,13 +75,13 @@ function VisibleContextMeter({ usage }: { usage: ContextUsage }) {
|
||||
)}
|
||||
</button>
|
||||
{open && (
|
||||
<div className="option-menu absolute bottom-[calc(100%_+_8px)] left-0 max-h-95 flex flex-col bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] z-50 overflow-hidden min-w-47.5 [&.align-right]:left-auto [&.align-right]:right-0 [&.drop-down]:bottom-auto [&.drop-down]:top-[calc(100%_+_4px)] [&.session-menu]:left-auto [&.session-menu]:right-1.5 [&.session-menu]:top-[calc(100%_-_2px)] [&.session-menu]:min-w-35 align-right context-meter-menu w-70 pt-2.5 px-3 pb-3 [&_.progress]:mt-2 [&_.progress]:mx-0 [&_.progress]:mb-0 [&_.progress-track]:h-[5px] [&_.progress-track]:border-0 [&_.progress-track]:bg-border">
|
||||
<div className="option-menu absolute bottom-[calc(100%_+_8px)] start-0 max-h-95 flex flex-col bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] z-50 overflow-hidden min-w-47.5 [&.align-right]:start-auto [&.align-right]:end-0 [&.drop-down]:bottom-auto [&.drop-down]:top-[calc(100%_+_4px)] [&.session-menu]:start-auto [&.session-menu]:end-1.5 [&.session-menu]:top-[calc(100%_-_2px)] [&.session-menu]:min-w-35 align-right context-meter-menu w-70 pt-2.5 px-3 pb-3 [&_.progress]:mt-2 [&_.progress]:mx-0 [&_.progress]:mb-0 [&_.progress-track]:h-[5px] [&_.progress-track]:border-0 [&_.progress-track]:bg-border">
|
||||
<div className="context-meter-head flex justify-between items-baseline gap-3 text-sm text-muted">
|
||||
<span>Context window</span>
|
||||
<span>{m.context_meter_context_window()}</span>
|
||||
<span className="context-meter-value text-text tabular-nums">
|
||||
{pct === null
|
||||
? `${fmtTokens(usedTokens)} tokens`
|
||||
: `${fmtTokens(usedTokens)} / ${fmtTokens(contextWindow!)} (${pct}%)`}
|
||||
? m.context_meter_tokens({ value: ltr(fmtTokens(usedTokens)) })
|
||||
: m.context_meter_usage({ used: ltr(fmtTokens(usedTokens)), total: ltr(fmtTokens(contextWindow!)), percent: ltr(percent) })}
|
||||
</span>
|
||||
</div>
|
||||
{pct !== null && (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ChevronDown, CircleStop } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
@@ -17,7 +18,7 @@ import type { TabOpenIntent } from "../tabPreview";
|
||||
|
||||
export type ExperimentView = "overview" | "terminal";
|
||||
|
||||
/** An experiment's detail view, rendered as right-pane tab content. Mount it
|
||||
/** An experiment's detail view, rendered as end-pane tab content. Mount it
|
||||
* keyed by `${experiment.id}:${view}` so per-view state resets on switch. */
|
||||
export function DetailDrawer({
|
||||
experiment,
|
||||
@@ -151,34 +152,34 @@ function TerminalView({
|
||||
{live && (
|
||||
<button className={`${SMALL_BUTTON_CLASS_NAME} ghost`} disabled={cancelling} onClick={() => void stop()}>
|
||||
<CircleStop size={13} />
|
||||
{cancelling ? "Cancelling…" : "Stop"}
|
||||
{cancelling ? m.common_cancelling() : m.common_stop()}
|
||||
</button>
|
||||
)}
|
||||
{expRuns.length > 0 && selectedRun && (
|
||||
<div className="run-history relative shrink-0" ref={historyRef}>
|
||||
<button
|
||||
className="run-picker inline-flex items-center gap-2 pt-1 pr-1.5 pb-1 pl-2.5 border border-border rounded-md bg-background text-text [&:hover]:bg-surface [&_.run-label]:text-sm [&_.run-label]:font-semibold"
|
||||
title="Switch run"
|
||||
className="run-picker inline-flex items-center gap-2 pt-1 pe-1.5 pb-1 ps-2.5 border border-border rounded-md bg-background text-text [&:hover]:bg-surface [&_.run-label]:text-sm [&_.run-label]:font-semibold"
|
||||
title={m.detail_drawer_switch_run()}
|
||||
onClick={() => setHistoryOpen((v) => !v)}
|
||||
>
|
||||
<span className="run-label">Run {runNumber(selectedRun.id)}</span>
|
||||
<span className="run-label">{m.detail_drawer_run()} {runNumber(selectedRun.id)}</span>
|
||||
<StatusBadge
|
||||
status={cancelling ? "cancelling" : runDisplayStatus(selectedRun)}
|
||||
/>
|
||||
<ChevronDown size={14} className="run-picker-chev text-muted shrink-0" />
|
||||
</button>
|
||||
{historyOpen && (
|
||||
<div className="history-menu absolute top-[calc(100%_+_6px)] right-0 min-w-57.5 max-h-80 overflow-y-auto bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] p-[5px] z-50">
|
||||
<div className="history-menu absolute top-[calc(100%_+_6px)] end-0 min-w-57.5 max-h-80 overflow-y-auto bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] p-[5px] z-50">
|
||||
{expRuns.map((r) => (
|
||||
<button
|
||||
key={r.id}
|
||||
className={`history-item flex items-center gap-2 w-full text-left py-1.5 px-2 text-sm rounded-sm [&:hover]:bg-surface [&.active]:bg-surface [&_.run-label]:font-semibold [&_.when]:ml-auto [&_.when]:text-xs [&_.when]:text-muted ${r.id === selectedRun?.id ? "active" : ""}`}
|
||||
className={`history-item flex items-center gap-2 w-full text-start py-1.5 px-2 text-sm rounded-sm [&:hover]:bg-surface [&.active]:bg-surface [&_.run-label]:font-semibold [&_.when]:ms-auto [&_.when]:text-xs [&_.when]:text-muted ${r.id === selectedRun?.id ? "active" : ""}`}
|
||||
onClick={() => {
|
||||
onSelectRun(r.id);
|
||||
setHistoryOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="run-label">Run {runNumber(r.id)}</span>
|
||||
<span className="run-label">{m.detail_drawer_run()} {runNumber(r.id)}</span>
|
||||
<StatusBadge status={runDisplayStatus(r)} />
|
||||
<span className="when">{timeAgo(r.createdAt)}</span>
|
||||
</button>
|
||||
@@ -189,13 +190,13 @@ function TerminalView({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="term-fill flex-1 min-h-0 bg-[var(--term-bg)] pt-1 pr-0 pb-1 pl-1.5">
|
||||
<div className="term-fill flex-1 min-h-0 bg-[var(--term-bg)] pt-1 pe-0 pb-1 ps-1.5">
|
||||
{selectedRun ? (
|
||||
// Key by run id so switching runs in the history dropdown remounts
|
||||
// the terminal with the selected run's output.
|
||||
<LogTerminal key={selectedRun.id} runId={selectedRun.id} />
|
||||
) : (
|
||||
<div className="term-empty h-full flex items-center justify-center p-6 text-center text-md text-muted">No runs yet — ask the agent to launch one.</div>
|
||||
<div className="term-empty h-full flex items-center justify-center p-6 text-center text-md text-muted">{m.detail_drawer_no_runs_yet_ask_the_agent_to_launch()}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { getLocale } from "../paraglide/runtime.js";
|
||||
import { ltr } from "../i18n";
|
||||
// Floating detail card shown while hovering an experiment node in the tree.
|
||||
// Rendered through a portal at a fixed viewport position (never inside the
|
||||
// ReactFlow node: the canvas transform would scale it with zoom, and growing
|
||||
@@ -13,6 +16,7 @@ import { parseDiff, type FileData } from "react-diff-view";
|
||||
import {
|
||||
backendKind,
|
||||
fmtDuration,
|
||||
fmtNumber,
|
||||
getRunDiff,
|
||||
runDisplayStatus,
|
||||
timeAgo,
|
||||
@@ -110,7 +114,7 @@ function fmtCreated(ms: number): string {
|
||||
d.getFullYear() === new Date().getFullYear()
|
||||
? { month: "short", day: "numeric" }
|
||||
: { month: "short", day: "numeric", year: "numeric" };
|
||||
return d.toLocaleDateString(undefined, opts);
|
||||
return d.toLocaleDateString(getLocale(), opts);
|
||||
}
|
||||
|
||||
export function ExpHoverCard({
|
||||
@@ -265,7 +269,7 @@ export function ExpHoverCard({
|
||||
{...tabOpenGestureHandlers<HTMLButtonElement>(onOpenLogs)}
|
||||
>
|
||||
<Terminal size={13} />
|
||||
Logs
|
||||
{m.exp_hover_card_logs()}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -273,7 +277,7 @@ export function ExpHoverCard({
|
||||
{...tabOpenGestureHandlers<HTMLButtonElement>(onOpenCode)}
|
||||
>
|
||||
<FolderTree size={13} />
|
||||
Code
|
||||
{m.exp_hover_card_code()}
|
||||
</button>
|
||||
</div>
|
||||
{body && (
|
||||
@@ -283,17 +287,19 @@ export function ExpHoverCard({
|
||||
)}
|
||||
{body && (clamped || expanded) && (
|
||||
<button type="button" className="hc-toggle" onClick={() => setExpanded((v) => !v)}>
|
||||
{expanded ? "Show less" : "Show more"}
|
||||
{expanded ? m.common_show_less() : m.common_show_more()}
|
||||
</button>
|
||||
)}
|
||||
{failureNote && <div className="hc-failure">{failureNote}</div>}
|
||||
<div className="hc-stats">
|
||||
<span>
|
||||
{runs.length === 1 ? "1 run" : `${runs.length} runs`}
|
||||
{counts.done > 0 && ` · ${counts.done} done`}
|
||||
{counts.failed > 0 && ` · ${counts.failed} failed`}
|
||||
{counts.cancelled > 0 && ` · ${counts.cancelled} cancelled`}
|
||||
{counts.live > 0 && ` · ${counts.live} live`}
|
||||
{new Intl.ListFormat(getLocale(), { style: "short" }).format([
|
||||
runs.length === 1 ? m.hover_one_run() : m.hover_run_count({ count: fmtNumber(runs.length) }),
|
||||
...(counts.done > 0 ? [m.hover_done_count({ count: fmtNumber(counts.done) })] : []),
|
||||
...(counts.failed > 0 ? [m.hover_failed_count({ count: fmtNumber(counts.failed) })] : []),
|
||||
...(counts.cancelled > 0 ? [m.hover_cancelled_count({ count: fmtNumber(counts.cancelled) })] : []),
|
||||
...(counts.live > 0 ? [m.hover_live_count({ count: fmtNumber(counts.live) })] : []),
|
||||
])}
|
||||
</span>
|
||||
{latestRun && backendKind(latestRun.backend) && <BackendBadge backend={latestRun.backend} />}
|
||||
{duration && <span>{duration}</span>}
|
||||
@@ -307,14 +313,16 @@ export function ExpHoverCard({
|
||||
</span>
|
||||
{parentSlug && (
|
||||
<span>
|
||||
from <span className="hc-mono">{parentSlug}</span>
|
||||
{m.exp_hover_card_from()} <span className="hc-mono">{parentSlug}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{diffStat && diffStat.fileCount > 0 && (
|
||||
<div
|
||||
className="hc-git-row"
|
||||
title={`Committed changes vs ${parentSlug ?? "parent"}${diffStat.truncated ? " (diff truncated — counts are lower bounds)" : ""}`}
|
||||
title={diffStat.truncated
|
||||
? m.a11y_committed_changes_truncated({ parent: ltr(parentSlug ?? "parent") })
|
||||
: m.a11y_committed_changes({ parent: ltr(parentSlug ?? "parent") })}
|
||||
>
|
||||
<span>
|
||||
{diffStat.truncated && "≥ "}
|
||||
@@ -322,15 +330,15 @@ export function ExpHoverCard({
|
||||
<span className="diff-stat-del text-accent-red">−{diffStat.deletions}</span>
|
||||
{" · "}
|
||||
{diffStat.fileCount === 1 && !diffStat.truncated
|
||||
? "1 file"
|
||||
: `${diffStat.fileCount}${diffStat.truncated ? "+" : ""} files`}
|
||||
? m.hover_one_file()
|
||||
: diffStat.truncated ? m.hover_files_at_least({ count: fmtNumber(diffStat.fileCount) }) : m.hover_file_count({ count: fmtNumber(diffStat.fileCount) })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="hc-foot">
|
||||
<span className="hc-mono">$ {exp.runCommand}</span>
|
||||
<span>created {fmtCreated(exp.createdAt)}</span>
|
||||
<span>{m.exp_hover_card_created()} {fmtCreated(exp.createdAt)}</span>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { getLocale } from "../paraglide/runtime.js";
|
||||
import {
|
||||
CalendarDays,
|
||||
Clock3,
|
||||
@@ -41,7 +43,7 @@ const EXPERIMENT_OVERVIEW_COMMAND_CLASS_NAME = [
|
||||
].join(" ");
|
||||
|
||||
function fmtDate(ms: number): string {
|
||||
return new Date(ms).toLocaleString(undefined, {
|
||||
return new Date(ms).toLocaleString(getLocale(), {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
@@ -102,7 +104,7 @@ export function ExperimentOverview({
|
||||
)}
|
||||
>
|
||||
<Terminal size={15} />
|
||||
Logs
|
||||
{m.experiment_overview_logs()}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -110,34 +112,34 @@ export function ExperimentOverview({
|
||||
{...tabOpenGestureHandlers<HTMLButtonElement>(onOpenCode)}
|
||||
>
|
||||
<FolderTree size={15} />
|
||||
Code
|
||||
{m.experiment_overview_code()}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{experiment.description && (
|
||||
<section className="experiment-overview-section mt-5.5 pt-4.5 border-t border-t-border-variant [&_h2]:mt-0 [&_h2]:mx-0 [&_h2]:mb-3.5 [&_h2]:text-text [&_h2]:text-md [&_h2]:font-semibold overview-description [&_.md]:text-text [&_.md]:leading-[1.65]">
|
||||
<h2>Description</h2>
|
||||
<h2>{m.experiment_overview_description()}</h2>
|
||||
<Md text={experiment.description} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className={EXPERIMENT_OVERVIEW_SECTION_CLASS_NAME}>
|
||||
<h2>{latestRun ? "Latest run" : "Runs"}</h2>
|
||||
<h2>{latestRun ? m.experiment_latest_run() : m.experiment_runs()}</h2>
|
||||
{latestRun && (
|
||||
<>
|
||||
<div className="experiment-overview-meta flex items-center flex-wrap gap-y-2.5 gap-x-4.5 text-text text-sm [&_svg]:text-muted [&_.backend-badge]:text-text [&_.status-badge]:text-text [&_>_span]:inline-flex [&_>_span]:items-center [&_>_span]:gap-[5px] [&_code]:text-text [&_code]:text-xs">
|
||||
<StatusBadge status={runDisplayStatus(latestRun)} />
|
||||
<BackendBadge backend={latestRun.backend} />
|
||||
<span title="Started">
|
||||
<span title={m.experiment_overview_started()}>
|
||||
<CalendarDays size={13} />
|
||||
{fmtDate(latestRun.createdAt)}
|
||||
</span>
|
||||
<span title="Duration">
|
||||
<span title={m.experiment_overview_duration()}>
|
||||
<Clock3 size={13} />
|
||||
{runDuration(latestRun, now)}
|
||||
</span>
|
||||
{latestRun.commitSha && (
|
||||
<span title="Commit">
|
||||
<span title={m.experiment_overview_commit()}>
|
||||
<GitCommitHorizontal size={14} />
|
||||
<code>{latestRun.commitSha.slice(0, 7)}</code>
|
||||
</span>
|
||||
@@ -145,7 +147,7 @@ export function ExperimentOverview({
|
||||
{latestRun.exitCode !== null &&
|
||||
latestRun.exitCode !== undefined &&
|
||||
latestRun.exitCode !== 0 && (
|
||||
<span>exit {latestRun.exitCode}</span>
|
||||
<span>{m.experiment_overview_exit()} {latestRun.exitCode}</span>
|
||||
)}
|
||||
</div>
|
||||
{latestRun.command && (
|
||||
@@ -172,11 +174,11 @@ export function ExperimentOverview({
|
||||
/>
|
||||
{parentExperiment && (
|
||||
<span>
|
||||
from <code>{parentExperiment.slug}</code>
|
||||
{m.experiment_overview_from()} <code>{parentExperiment.slug}</code>
|
||||
</span>
|
||||
)}
|
||||
<span title={fmtDate(experiment.createdAt)}>
|
||||
created {timeAgo(experiment.createdAt)}
|
||||
{m.experiment_overview_created()} {timeAgo(experiment.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
{experiment.runCommand !== latestRun?.command && (
|
||||
@@ -186,8 +188,8 @@ export function ExperimentOverview({
|
||||
|
||||
{runs.length > 0 && (
|
||||
<section className={EXPERIMENT_OVERVIEW_SECTION_CLASS_NAME}>
|
||||
<h2>Run history</h2>
|
||||
<div className="experiment-run-history border-t border-t-border-variant [&_button]:w-full [&_button]:grid [&_button]:grid-cols-[minmax(72px,_0.7fr)_minmax(100px,_1fr)_minmax(70px,_0.7fr)_60px_16px] [&_button]:items-center [&_button]:gap-3.5 [&_button]:py-[11px] [&_button]:px-0.5 [&_button]:border-b [&_button]:border-b-border-variant [&_button]:text-text [&_button]:text-left [&_button]:text-sm [&_button:hover]:bg-surface [@media((max-width:_720px))]:[&_button]:grid-cols-[65px_1fr_60px_16px] [@media((max-width:_720px))]:[&_button_>_:nth-child(3)]:hidden">
|
||||
<h2>{m.experiment_overview_run_history()}</h2>
|
||||
<div className="experiment-run-history border-t border-t-border-variant [&_button]:w-full [&_button]:grid [&_button]:grid-cols-[minmax(72px,_0.7fr)_minmax(100px,_1fr)_minmax(70px,_0.7fr)_60px_16px] [&_button]:items-center [&_button]:gap-3.5 [&_button]:py-[11px] [&_button]:px-0.5 [&_button]:border-b [&_button]:border-b-border-variant [&_button]:text-text [&_button]:text-start [&_button]:text-sm [&_button:hover]:bg-surface [@media((max-width:_720px))]:[&_button]:grid-cols-[65px_1fr_60px_16px] [@media((max-width:_720px))]:[&_button_>_:nth-child(3)]:hidden">
|
||||
{runs.map((run, index) => (
|
||||
<button
|
||||
key={run.id}
|
||||
@@ -195,7 +197,7 @@ export function ExperimentOverview({
|
||||
onOpenLogs(run.id, intent),
|
||||
)}
|
||||
>
|
||||
<span className="experiment-run-number font-mono text-xs font-semibold">Run {runs.length - index}</span>
|
||||
<span className="experiment-run-number font-mono text-xs font-semibold">{m.experiment_overview_run()} {runs.length - index}</span>
|
||||
<StatusBadge status={runDisplayStatus(run)} />
|
||||
<span>{timeAgo(run.createdAt)}</span>
|
||||
<span>{runDuration(run, now)}</span>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
import { CircleStop, FolderTree, GitBranch, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { runDisplayStatus, timeAgo, type Experiment, type Run } from "../api";
|
||||
import { fmtNumber, runDisplayStatus, timeAgo, type Experiment, type Run } from "../api";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
import { tabOpenGestureHandlers, type TabOpenIntent } from "../tabPreview";
|
||||
|
||||
@@ -15,7 +17,7 @@ const EXPERIMENT_TABLE_ACTION_CLASS_NAME = [
|
||||
"[&.danger]:bg-[color-mix(in_oklab,_var(--accent-red)_6%,_var(--base))]",
|
||||
"[&.danger]:text-accent-red [&.danger:hover:not(:disabled)]:border-accent-red",
|
||||
"[&.danger:hover:not(:disabled)]:bg-[color-mix(in_oklab,_var(--accent-red)_10%,_var(--base))]",
|
||||
"[@container((max-width:_560px))]:[&.danger]:ml-auto",
|
||||
"[@container((max-width:_560px))]:[&.danger]:ms-auto",
|
||||
].join(" ");
|
||||
|
||||
export function ExperimentsTable({
|
||||
@@ -56,7 +58,7 @@ export function ExperimentsTable({
|
||||
if (sortedExperiments.length === 0) {
|
||||
return (
|
||||
<div className="empty-state absolute inset-0 flex flex-col items-center justify-center gap-2.5 p-6 text-center text-subtext [&_p]:max-w-[46ch] [&_p]:m-0 [&_p]:leading-normal [&_p]:text-balance [&_p.empty-state-title]:text-2xl [&_p.empty-state-title]:font-normal [&_p.empty-state-title]:text-text [&_p.empty-state-hint]:text-lg [&_p.empty-state-hint]:text-subtext experiments-empty-state [&_p]:text-2xl">
|
||||
<p>{emptyHint ?? "No experiments yet."}</p>
|
||||
<p>{emptyHint ?? m.experiments_none_yet()}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -80,10 +82,10 @@ export function ExperimentsTable({
|
||||
<div className="experiments-table-wrap absolute inset-0 overflow-auto bg-background @container">
|
||||
{cancelError && (
|
||||
<div className="experiments-table-error py-2 px-3 text-accent-red text-sm border-b border-b-border" role="alert">
|
||||
Stop failed: {cancelError}
|
||||
{m.experiments_table_stop_failed()} {cancelError}
|
||||
</div>
|
||||
)}
|
||||
<div className="experiments-table w-full text-md bg-background" role="list" aria-label="Experiments">
|
||||
<div className="experiments-table w-full text-md bg-background" role="list" aria-label={m.experiments_table_experiments()}>
|
||||
{sortedExperiments.map((experiment) => {
|
||||
const experimentRuns = runsByExperiment.get(experiment.id) ?? [];
|
||||
const latestRun = experimentRuns[0] ?? null;
|
||||
@@ -118,7 +120,7 @@ export function ExperimentsTable({
|
||||
<div className="experiment-table-name [grid-area:name] self-start min-w-0">
|
||||
<button
|
||||
type="button"
|
||||
className="experiment-table-title block w-full overflow-hidden text-text font-semibold text-left text-ellipsis whitespace-nowrap"
|
||||
className="experiment-table-title block w-full overflow-hidden text-text font-semibold text-start text-ellipsis whitespace-nowrap"
|
||||
{...tabOpenGestureHandlers<HTMLButtonElement>((intent) =>
|
||||
onOpen(experiment, intent),
|
||||
{ stopPropagation: true })}
|
||||
@@ -135,18 +137,16 @@ export function ExperimentsTable({
|
||||
<StatusBadge status={status} />
|
||||
</div>
|
||||
<div className="experiment-run-summary flex items-center min-w-0 gap-2 text-subtext text-xs font-medium">
|
||||
<span>
|
||||
{experimentRuns.length} {experimentRuns.length === 1 ? "run" : "runs"}
|
||||
</span>
|
||||
<span>{experimentRuns.length === 1 ? m.experiments_one_run() : m.experiments_run_count({ count: fmtNumber(experimentRuns.length) })}</span>
|
||||
</div>
|
||||
<div className="experiment-table-latest flex items-center gap-1.5 min-w-0 text-subtext text-xs font-medium whitespace-nowrap">
|
||||
<span>{latestRun ? timeAgo(latestRun.createdAt) : "Not run yet"}</span>
|
||||
<span>{latestRun ? timeAgo(latestRun.createdAt) : m.experiments_not_run_yet()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="experiment-table-actions [grid-area:actions] flex flex-wrap items-center justify-start gap-2 mt-3"
|
||||
role="group"
|
||||
aria-label={`Actions for ${experiment.title || experiment.slug}`}
|
||||
aria-label={m.a11y_actions_for({ name: experiment.title || experiment.slug })}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onDoubleClick={(event) => event.stopPropagation()}
|
||||
onAuxClick={(event) => event.stopPropagation()}
|
||||
@@ -154,33 +154,33 @@ export function ExperimentsTable({
|
||||
<button
|
||||
className={EXPERIMENT_TABLE_ACTION_CLASS_NAME}
|
||||
disabled={!logsRun}
|
||||
title={logsRun ? "Open logs" : "No runs yet"}
|
||||
title={logsRun ? m.experiments_open_logs() : m.experiments_no_runs_yet()}
|
||||
{...tabOpenGestureHandlers<HTMLButtonElement>((intent) => {
|
||||
if (logsRun) onOpenLogs(experiment.id, logsRun.id, intent);
|
||||
}, { stopPropagation: true })}
|
||||
>
|
||||
<Terminal size={15} />
|
||||
Logs
|
||||
{m.experiments_table_logs()}
|
||||
</button>
|
||||
<button
|
||||
className={EXPERIMENT_TABLE_ACTION_CLASS_NAME}
|
||||
title={`Browse code on ${experiment.branchName}`}
|
||||
title={m.a11y_browse_code_on({ branch: ltr(experiment.branchName) })}
|
||||
{...tabOpenGestureHandlers<HTMLButtonElement>((intent) =>
|
||||
onOpenCode(experiment.id, intent),
|
||||
{ stopPropagation: true })}
|
||||
>
|
||||
<FolderTree size={15} />
|
||||
Code
|
||||
{m.experiments_table_code()}
|
||||
</button>
|
||||
{liveRun && (
|
||||
<button
|
||||
className="experiment-table-action inline-flex items-center gap-1.5 py-1.5 px-2.5 border border-border rounded-md bg-background text-text text-sm font-medium leading-none [&:hover:not(:disabled)]:bg-surface [&:hover:not(:disabled)]:border-border-strong [&:disabled]:text-muted [&:disabled]:cursor-default [&:disabled]:opacity-50 [&.danger]:border-[color-mix(in_oklab,_var(--accent-red)_42%,_var(--border))] [&.danger]:bg-[color-mix(in_oklab,_var(--accent-red)_6%,_var(--base))] [&.danger]:text-accent-red [&.danger:hover:not(:disabled)]:border-accent-red [&.danger:hover:not(:disabled)]:bg-[color-mix(in_oklab,_var(--accent-red)_10%,_var(--base))] [@container((max-width:_560px))]:[&.danger]:ml-auto danger"
|
||||
className="experiment-table-action inline-flex items-center gap-1.5 py-1.5 px-2.5 border border-border rounded-md bg-background text-text text-sm font-medium leading-none [&:hover:not(:disabled)]:bg-surface [&:hover:not(:disabled)]:border-border-strong [&:disabled]:text-muted [&:disabled]:cursor-default [&:disabled]:opacity-50 [&.danger]:border-[color-mix(in_oklab,_var(--accent-red)_42%,_var(--border))] [&.danger]:bg-[color-mix(in_oklab,_var(--accent-red)_6%,_var(--base))] [&.danger]:text-accent-red [&.danger:hover:not(:disabled)]:border-accent-red [&.danger:hover:not(:disabled)]:bg-[color-mix(in_oklab,_var(--accent-red)_10%,_var(--base))] [@container((max-width:_560px))]:[&.danger]:ms-auto danger"
|
||||
disabled={cancelling}
|
||||
title={cancelling ? "Stop requested" : "Stop run"}
|
||||
title={cancelling ? m.experiments_stop_requested() : m.experiments_stop_run()}
|
||||
onClick={() => void requestCancel(liveRun.id)}
|
||||
>
|
||||
<CircleStop size={15} />
|
||||
{cancelling ? "Stopping…" : "Stop"}
|
||||
{cancelling ? m.common_stopping() : m.common_stop()}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
// Mirror of openresearch.sh's AgentFileView: one file from the project —
|
||||
// a branch's committed copy when the tab carries a ref, else the chat
|
||||
// session's worktree, else the hub clone, else the project's artifacts, with
|
||||
@@ -101,12 +103,12 @@ function CopyableCommand({ command }: { command: string }) {
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
data-tip={
|
||||
state === "copied"
|
||||
? "Copied"
|
||||
? m.common_copied()
|
||||
: state === "select"
|
||||
? "Selected — press ⌘C"
|
||||
: "Copy command"
|
||||
? m.file_viewer_selected_copy_shortcut()
|
||||
: m.file_viewer_copy_command()
|
||||
}
|
||||
aria-label="Copy install command"
|
||||
aria-label={m.file_viewer_copy_install_command()}
|
||||
onClick={() => void copy()}
|
||||
>
|
||||
{state === "copied" ? <Check size={13} /> : <Copy size={13} />}
|
||||
@@ -309,14 +311,14 @@ export function FileViewer({
|
||||
if (overleafConflicts > 0) setShowOverleaf(true);
|
||||
}, [overleafConflicts]);
|
||||
const overleafTip = overleaf.error
|
||||
? "Overleaf sync failed"
|
||||
? m.overleaf_sync_failed()
|
||||
: overleafConflicts > 0
|
||||
? "Changed here and on Overleaf — choose which copy to keep"
|
||||
? m.overleaf_conflict_tip()
|
||||
: overleaf.blocked
|
||||
? "Save this file to sync it with Overleaf"
|
||||
? m.overleaf_save_to_sync()
|
||||
: overleaf.link
|
||||
? "In step with Overleaf"
|
||||
: "Send this paper to Overleaf";
|
||||
? m.overleaf_in_sync()
|
||||
: m.overleaf_send_paper();
|
||||
const overleafColor =
|
||||
overleaf.error || overleafConflicts > 0
|
||||
? "text-accent-red"
|
||||
@@ -459,16 +461,14 @@ export function FileViewer({
|
||||
}, [data]);
|
||||
|
||||
const notFoundCopy = (d: LoadedFile) => {
|
||||
if (d.source === "absolute") return "File not found on disk.";
|
||||
if (d.source === "absolute") return m.file_viewer_not_found_on_disk();
|
||||
if (isArtifacts)
|
||||
return `File not found in the project's artifacts or the ${
|
||||
sessionId ? "session's worktree" : "project clone"
|
||||
}.`;
|
||||
if (gitRef) return `File not found on branch ${gitRef}.`;
|
||||
return m.file_viewer_not_found_artifacts_or_root({ root: sessionId ? m.file_viewer_session_worktree() : m.file_viewer_project_clone() });
|
||||
if (gitRef) return m.file_viewer_not_found_on_branch({ branch: ltr(gitRef) });
|
||||
if (sessionId && d.source === "checkout" && d.file.root === "clone")
|
||||
return "This session's worktree isn't available, and the file isn't in the project clone or its artifacts.";
|
||||
return m.file_viewer_worktree_unavailable_not_found();
|
||||
const root = d.source === "checkout" ? d.file.root : d.checkoutRoot;
|
||||
return `File not found in the ${root === "worktree" ? "session's worktree" : "project clone"} or the project's artifacts.`;
|
||||
return m.file_viewer_not_found_root_or_artifacts({ root: root === "worktree" ? m.file_viewer_session_worktree() : m.file_viewer_project_clone() });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -479,7 +479,7 @@ export function FileViewer({
|
||||
{filePath}
|
||||
</code>
|
||||
{branchLabel && (
|
||||
<span className="file-view-branch inline-flex items-center gap-1 min-w-0 font-mono text-xs text-muted border border-border-variant rounded-sm py-px px-1.5 max-w-65 overflow-hidden text-ellipsis whitespace-nowrap shrink-0 [&_svg]:flex-none" title={`Branch: ${branchLabel}`}>
|
||||
<span className="file-view-branch inline-flex items-center gap-1 min-w-0 font-mono text-xs text-muted border border-border-variant rounded-sm py-px px-1.5 max-w-65 overflow-hidden text-ellipsis whitespace-nowrap shrink-0 [&_svg]:flex-none" title={m.a11y_branch({ branch: ltr(branchLabel) })}>
|
||||
<GitBranch size={11} />
|
||||
{branchLabel}
|
||||
</span>
|
||||
@@ -487,16 +487,16 @@ export function FileViewer({
|
||||
{showingEditor && (saving || dirty || saveError) && (
|
||||
<span
|
||||
className={`file-view-save-status inline-flex items-center gap-1 text-xs shrink-0 ${saveError ? "text-accent-red" : "text-muted"}`}
|
||||
title={saveError ?? (saving ? "Saving…" : "Unsaved — ⌘S or click away to save")}
|
||||
title={saveError ?? (saving ? m.common_saving() : m.file_viewer_unsaved_tip())}
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<span className={SPINNER_CLASS_NAME} /> Saving…
|
||||
<span className={SPINNER_CLASS_NAME} /> {m.file_viewer_saving()}
|
||||
</>
|
||||
) : saveError ? (
|
||||
"Save failed"
|
||||
m.file_viewer_save_failed()
|
||||
) : (
|
||||
"Unsaved"
|
||||
m.file_viewer_unsaved()
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
@@ -505,13 +505,13 @@ export function FileViewer({
|
||||
className={`${ICON_BUTTON_CLASS_NAME} ${!latex.showPdf ? "active" : ""}`}
|
||||
data-tip={
|
||||
latex.stale && latex.showPdf
|
||||
? "Compiled PDF is out of date"
|
||||
? m.file_viewer_pdf_out_of_date()
|
||||
: latex.showPdf
|
||||
? "View source"
|
||||
: "Show compiled PDF"
|
||||
? m.common_view_source()
|
||||
: m.file_viewer_show_compiled_pdf()
|
||||
}
|
||||
data-tip-align="end"
|
||||
aria-label={latex.showPdf ? "View source" : "Show compiled PDF"}
|
||||
aria-label={latex.showPdf ? m.common_view_source() : m.file_viewer_show_compiled_pdf()}
|
||||
onClick={() => latex.setShowPdf(!latex.showPdf)}
|
||||
>
|
||||
{latex.showPdf ? (
|
||||
@@ -526,11 +526,11 @@ export function FileViewer({
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
data-tip={
|
||||
latex.stale
|
||||
? `Download ${compiledPdfName} (out of date — recompile first)`
|
||||
: `Download ${compiledPdfName}`
|
||||
? m.file_viewer_download_stale_pdf({ name: ltr(compiledPdfName) })
|
||||
: m.a11y_download_file({ name: ltr(compiledPdfName) })
|
||||
}
|
||||
data-tip-align="end"
|
||||
aria-label={`Download ${compiledPdfName}`}
|
||||
aria-label={m.a11y_download_file({ name: ltr(compiledPdfName) })}
|
||||
href={compiledPdfUrl}
|
||||
download={compiledPdfName}
|
||||
>
|
||||
@@ -542,7 +542,7 @@ export function FileViewer({
|
||||
className={`${ICON_BUTTON_CLASS_NAME} ${showOverleaf ? "active" : ""}`}
|
||||
data-tip={overleafTip}
|
||||
data-tip-align="end"
|
||||
aria-label={`Overleaf — ${overleafTip.toLowerCase()}`}
|
||||
aria-label={m.a11y_overleaf_status({ status: overleafTip })}
|
||||
aria-expanded={showOverleaf}
|
||||
onClick={() => setShowOverleaf((open) => !open)}
|
||||
>
|
||||
@@ -556,9 +556,9 @@ export function FileViewer({
|
||||
{isLatex && onDisk && (
|
||||
<button
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
data-tip={latex.compiled ? "Recompile PDF" : "Compile PDF"}
|
||||
data-tip={latex.compiled ? m.file_viewer_recompile_pdf() : m.file_viewer_compile_pdf()}
|
||||
data-tip-align="end"
|
||||
aria-label={latex.compiled ? "Recompile PDF" : "Compile PDF"}
|
||||
aria-label={latex.compiled ? m.file_viewer_recompile_pdf() : m.file_viewer_compile_pdf()}
|
||||
disabled={latex.compiling || !latex.engine}
|
||||
onClick={() => void compileFromDisk()}
|
||||
>
|
||||
@@ -568,9 +568,9 @@ export function FileViewer({
|
||||
{isMarkdown && (
|
||||
<button
|
||||
className={`${ICON_BUTTON_CLASS_NAME} ${showSource ? "active" : ""}`}
|
||||
data-tip={showSource ? "Rendered view" : "View source"}
|
||||
data-tip={showSource ? m.common_rendered_view() : m.common_view_source()}
|
||||
data-tip-align="end"
|
||||
aria-label={showSource ? "Rendered view" : "View source"}
|
||||
aria-label={showSource ? m.common_rendered_view() : m.common_view_source()}
|
||||
onClick={() => setShowSource((s) => !s)}
|
||||
>
|
||||
<Code size={13} />
|
||||
@@ -579,9 +579,9 @@ export function FileViewer({
|
||||
{onDisk && (
|
||||
<button
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
data-tip={editorError ?? "Open in default editor"}
|
||||
data-tip={editorError ?? m.file_viewer_open_in_default_editor()}
|
||||
data-tip-align="end"
|
||||
aria-label="Open in default editor"
|
||||
aria-label={m.file_viewer_open_in_default_editor()}
|
||||
disabled={openingEditor}
|
||||
onClick={() => void openInEditor()}
|
||||
>
|
||||
@@ -590,9 +590,9 @@ export function FileViewer({
|
||||
)}
|
||||
<button
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
data-tip="Reload file"
|
||||
data-tip={m.file_viewer_reload_file()}
|
||||
data-tip-align="end"
|
||||
aria-label="Reload file"
|
||||
aria-label={m.file_viewer_reload_file()}
|
||||
onClick={() => setNonce((n) => n + 1)}
|
||||
>
|
||||
{loading ? <span className={SPINNER_CLASS_NAME} /> : <RotateCw size={13} />}
|
||||
@@ -602,8 +602,7 @@ export function FileViewer({
|
||||
editable, and the editor's `h-full` would push it out of view. */}
|
||||
{!error && viaCheckout && loaded?.source === "checkout" && (
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted border-b border-b-border-variant shrink-0">
|
||||
Not in the project's artifacts — showing the copy from the{" "}
|
||||
{loaded.file.root === "worktree" ? "session's worktree" : "project clone"}.
|
||||
{m.file_viewer_not_in_artifacts_showing_root({ root: loaded.file.root === "worktree" ? m.file_viewer_session_worktree() : m.file_viewer_project_clone() })}
|
||||
</div>
|
||||
)}
|
||||
{(latex.error || latex.log) && (
|
||||
@@ -616,14 +615,14 @@ export function FileViewer({
|
||||
>
|
||||
{latex.error ??
|
||||
(latex.builtWithErrors
|
||||
? "Compiled, but the engine reported errors — check the output below."
|
||||
: "Compile failed")}
|
||||
? m.file_viewer_compiled_with_errors()
|
||||
: m.file_viewer_compile_failed())}
|
||||
</span>
|
||||
<button
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
data-tip="Dismiss"
|
||||
data-tip={m.file_viewer_dismiss()}
|
||||
data-tip-align="end"
|
||||
aria-label="Dismiss compile message"
|
||||
aria-label={m.file_viewer_dismiss_compile_message()}
|
||||
onClick={latex.dismiss}
|
||||
>
|
||||
<X size={13} />
|
||||
@@ -639,8 +638,7 @@ export function FileViewer({
|
||||
{liveTex && overleaf.staleOnDisk && (
|
||||
<div className="file-view-note shrink-0 border-b border-b-border-variant py-2.5 px-4 flex items-center flex-wrap gap-2 text-sm text-accent-amber">
|
||||
<span className="flex-1 min-w-0">
|
||||
Overleaf's copy of this file was pulled while you had unsaved edits, so what you
|
||||
see is no longer what is on disk. Saving now sends this draft to Overleaf instead.
|
||||
{m.file_viewer_overleaf_apos_s_copy_of_this_file_was()}
|
||||
</span>
|
||||
<button
|
||||
className={BUTTON_CLASS_NAME}
|
||||
@@ -649,7 +647,7 @@ export function FileViewer({
|
||||
setNonce((n) => n + 1);
|
||||
}}
|
||||
>
|
||||
Discard my edits and reload
|
||||
{m.file_viewer_discard_my_edits_and_reload()}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -660,9 +658,9 @@ export function FileViewer({
|
||||
</span>
|
||||
<button
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
data-tip="Dismiss"
|
||||
data-tip={m.file_viewer_dismiss()}
|
||||
data-tip-align="end"
|
||||
aria-label="Dismiss Overleaf message"
|
||||
aria-label={m.file_viewer_dismiss_overleaf_message()}
|
||||
onClick={overleaf.dismiss}
|
||||
>
|
||||
<X size={13} />
|
||||
@@ -687,7 +685,7 @@ export function FileViewer({
|
||||
)}
|
||||
{showingPdf && latex.stale && (
|
||||
<div className="file-view-note shrink-0 border-b border-b-border-variant py-2 px-4 text-sm text-subtext">
|
||||
This PDF was compiled from an earlier version of the source — recompile to update it.
|
||||
{m.file_viewer_this_pdf_was_compiled_from_an_earlier_version()}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
@@ -704,22 +702,21 @@ export function FileViewer({
|
||||
>
|
||||
{!showingEditor && !error && !isArtifacts && loaded?.source === "checkout" && !loaded.file.notFound && !gitRef && sessionId && loaded.file.root === "clone" && (
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">
|
||||
This session's worktree isn't available — showing the project clone's copy.
|
||||
{m.file_viewer_this_session_apos_s_worktree_isn_apos_t()}
|
||||
</div>
|
||||
)}
|
||||
{!showingEditor && !error && loaded?.source === "artifact" && !loaded.file.notFound && viaArtifacts && (
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">
|
||||
Not in the {loaded.checkoutRoot === "worktree" ? "session's worktree" : "project clone"} —
|
||||
showing the copy from the project's artifacts.
|
||||
{m.file_viewer_artifact_fallback({ root: loaded.checkoutRoot === "worktree" ? m.file_viewer_session_worktree() : m.file_viewer_project_clone() })}
|
||||
</div>
|
||||
)}
|
||||
{error ? (
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">Failed to load file: {error}</div>
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">{m.file_viewer_failed_to_load_file()} {ltr(error)}</div>
|
||||
) : data === null ? (
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">Loading…</div>
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">{m.file_viewer_loading()}</div>
|
||||
) : data.notFound ? (
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">
|
||||
{loaded ? notFoundCopy(loaded) : "File not found."}
|
||||
{loaded ? notFoundCopy(loaded) : m.file_viewer_not_found()}
|
||||
</div>
|
||||
) : mediaKind ? (
|
||||
<MediaPreview
|
||||
@@ -729,7 +726,7 @@ export function FileViewer({
|
||||
/>
|
||||
) : data.binary ? (
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">
|
||||
Binary file — no inline preview. <a href={rawUrl} download={path.split("/").pop() ?? path}>Download</a>
|
||||
{m.file_viewer_binary_file_no_inline_preview()} <a href={rawUrl} download={path.split("/").pop() ?? path}>{m.file_viewer_download()}</a>
|
||||
</div>
|
||||
) : showingPdf && pdfPaneUrl && compiledPdfName ? (
|
||||
<MediaPreview
|
||||
@@ -786,7 +783,7 @@ export function FileViewer({
|
||||
onScrollRequestHandled={onLineScrollRequestHandled}
|
||||
/>
|
||||
{data.truncated && (
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">File truncated — showing the first 512 KB.</div>
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">{m.file_viewer_file_truncated_showing_the_first_512_kb()}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
// Mirror of openresearch.sh's GitDiff: per-file collapsible cards over
|
||||
// react-diff-view's unified view, with refractor syntax highlighting.
|
||||
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
} from "react-diff-view";
|
||||
import { refractor } from "refractor";
|
||||
import { detectSyntaxLanguageFromFilePath } from "../syntaxLanguage";
|
||||
import { fmtBytes, fmtNumber } from "../api";
|
||||
const DIFF_CLASS_NAME = [
|
||||
"openresearch-diff flex flex-col gap-4",
|
||||
"[&_.openresearch-diff-file]:[--openresearch-diff-selection-background-color:color-mix(_in_oklab,_var(--surface)_76%,_var(--primary)_)]",
|
||||
@@ -57,14 +60,14 @@ const DIFF_CLASS_NAME = [
|
||||
"[&_.openresearch-diff-file.diff-unified_col.diff-gutter-col:nth-child(2)]:w-[1%]",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:first-child]:hidden",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:sticky",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:left-0",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:start-0",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:z-1",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:w-[1%]",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:pt-0 [&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:pr-2.5 [&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:pb-0 [&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:pl-3.5",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:pt-0 [&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:pe-2.5 [&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:pb-0 [&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:ps-3.5",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:whitespace-nowrap",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:text-right",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:text-end",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:text-[var(--openresearch-diff-unified-gutter-text-color)]",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:border-r [&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:border-r-[var(--openresearch-diff-divider-color)]",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:border-e [&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:border-e-[var(--openresearch-diff-divider-color)]",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:select-none",
|
||||
"[&_.openresearch-diff-file.diff-unified_.diff-line_>_td:nth-child(2)]:cursor-default",
|
||||
"[&_.openresearch-diff-file_.diff-line]:leading-[1.55]",
|
||||
@@ -163,11 +166,6 @@ const renderUnifiedGutter: RenderGutter = ({ change, side }) => {
|
||||
return getUnifiedLineNumber(change);
|
||||
};
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
|
||||
export function TruncatedDiffNotice({
|
||||
bytesRead,
|
||||
byteLimit,
|
||||
@@ -177,10 +175,9 @@ export function TruncatedDiffNotice({
|
||||
}) {
|
||||
return (
|
||||
<div className="truncated-notice border border-accent-amber rounded-md bg-accent-amber-subtle py-3 px-3.5 text-md [&_h4]:mt-0 [&_h4]:mx-0 [&_h4]:mb-1 [&_h4]:text-md [&_h4]:text-accent-amber [&_p]:m-0 [&_p]:text-subtext">
|
||||
<h4>Diff preview truncated</h4>
|
||||
<h4>{m.git_diff_diff_preview_truncated()}</h4>
|
||||
<p>
|
||||
Showing the first {formatBytes(byteLimit)} ({formatBytes(bytesRead)} read). View the complete
|
||||
diff locally with git.
|
||||
{m.git_diff_truncated_detail({ limit: ltr(fmtBytes(byteLimit)), read: ltr(fmtBytes(bytesRead)) })}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -208,7 +205,7 @@ function DiffFileCard({
|
||||
return (
|
||||
<section className={`diff-file-card overflow-hidden border border-border rounded-md bg-background [&.expanded_.diff-file-header]:border-b [&.expanded_.diff-file-header]:border-b-border ${expanded ? "expanded" : ""}`}>
|
||||
<button
|
||||
className="diff-file-header sticky top-0 z-10 flex items-center justify-between gap-3 w-full text-left py-2 px-3 bg-canvas cursor-pointer [&_.chev]:text-muted [&_.chev]:text-2xs [&_.chev]:shrink-0 [&_.chev]:w-3 [&_.path]:flex [&_.path]:items-center [&_.path]:gap-2 [&_.path]:min-w-0 [&_.path]:flex-1 [&_.path_code]:min-w-0 [&_.path_code]:flex-1 [&_.path_code]:overflow-hidden [&_.path_code]:text-ellipsis [&_.path_code]:whitespace-nowrap [&_.path_code]:font-mono [&_.path_code]:text-xs [&_.path_code]:font-semibold [&_.path_code]:text-text [&_.stats]:flex [&_.stats]:items-center [&_.stats]:gap-2 [&_.stats]:shrink-0 [&_.stats]:font-mono [&_.stats]:text-2xs [&_.stats]:font-medium [&_.stats]:tabular-nums"
|
||||
className="diff-file-header sticky top-0 z-10 flex items-center justify-between gap-3 w-full text-start py-2 px-3 bg-canvas cursor-pointer [&_.chev]:text-muted [&_.chev]:text-2xs [&_.chev]:shrink-0 [&_.chev]:w-3 [&_.path]:flex [&_.path]:items-center [&_.path]:gap-2 [&_.path]:min-w-0 [&_.path]:flex-1 [&_.path_code]:min-w-0 [&_.path_code]:flex-1 [&_.path_code]:overflow-hidden [&_.path_code]:text-ellipsis [&_.path_code]:whitespace-nowrap [&_.path_code]:font-mono [&_.path_code]:text-xs [&_.path_code]:font-semibold [&_.path_code]:text-text [&_.stats]:flex [&_.stats]:items-center [&_.stats]:gap-2 [&_.stats]:shrink-0 [&_.stats]:font-mono [&_.stats]:text-2xs [&_.stats]:font-medium [&_.stats]:tabular-nums"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => setExpanded((e) => !e)}
|
||||
>
|
||||
@@ -225,7 +222,7 @@ function DiffFileCard({
|
||||
</button>
|
||||
{expanded &&
|
||||
(file.hunks.length === 0 ? (
|
||||
<div className="diff-empty py-2 px-3 text-muted text-md">No textual diff for this file.</div>
|
||||
<div className="diff-empty py-2 px-3 text-muted text-md">{m.git_diff_no_textual_diff_for_this_file()}</div>
|
||||
) : (
|
||||
<div className="diff-file-body overflow-x-auto bg-background">
|
||||
<Diff
|
||||
@@ -259,8 +256,8 @@ function DiffFiles({ files, className }: { files: FileData[]; className?: string
|
||||
|
||||
export function GitDiff({ diff, className }: { diff: string; className?: string }) {
|
||||
const parsed = useMemo(() => parseDiffFiles(diff, false), [diff]);
|
||||
if (parsed.failed) return <div className="diff-empty py-2 px-3 text-muted text-md">Unable to parse this diff.</div>;
|
||||
if (parsed.files.length === 0) return <div className="diff-empty py-2 px-3 text-muted text-md">No changes.</div>;
|
||||
if (parsed.failed) return <div className="diff-empty py-2 px-3 text-muted text-md">{m.git_diff_unable_to_parse_this_diff()}</div>;
|
||||
if (parsed.files.length === 0) return <div className="diff-empty py-2 px-3 text-muted text-md">{m.git_diff_no_changes()}</div>;
|
||||
return <DiffFiles files={parsed.files} className={className} />;
|
||||
}
|
||||
|
||||
@@ -302,25 +299,25 @@ export function GitDiffExplorer({ diff, partial = false }: { diff: string; parti
|
||||
if (parsed.failed) {
|
||||
return (
|
||||
<div className="diff-empty py-2 px-3 text-muted text-md">
|
||||
{partial ? "No complete file preview was available before the cutoff." : "Unable to parse this diff."}
|
||||
{partial ? m.git_diff_no_complete_preview() : m.git_diff_parse_failed()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (items.length === 0) return <div className="diff-empty py-2 px-3 text-muted text-md">No changes.</div>;
|
||||
if (items.length === 0) return <div className="diff-empty py-2 px-3 text-muted text-md">{m.git_diff_no_changes()}</div>;
|
||||
|
||||
return (
|
||||
<div className="diff-explorer @container">
|
||||
<div className="diff-explorer-toolbar flex items-center justify-between gap-3 mb-2.5 text-sm [&_button]:py-0.5 [&_button]:px-0 [&_button]:text-muted [&_button]:text-xs [&_button]:font-medium [&_button:hover]:text-text [&_button:hover]:underline [&_button:hover]:underline-offset-2">
|
||||
<strong>
|
||||
{partial
|
||||
? `${items.length} ${items.length === 1 ? "file" : "files"} shown (partial)`
|
||||
? items.length === 1 ? m.git_diff_one_file_partial() : m.git_diff_files_partial({ count: fmtNumber(items.length) })
|
||||
: items.length === 1
|
||||
? "1 changed file"
|
||||
: `${items.length} changed files`}
|
||||
? m.git_diff_one_changed_file()
|
||||
: m.git_diff_changed_file_count({ count: fmtNumber(items.length) })}
|
||||
</strong>
|
||||
{!partial && (
|
||||
<button type="button" onClick={() => setShowFullDiff((current) => !current)}>
|
||||
{showingFullDiff ? "Back to preview" : "View full diff"}
|
||||
{showingFullDiff ? m.git_diff_back_to_preview() : m.git_diff_view_full()}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -328,7 +325,7 @@ export function GitDiffExplorer({ diff, partial = false }: { diff: string; parti
|
||||
<DiffFiles files={files} />
|
||||
) : (
|
||||
<div className="diff-explorer-layout grid grid-cols-[minmax(180px,_260px)_minmax(0,_1fr)] items-start gap-3.5 [@container((max-width:_960px))]:grid-cols-1">
|
||||
<div className="diff-explorer-files sticky top-0 max-h-[min(70vh,_720px)] overflow-auto border border-border rounded-md bg-background [&_button]:grid [&_button]:grid-cols-[18px_minmax(0,_1fr)_auto_auto] [&_button]:items-center [&_button]:gap-[7px] [&_button]:w-full [&_button]:py-2 [&_button]:px-[9px] [&_button]:border-b [&_button]:border-b-border-variant [&_button]:text-text [&_button]:text-left [&_button:last-child]:border-b-0 [&_button:hover]:bg-surface [&_button.active]:bg-surface [&_button.active]:shadow-[inset_2px_0_0_var(--text)] [&_code]:overflow-hidden [&_code]:text-ellipsis [&_code]:whitespace-nowrap [&_code]:text-xs [@container((max-width:_960px))]:static [@container((max-width:_960px))]:max-h-55" aria-label="Changed files">
|
||||
<div className="diff-explorer-files sticky top-0 max-h-[min(70vh,_720px)] overflow-auto border border-border rounded-md bg-background [&_button]:grid [&_button]:grid-cols-[18px_minmax(0,_1fr)_auto_auto] [&_button]:items-center [&_button]:gap-[7px] [&_button]:w-full [&_button]:py-2 [&_button]:px-[9px] [&_button]:border-b [&_button]:border-b-border-variant [&_button]:text-text [&_button]:text-start [&_button:last-child]:border-b-0 [&_button:hover]:bg-surface [&_button.active]:bg-surface [&_button.active]:shadow-[inset_2px_0_0_var(--text)] [&_code]:overflow-hidden [&_code]:text-ellipsis [&_code]:whitespace-nowrap [&_code]:text-xs [@container((max-width:_960px))]:static [@container((max-width:_960px))]:max-h-55" aria-label={m.git_diff_changed_files()}>
|
||||
{items.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { useState } from "react";
|
||||
import { BUTTON_CLASS_NAME } from "../styleClasses";
|
||||
|
||||
@@ -42,10 +43,10 @@ export function TokenForm<T>({
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button type="submit" className={BUTTON_CLASS_NAME} disabled={saving || !token.trim()}>
|
||||
{saving ? "Saving…" : "Save"}
|
||||
{saving ? m.common_saving() : m.common_save()}
|
||||
</button>
|
||||
<a href={createHref} target="_blank" rel="noreferrer">
|
||||
Create a token ↗
|
||||
{m.git_token_form_create_a_token()}
|
||||
</a>
|
||||
{error && <div className="error">{error}</div>}
|
||||
</form>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ChevronDown,
|
||||
@@ -44,10 +45,10 @@ export function RailHeader({
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className="rail-brand flex items-center gap-1 h-16 p-2 border-b border-b-border shrink-0 [&_.project-switcher]:relative [&_.project-switcher]:flex-1 [&_.project-switcher]:self-stretch [&_.project-switcher]:min-w-0 [&_.project-back]:shrink-0 [&_.brand]:flex [&_.brand]:items-center [&_.brand]:justify-between [&_.brand]:gap-2 [&_.brand]:w-full [&_.brand]:h-full [&_.brand]:min-w-0 [&_.brand]:font-semibold [&_.brand]:text-base [&_.brand]:text-text [&_.brand]:py-1 [&_.brand]:px-1.5 [&_.brand]:border [&_.brand]:border-transparent [&_.brand]:rounded-sm [&_.brand:hover]:bg-surface [&_.brand:hover]:border-border [&_.brand.open]:bg-surface [&_.brand.open]:border-border [&_.brand_svg]:shrink-0 [&_.brand-project-copy]:flex [&_.brand-project-copy]:flex-col [&_.brand-project-copy]:gap-[3px] [&_.brand-project-copy]:min-w-0 [&_.brand-project-copy]:leading-[1.15] [&_.brand-project-copy]:text-left [&_.brand-project-label]:text-muted [&_.brand-project-label]:text-2xs [&_.brand-project-label]:font-medium [&_.brand-project-label]:tracking-[0.04em] [&_.brand-project-label]:uppercase [&_.brand_.brand-project]:min-w-0 [&_.brand_.brand-project]:overflow-hidden [&_.brand_.brand-project]:text-ellipsis [&_.brand_.brand-project]:whitespace-nowrap [&_.brand_.brand-project]:text-2xl [&_.project-chevron]:text-muted [&_.project-chevron]:opacity-0 [&_.project-chevron]:transition-transform [&_.project-chevron]:duration-120 [&_.project-chevron]:ease-standard [&_.brand:hover_.project-chevron]:opacity-100 [&_.brand.open_.project-chevron]:opacity-100 [&_.brand.open_.project-chevron]:rotate-180 [&_.project-menu]:left-0 [&_.project-menu]:w-52.5 [&_.project-menu]:z-70">
|
||||
<div className="rail-brand flex items-center gap-1 h-16 p-2 border-b border-b-border shrink-0 [&_.project-switcher]:relative [&_.project-switcher]:flex-1 [&_.project-switcher]:self-stretch [&_.project-switcher]:min-w-0 [&_.project-back]:shrink-0 [&_.brand]:flex [&_.brand]:items-center [&_.brand]:justify-between [&_.brand]:gap-2 [&_.brand]:w-full [&_.brand]:h-full [&_.brand]:min-w-0 [&_.brand]:font-semibold [&_.brand]:text-base [&_.brand]:text-text [&_.brand]:py-1 [&_.brand]:px-1.5 [&_.brand]:border [&_.brand]:border-transparent [&_.brand]:rounded-sm [&_.brand:hover]:bg-surface [&_.brand:hover]:border-border [&_.brand.open]:bg-surface [&_.brand.open]:border-border [&_.brand_svg]:shrink-0 [&_.brand-project-copy]:flex [&_.brand-project-copy]:flex-col [&_.brand-project-copy]:gap-[3px] [&_.brand-project-copy]:min-w-0 [&_.brand-project-copy]:leading-[1.15] [&_.brand-project-copy]:text-start [&_.brand-project-label]:text-muted [&_.brand-project-label]:text-2xs [&_.brand-project-label]:font-medium [&_.brand-project-label]:tracking-[0.04em] [&_.brand-project-label]:uppercase [&_.brand_.brand-project]:min-w-0 [&_.brand_.brand-project]:overflow-hidden [&_.brand_.brand-project]:text-ellipsis [&_.brand_.brand-project]:whitespace-nowrap [&_.brand_.brand-project]:text-2xl [&_.project-chevron]:text-muted [&_.project-chevron]:opacity-0 [&_.project-chevron]:transition-transform [&_.project-chevron]:duration-120 [&_.project-chevron]:ease-standard [&_.brand:hover_.project-chevron]:opacity-100 [&_.brand.open_.project-chevron]:opacity-100 [&_.brand.open_.project-chevron]:rotate-180 [&_.project-menu]:start-0 [&_.project-menu]:w-52.5 [&_.project-menu]:z-70">
|
||||
<button
|
||||
className={`${ICON_BUTTON_CLASS_NAME} project-back !text-text`}
|
||||
aria-label="All projects"
|
||||
aria-label={m.header_all_projects()}
|
||||
onClick={onHome}
|
||||
>
|
||||
<ArrowLeft size={18} />
|
||||
@@ -60,13 +61,13 @@ export function RailHeader({
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span className="brand-project-copy">
|
||||
<span className="brand-project-label">Project</span>
|
||||
<span className="brand-project-label">{m.header_project()}</span>
|
||||
<span className="brand-project">{projectName}</span>
|
||||
</span>
|
||||
<ChevronDown className="project-chevron" size={14} />
|
||||
</button>
|
||||
{open && (
|
||||
<div className="option-menu absolute bottom-[calc(100%_+_8px)] left-0 max-h-95 flex flex-col bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] z-50 overflow-hidden min-w-47.5 p-1.5 [&.align-right]:left-auto [&.align-right]:right-0 [&.drop-down]:bottom-auto [&.drop-down]:top-[calc(100%_+_4px)] [&.session-menu]:left-auto [&.session-menu]:right-1.5 [&.session-menu]:top-[calc(100%_-_2px)] [&.session-menu]:min-w-35 drop-down project-menu">
|
||||
<div className="option-menu absolute bottom-[calc(100%_+_8px)] start-0 max-h-95 flex flex-col bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] z-50 overflow-hidden min-w-47.5 p-1.5 [&.align-right]:start-auto [&.align-right]:end-0 [&.drop-down]:bottom-auto [&.drop-down]:top-[calc(100%_+_4px)] [&.session-menu]:start-auto [&.session-menu]:end-1.5 [&.session-menu]:top-[calc(100%_-_2px)] [&.session-menu]:min-w-35 drop-down project-menu">
|
||||
<button
|
||||
className={MODEL_ITEM_CLASS_NAME}
|
||||
onClick={() => {
|
||||
@@ -75,7 +76,7 @@ export function RailHeader({
|
||||
}}
|
||||
>
|
||||
<span className={PROJECT_MENU_LABEL_CLASS_NAME}>
|
||||
<FolderGit2 size={14} />Configure Repository
|
||||
<FolderGit2 size={14} />{m.header_configure_repository()}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@@ -85,7 +86,7 @@ export function RailHeader({
|
||||
onHome();
|
||||
}}
|
||||
>
|
||||
<span className={PROJECT_MENU_LABEL_CLASS_NAME}><History size={14} />All projects</span>
|
||||
<span className={PROJECT_MENU_LABEL_CLASS_NAME}><History size={14} />{m.header_all_projects()}</span>
|
||||
</button>
|
||||
<button
|
||||
className={MODEL_ITEM_CLASS_NAME}
|
||||
@@ -95,7 +96,7 @@ export function RailHeader({
|
||||
onNewProject();
|
||||
}}
|
||||
>
|
||||
<span className={PROJECT_MENU_LABEL_CLASS_NAME}><FolderPlus size={14} />Create a new project</span>
|
||||
<span className={PROJECT_MENU_LABEL_CLASS_NAME}><FolderPlus size={14} />{m.header_create_a_new_project()}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -103,9 +104,9 @@ export function RailHeader({
|
||||
{onCollapse && (
|
||||
<button
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
data-tip="Hide sidebar"
|
||||
data-tip={m.header_hide_sidebar()}
|
||||
data-tip-align="end"
|
||||
aria-label="Hide sidebar"
|
||||
aria-label={m.header_hide_sidebar()}
|
||||
onClick={onCollapse}
|
||||
>
|
||||
<PanelLeft size={15} />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
// Literature-source toggles shown inline in the composer chat-settings panel:
|
||||
// which sources discovery and paper reading may use. State lives in settings.json
|
||||
// (same `/api/settings/lit-sources` endpoint the CLI enforces).
|
||||
@@ -38,7 +39,7 @@ export function LitSourcesList() {
|
||||
.finally(() => setSaving(false));
|
||||
};
|
||||
|
||||
if (!settings) return <div className="py-1.5 px-2 text-muted text-sm">Loading…</div>;
|
||||
if (!settings) return <div className="py-1.5 px-2 text-muted text-sm">{m.lit_sources_picker_loading()}</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { FilePresentation } from "../api";
|
||||
|
||||
@@ -20,8 +22,8 @@ export function mediaPreviewKind(
|
||||
function DownloadFallback({ url, name }: { url: string; name: string }) {
|
||||
return (
|
||||
<div className="file-view-note py-2.5 px-4 text-sm text-muted">
|
||||
This browser can’t preview this media format.{" "}
|
||||
<a href={url} download={name}>Download {name}</a>
|
||||
{m.media_preview_this_browser_can_t_preview_this_media_format()}{" "}
|
||||
<a href={url} download={name}>{m.media_preview_download()} {ltr(name)}</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -96,8 +98,8 @@ export function MediaPreview({
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
{preview}
|
||||
{downloadBar && (
|
||||
<div className="shrink-0 border-t border-border-variant py-1.5 px-3 text-right text-xs">
|
||||
<a href={url} download={name}>Download {name}</a>
|
||||
<div className="shrink-0 border-t border-border-variant py-1.5 px-3 text-end text-xs">
|
||||
<a href={url} download={name}>{m.media_preview_download()} {name}</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
import { Check, ChevronDown, ChevronLeft, ChevronRight, Lock, Zap } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react";
|
||||
import {
|
||||
getHarnesses,
|
||||
fmtNumber,
|
||||
harnessModelLabel,
|
||||
modelLabel,
|
||||
reasoningFor,
|
||||
@@ -212,13 +215,13 @@ export function ModelPicker({
|
||||
? selected
|
||||
? harnessModelLabel(selected)
|
||||
: modelLabel(value.model)
|
||||
: "Default model"
|
||||
: "Model";
|
||||
: m.model_picker_default_model()
|
||||
: m.model_picker_model();
|
||||
const effectiveReasoningId = value?.reasoningLevel ?? defaultReasoningId ?? reasoningChoices[0]?.id;
|
||||
const reasoningLabel = reasoningChoices.find((choice) => choice.id === effectiveReasoningId)?.label;
|
||||
const effectivePermissionId = value?.permissionMode ?? defaultPermissionId ?? permissionChoices[0]?.id;
|
||||
const permissionLabel = permissionChoices.find((choice) => choice.id === effectivePermissionId)?.label;
|
||||
const reasoningAxisLabel = value?.harness === "opencode" ? "Variant" : "Effort";
|
||||
const reasoningAxisLabel = value?.harness === "opencode" ? m.model_picker_variant() : m.model_picker_effort();
|
||||
const selectedHarness = harnesses.find((harness) => harness.id === value?.harness);
|
||||
const speedChoices = serviceTiersFor(selectedHarness, value?.model);
|
||||
const effectiveServiceTier = reconcileServiceTier(
|
||||
@@ -250,7 +253,7 @@ export function ModelPicker({
|
||||
) => (
|
||||
<button
|
||||
type="button"
|
||||
className="model-root-row flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-md text-text hover:bg-surface"
|
||||
className="model-root-row flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-start text-md text-text hover:bg-surface"
|
||||
aria-haspopup="menu"
|
||||
onClick={() => setPage(next)}
|
||||
>
|
||||
@@ -264,7 +267,7 @@ export function ModelPicker({
|
||||
<button
|
||||
ref={submenuHeaderRef}
|
||||
type="button"
|
||||
className="model-submenu-header flex w-full items-center gap-2 border-0 border-b border-solid border-b-border-variant bg-transparent px-2 py-2 text-left text-sm font-medium text-text hover:bg-surface"
|
||||
className="model-submenu-header flex w-full items-center gap-2 border-0 border-b border-solid border-b-border-variant bg-transparent px-2 py-2 text-start text-sm font-medium text-text hover:bg-surface"
|
||||
onClick={() => {
|
||||
setPage("root");
|
||||
setFilter("");
|
||||
@@ -288,7 +291,7 @@ export function ModelPicker({
|
||||
<span>
|
||||
{choice.label}
|
||||
{choice.id === defaultId && (
|
||||
<span className="font-normal text-muted"> · Default</span>
|
||||
<span className="font-normal text-muted"> {m.model_picker_default()}</span>
|
||||
)}
|
||||
</span>
|
||||
{choice.description && (
|
||||
@@ -309,7 +312,7 @@ export function ModelPicker({
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
className={`${COMPOSER_CONTROL_CLASS_NAME} composer-pill min-w-0 max-w-full gap-[5px] px-2 text-md text-text whitespace-nowrap`}
|
||||
title={`Harness + model for this chat: ${label}${reasoningLabel ? ` · ${reasoningLabel}` : ""}${speedLabel ? ` · ${speedLabel}` : ""}`}
|
||||
title={m.a11y_chat_model({ label: `${label}${reasoningLabel ? ` · ${reasoningLabel}` : ""}${speedLabel ? ` · ${speedLabel}` : ""}` })}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
onClick={() => {
|
||||
@@ -325,36 +328,36 @@ export function ModelPicker({
|
||||
) : value?.harness ? (
|
||||
<HarnessLogo harness={value.harness} size={14} />
|
||||
) : null}
|
||||
{effectiveServiceTier === "priority" && <span className="sr-only">Fast speed · </span>}
|
||||
{effectiveServiceTier === "priority" && <span className="sr-only">{m.model_picker_fast_speed()} </span>}
|
||||
<span className="model-picker-label min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{label}
|
||||
{reasoningLabel && <span className="model-picker-reasoning ml-1 text-muted">{reasoningLabel}</span>}
|
||||
{reasoningLabel && <span className="model-picker-reasoning ms-1 text-muted">{reasoningLabel}</span>}
|
||||
</span>
|
||||
<ChevronDown size={14} className="shrink-0 text-muted" />
|
||||
</button>
|
||||
{open && (
|
||||
<div className="model-menu absolute bottom-[calc(100%_+_8px)] left-0 max-h-100 flex flex-col bg-background border border-border rounded-md shadow-[0_10px_26px_rgba(0,_0,_0,_0.16)] z-50 overflow-hidden w-72 [&.align-right]:left-auto [&.align-right]:right-0 [&_input]:rounded-none [&_input]:border-0 [&_input]:border-b [&_input]:border-b-border-variant [&_input]:bg-none [&_input]:bg-transparent [&_input]:py-2 [&_input]:px-2.5 [&_input]:text-sm [&_input]:outline-none align-right">
|
||||
<div className="model-menu absolute bottom-[calc(100%_+_8px)] start-0 max-h-100 flex flex-col bg-background border border-border rounded-md shadow-[0_10px_26px_rgba(0,_0,_0,_0.16)] z-50 overflow-hidden w-72 [&.align-right]:start-auto [&.align-right]:end-0 [&_input]:rounded-none [&_input]:border-0 [&_input]:border-b [&_input]:border-b-border-variant [&_input]:bg-none [&_input]:bg-transparent [&_input]:py-2 [&_input]:px-2.5 [&_input]:text-sm [&_input]:outline-none align-right">
|
||||
{page === "root" && (
|
||||
<div className="model-root-menu p-1">
|
||||
{menuRow("Model", label, "models")}
|
||||
{menuRow(m.model_picker_model(), label, "models")}
|
||||
{reasoningChoices.length > 0 && menuRow(reasoningAxisLabel, reasoningLabel, "reasoning")}
|
||||
{speedChoices.length > 0 && menuRow("Speed", speedLabel, "speed")}
|
||||
{permissionChoices.length > 0 && menuRow("Mode", permissionLabel, "permissions")}
|
||||
{speedChoices.length > 0 && menuRow(m.model_picker_speed(), speedLabel, "speed")}
|
||||
{permissionChoices.length > 0 && menuRow(m.model_picker_mode(), permissionLabel, "permissions")}
|
||||
</div>
|
||||
)}
|
||||
{page === "models" && (
|
||||
<>
|
||||
{submenuHeader("Model")}
|
||||
{submenuHeader(m.model_picker_model())}
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Search models…"
|
||||
placeholder={m.model_picker_search_models()}
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
/>
|
||||
<div className="model-menu-list overflow-y-auto p-1.5">
|
||||
{groups.map(({ harness, models, hidden }) => (
|
||||
<div key={harness.id} className="[&_.model-item]:pl-6">
|
||||
<div key={harness.id} className="[&_.model-item]:ps-6">
|
||||
<div className={MODEL_GROUP_CLASS_NAME}>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<HarnessLogo harness={harness.id} size={14} />
|
||||
@@ -362,13 +365,13 @@ export function ModelPicker({
|
||||
</span>
|
||||
{!harness.agentReady && (
|
||||
<span className="model-group-status inline-flex items-center gap-1 text-accent-amber font-normal">
|
||||
<Lock size={10} /> Unavailable
|
||||
<Lock size={10} /> {m.model_picker_unavailable()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!harness.agentReady ? (
|
||||
<div className="model-more [&_code]:font-mono [&_code]:text-xs [&_code]:bg-panel [&_code]:border [&_code]:border-border-variant [&_code]:rounded-xs [&_code]:py-px [&_code]:px-[5px] [&_code]:whitespace-nowrap pt-1 px-2 pb-2 text-xs text-muted model-unavailable leading-normal border-b border-b-border-variant">
|
||||
{harness.agentNote ? renderNote(harness.agentNote) : "Not available"}
|
||||
{harness.agentNote ? renderNote(harness.agentNote) : m.model_picker_not_available()}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -380,8 +383,8 @@ export function ModelPicker({
|
||||
{harness.models.length === 0 && (
|
||||
<button className={MODEL_ITEM_CLASS_NAME} onClick={() => pick(harness, null)}>
|
||||
<span>
|
||||
Default model
|
||||
<span className="model-id">CLI configuration</span>
|
||||
{m.model_picker_default_model()}
|
||||
<span className="model-id">{m.model_picker_cli_configuration()}</span>
|
||||
</span>
|
||||
{value?.harness === harness.id && value?.model === null && (
|
||||
<Check size={13} />
|
||||
@@ -402,7 +405,7 @@ export function ModelPicker({
|
||||
</button>
|
||||
))}
|
||||
{hidden > 0 && (
|
||||
<div className={MODEL_MORE_CLASS_NAME}>{hidden} more — search to find</div>
|
||||
<div className={MODEL_MORE_CLASS_NAME}>{m.model_picker_more({ count: fmtNumber(hidden) })}</div>
|
||||
)}
|
||||
{/* Free-form escape hatch: the catalogs are curated menus,
|
||||
not the set of ids the CLIs accept — `--model
|
||||
@@ -415,8 +418,7 @@ export function ModelPicker({
|
||||
onClick={() => pick(harness, filter.trim())}
|
||||
>
|
||||
<span>
|
||||
Use “{filter.trim()}”
|
||||
<span className="model-id">pass as the model id</span>
|
||||
{m.model_picker_use_id({ id: ltr(filter.trim()) })}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -424,12 +426,12 @@ export function ModelPicker({
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{harnesses.length === 0 && <div className={MODEL_MORE_CLASS_NAME}>Detecting harnesses…</div>}
|
||||
{harnesses.length === 0 && <div className={MODEL_MORE_CLASS_NAME}>{m.model_picker_detecting_harnesses()}</div>}
|
||||
</div>
|
||||
{lockHarness && value && harnesses.length > 1 && (
|
||||
<div className="model-locked-note flex items-center gap-1.5 py-[7px] px-3 text-xs text-muted border-t border-t-border-variant [&_svg]:shrink-0">
|
||||
<Lock size={11} />
|
||||
Sessions keep their harness — new chat to switch
|
||||
{m.model_picker_sessions_keep_their_harness_new_chat_to_switch()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -442,13 +444,13 @@ export function ModelPicker({
|
||||
)}
|
||||
{page === "permissions" && (
|
||||
<>
|
||||
{submenuHeader("Mode")}
|
||||
{submenuHeader(m.model_picker_mode())}
|
||||
{choiceList(permissionChoices, effectivePermissionId, defaultPermissionId, choosePermission)}
|
||||
</>
|
||||
)}
|
||||
{page === "speed" && (
|
||||
<>
|
||||
{submenuHeader("Speed")}
|
||||
{submenuHeader(m.model_picker_speed())}
|
||||
{choiceList(speedChoices, effectiveServiceTier ?? undefined, "default", chooseSpeed)}
|
||||
</>
|
||||
)}
|
||||
@@ -539,7 +541,7 @@ export function OptionPicker({
|
||||
<ChevronDown size={12} />
|
||||
</button>
|
||||
{open && (
|
||||
<div className={`option-menu absolute bottom-[calc(100%_+_8px)] left-0 max-h-95 flex flex-col bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] z-50 overflow-hidden min-w-47.5 p-1.5 [&.align-right]:left-auto [&.align-right]:right-0 [&.drop-down]:bottom-auto [&.drop-down]:top-[calc(100%_+_4px)] [&.session-menu]:left-auto [&.session-menu]:right-1.5 [&.session-menu]:top-[calc(100%_-_2px)] [&.session-menu]:min-w-35 ${choices.some((choice) => choice.description) ? "min-w-80" : ""} ${variant === "field" ? "min-w-full" : ""} ${align === "right" ? "align-right" : ""} ${dropDown ? "drop-down" : ""}`}>
|
||||
<div className={`option-menu absolute bottom-[calc(100%_+_8px)] start-0 max-h-95 flex flex-col bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] z-50 overflow-hidden min-w-47.5 p-1.5 [&.align-right]:start-auto [&.align-right]:end-0 [&.drop-down]:bottom-auto [&.drop-down]:top-[calc(100%_+_4px)] [&.session-menu]:start-auto [&.session-menu]:end-1.5 [&.session-menu]:top-[calc(100%_-_2px)] [&.session-menu]:min-w-35 ${choices.some((choice) => choice.description) ? "min-w-80" : ""} ${variant === "field" ? "min-w-full" : ""} ${align === "right" ? "align-right" : ""} ${dropDown ? "drop-down" : ""}`}>
|
||||
{header && <div className={MODEL_GROUP_CLASS_NAME}>{header}</div>}
|
||||
{pinned && (
|
||||
<>
|
||||
@@ -553,7 +555,7 @@ export function OptionPicker({
|
||||
the behavior comes from instead. A named one ("Adaptive")
|
||||
gets the standard marker. */}
|
||||
<span className="option-default text-muted font-normal">
|
||||
{pinned.label === "Default" ? " · CLI configuration" : " · Default"}
|
||||
{m.model_picker_cli_configuration()}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -573,7 +575,7 @@ export function OptionPicker({
|
||||
the marker — it's one of the tiers, not a separate kind of
|
||||
choice like the pinned sentinel above. */}
|
||||
{!pinned && c.id === defaultId && (
|
||||
<span className="option-default text-muted font-normal"> · Default</span>
|
||||
<span className="option-default text-muted font-normal"> {m.model_picker_default()}</span>
|
||||
)}
|
||||
</span>
|
||||
{c.description && (
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
import { ArrowLeft, ArrowRight, Check, RefreshCw, X } from "lucide-react";
|
||||
import { Wordmark } from "./Wordmark";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -19,7 +21,6 @@ import {
|
||||
} from "../api";
|
||||
import { renderNote } from "./agentNote";
|
||||
import { HarnessLogo } from "./HarnessLogo";
|
||||
import { LIT_SOURCE_NAME, LitSourceLogo, type LitSource } from "./LitSourceLogo";
|
||||
import { onHarnessAuth } from "../events";
|
||||
import { GHOST_BUTTON_CLASS_NAME, MONO_CLASS_NAME, PAPER_TITLE_CLASS_NAME, PRIMARY_BUTTON_CLASS_NAME, SPINNER_CLASS_NAME, STATUS_BADGE_CLASS_NAME } from "../styleClasses";
|
||||
|
||||
@@ -50,8 +51,12 @@ const FINISH_ERROR_CLASS_NAME = [
|
||||
"text-text",
|
||||
].join(" ");
|
||||
|
||||
const RETRY_COPY = "Couldn't reach orx. Check it's still running, then re-check.";
|
||||
const RESEARCH_AREAS = ["AI/ML", "Biology", "Physics", "Other"];
|
||||
const RESEARCH_AREAS = [
|
||||
{ id: "AI/ML", label: m.onboarding_area_ai_ml },
|
||||
{ id: "Biology", label: m.onboarding_area_biology },
|
||||
{ id: "Physics", label: m.onboarding_area_physics },
|
||||
{ id: "Other", label: m.onboarding_area_other },
|
||||
];
|
||||
|
||||
/** First-run walkthrough: choose a local coding agent, verify Git, add a
|
||||
* research profile, then install and open the demo project. The local tool
|
||||
@@ -246,7 +251,7 @@ export function Onboarding({
|
||||
<Wordmark />
|
||||
</div>
|
||||
<h2 className="onb-title mt-0 mx-0 text-[2.5rem] leading-[1.08] tracking-[-0.035em]">
|
||||
A workspace for your research agents
|
||||
{m.onboarding_a_workspace_for_your_research_agents()}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="onb-intro-features relative min-[1120px]:col-start-2 min-[1120px]:row-start-1 min-[1120px]:self-end">
|
||||
@@ -258,33 +263,30 @@ export function Onboarding({
|
||||
<li className="rounded-2xl border border-border bg-background p-6 shadow-[0_14px_36px_color-mix(in_oklab,_var(--text)_6%,_transparent)]">
|
||||
<span>
|
||||
<strong className="mb-1.5 block text-2xl tracking-[-0.015em]">
|
||||
Consolidate your research
|
||||
{m.onboarding_consolidate_your_research()}
|
||||
</strong>
|
||||
<span className="block text-lg leading-[1.55] text-text">
|
||||
Track experiments, artifacts, compute, skills, and code all in one place.
|
||||
{m.onboarding_track_experiments_artifacts_compute_skills_and_code_all()}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
<li className="rounded-2xl border border-border bg-background p-6 shadow-[0_14px_36px_color-mix(in_oklab,_var(--text)_6%,_transparent)]">
|
||||
<span>
|
||||
<strong className="mb-1.5 block text-2xl tracking-[-0.015em]">
|
||||
Ground your agents
|
||||
{m.onboarding_ground_your_agents()}
|
||||
</strong>
|
||||
<span className="block text-lg leading-[1.55] text-text">
|
||||
Connected to <InlineLitSource source="alphaxiv" />,{" "}
|
||||
<InlineLitSource source="biorxiv" />, and{" "}
|
||||
<InlineLitSource source="openalex" />{" "}
|
||||
to ground your agents in the latest research.
|
||||
{m.onboarding_sources_description()}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
<li className="rounded-2xl border border-border bg-background p-6 shadow-[0_14px_36px_color-mix(in_oklab,_var(--text)_6%,_transparent)]">
|
||||
<span>
|
||||
<strong className="mb-1.5 block text-2xl tracking-[-0.015em]">
|
||||
Everything stays local
|
||||
{m.onboarding_everything_stays_local()}
|
||||
</strong>
|
||||
<span className="block text-lg leading-[1.55] text-text">
|
||||
Your code, data, and experiment history stay on your machine.
|
||||
{m.onboarding_your_code_data_and_experiment_history_stay_on()}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
@@ -295,25 +297,25 @@ export function Onboarding({
|
||||
className={`${PRIMARY_BUTTON_CLASS_NAME} !py-3.5 !px-7 !text-xl !rounded-lg`}
|
||||
onClick={() => setStep(1)}
|
||||
>
|
||||
Continue <ArrowRight size={20} />
|
||||
{m.onboarding_continue()} <ArrowRight size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : step === 1 ? (
|
||||
<>
|
||||
<div className="onb-eyebrow text-xl font-semibold text-muted mb-4.5">
|
||||
<Wordmark /> · Step 1 of 2
|
||||
<Wordmark /> {m.onboarding_step_1_of_2()}
|
||||
</div>
|
||||
<h2 className="onb-title mt-0 mx-0 mb-1.5 text-3xl tracking-[-0.01em]">Choose a coding agent</h2>
|
||||
<p className="onb-sub text-text text-base leading-[1.55] mt-0 mx-0 mb-5.5 max-w-120">OpenResearch uses a coding agent already installed on this machine.</p>
|
||||
<h2 className="onb-title mt-0 mx-0 mb-1.5 text-3xl tracking-[-0.01em]">{m.onboarding_choose_a_coding_agent()}</h2>
|
||||
<p className="onb-sub text-text text-base leading-[1.55] mt-0 mx-0 mb-5.5 max-w-120">{m.onboarding_open_research_uses_a_coding_agent_already_installed()}</p>
|
||||
{harnesses !== null && !anyAgentReady && (
|
||||
<p className={ONB_GATE_HINT_CLASS_NAME}>
|
||||
Sign in to at least one agent to continue.
|
||||
{m.onboarding_sign_in_to_at_least_one_agent_to()}
|
||||
</p>
|
||||
)}
|
||||
{harnesses !== null && anyAgentReady && preferredHarness === null && (
|
||||
<p className={ONB_GATE_HINT_CLASS_NAME}>
|
||||
Choose a coding agent to continue.
|
||||
{m.onboarding_choose_a_coding_agent_to_continue()}
|
||||
</p>
|
||||
)}
|
||||
<div className="onb-cards flex flex-col gap-3.5">
|
||||
@@ -328,10 +330,10 @@ export function Onboarding({
|
||||
))
|
||||
) : harnessError ? (
|
||||
// Never a spinner next to an error — detection isn't running.
|
||||
<div className={ONB_CARD_META_CLASS_NAME}>{RETRY_COPY}</div>
|
||||
<div className={ONB_CARD_META_CLASS_NAME}>{m.onboarding_retry_connection()}</div>
|
||||
) : (
|
||||
<div className="onb-loading flex items-center gap-2 text-subtext text-md py-2 px-0">
|
||||
<span className={SPINNER_CLASS_NAME} /> Detecting Claude Code, Codex, OpenCode…
|
||||
<span className={SPINNER_CLASS_NAME} /> {m.onboarding_detecting_claude_code_codex_open_code()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -339,24 +341,24 @@ export function Onboarding({
|
||||
<div className="onb-git-check mt-7" role="status" aria-live="polite">
|
||||
<LocalGitCard gitVersion={gitVersion} error={gitError} />
|
||||
{gitError ? (
|
||||
<p className={GIT_RETRY_HINT_CLASS_NAME}>{RETRY_COPY}</p>
|
||||
<p className={GIT_RETRY_HINT_CLASS_NAME}>{m.onboarding_retry_connection()}</p>
|
||||
) : (
|
||||
<p className={GIT_RETRY_HINT_CLASS_NAME}>
|
||||
Git is required for local experiments. Install Git, then re-check.
|
||||
{m.onboarding_git_is_required_for_local_experiments_install_git()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="onb-actions flex items-center gap-2.5 mt-5.5">
|
||||
<button className={GHOST_BUTTON_CLASS_NAME} onClick={() => setStep(0)}>
|
||||
<ArrowLeft size={12} /> Back
|
||||
<ArrowLeft size={12} /> {m.onboarding_back()}
|
||||
</button>
|
||||
{(harnessError ||
|
||||
gitError ||
|
||||
gitVersion === null ||
|
||||
(harnesses !== null && !anyAgentReady)) && (
|
||||
<button className={GHOST_BUTTON_CLASS_NAME} onClick={() => load(true, true)} disabled={checking}>
|
||||
<RefreshCw size={12} className={checking ? "spin animate-[settings-spin_0.9s_linear_infinite]" : ""} /> Re-check
|
||||
<RefreshCw size={12} className={checking ? "spin animate-[settings-spin_0.9s_linear_infinite]" : ""} /> {m.onboarding_re_check()}
|
||||
</button>
|
||||
)}
|
||||
<div style={{ flex: 1 }} />
|
||||
@@ -366,45 +368,45 @@ export function Onboarding({
|
||||
disabled={checking || !anyAgentReady || preferredHarness === null || !gitReady}
|
||||
title={
|
||||
checking
|
||||
? "Waiting for the local tool checks"
|
||||
? m.onboarding_waiting_tool_checks()
|
||||
: !anyAgentReady
|
||||
? "Sign in to at least one coding agent to continue"
|
||||
? m.onboarding_sign_in_agent_to_continue()
|
||||
: preferredHarness === null
|
||||
? "Choose your preferred coding agent"
|
||||
? m.onboarding_choose_preferred_agent()
|
||||
: gitError
|
||||
? "Re-check Git before continuing"
|
||||
? m.onboarding_recheck_git_to_continue()
|
||||
: gitVersion === undefined
|
||||
? "Waiting for the Git check"
|
||||
? m.onboarding_waiting_git_check()
|
||||
: gitVersion === null
|
||||
? "Install Git to continue"
|
||||
? m.onboarding_install_git_to_continue()
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Continue <ArrowRight size={13} />
|
||||
{m.onboarding_continue()} <ArrowRight size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="onb-eyebrow text-xl font-semibold text-muted mb-4.5">
|
||||
<Wordmark /> · Step 2 of 2
|
||||
<Wordmark /> {m.onboarding_step_2_of_2()}
|
||||
</div>
|
||||
<h2 className="onb-title mt-0 mx-0 mb-1.5 text-3xl tracking-[-0.01em] onb-profile-title mb-5.5">Tell us about your research</h2>
|
||||
<h2 className="onb-title mt-0 mx-0 mb-1.5 text-3xl tracking-[-0.01em] onb-profile-title mb-5.5">{m.onboarding_tell_us_about_your_research()}</h2>
|
||||
<div className="onb-cards flex flex-col gap-2.5">
|
||||
<div className={ONB_CARD_CLASS_NAME}>
|
||||
<fieldset className="onb-fieldset border-0 mt-0 mx-0 mb-4.5 p-0 [&_legend]:text-base [&_legend]:font-semibold [&_legend]:mb-1.5">
|
||||
<legend>What areas are you interested in?</legend>
|
||||
<p className="onb-field-hint text-muted text-sm leading-[1.4] mt-0 mx-0 mb-2">Choose one or more.</p>
|
||||
<legend>{m.onboarding_what_areas_are_you_interested_in()}</legend>
|
||||
<p className="onb-field-hint text-muted text-sm leading-[1.4] mt-0 mx-0 mb-2">{m.onboarding_choose_one_or_more()}</p>
|
||||
<div className="onb-area-options grid grid-cols-[repeat(2,_minmax(0,_1fr))] gap-2">
|
||||
{RESEARCH_AREAS.map((area) => (
|
||||
<label key={area} className="onb-area-option flex items-center gap-2 border border-border rounded-md cursor-pointer py-[9px] px-2.5 [&:has(input:checked)]:border-accent [&:has(input:checked)]:bg-primary-subtle [&_input]:m-0">
|
||||
<label key={area.id} className="onb-area-option flex items-center gap-2 border border-border rounded-md cursor-pointer py-[9px] px-2.5 [&:has(input:checked)]:border-accent [&:has(input:checked)]:bg-primary-subtle [&_input]:m-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={researchAreas.includes(area)}
|
||||
onChange={() => toggleResearchArea(area)}
|
||||
checked={researchAreas.includes(area.id)}
|
||||
onChange={() => toggleResearchArea(area.id)}
|
||||
disabled={finishing}
|
||||
/>
|
||||
<span>{area}</span>
|
||||
<span>{area.label()}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -414,13 +416,13 @@ export function Onboarding({
|
||||
value={otherArea}
|
||||
onChange={(event) => setOtherArea(event.target.value)}
|
||||
disabled={finishing}
|
||||
placeholder="Tell us your other research area"
|
||||
aria-label="Other research area"
|
||||
placeholder={m.onboarding_tell_us_your_other_research_area()}
|
||||
aria-label={m.onboarding_other_research_area()}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
<label className="onb-field-label text-base font-semibold mb-1.5" htmlFor="onb-background">
|
||||
Research background
|
||||
{m.onboarding_research_background()}
|
||||
</label>
|
||||
<textarea
|
||||
id="onb-background"
|
||||
@@ -429,14 +431,13 @@ export function Onboarding({
|
||||
onChange={(e) => setBackground(e.target.value)}
|
||||
disabled={finishing}
|
||||
rows={4}
|
||||
placeholder="e.g. I work on sample-efficient RL for LLM post-training, focused on reward-model-free methods."
|
||||
placeholder={m.onboarding_e_g_i_work_on_sample_efficient_rl()}
|
||||
/>
|
||||
<label className="onb-field-label text-base font-semibold mb-1.5" htmlFor="onb-paper-search">
|
||||
Representative papers
|
||||
{m.onboarding_representative_papers()}
|
||||
</label>
|
||||
<p className="onb-field-hint text-muted text-sm leading-[1.4] mt-0 mx-0 mb-2">
|
||||
Add papers that represent your research interests, including papers by other
|
||||
authors.
|
||||
{m.onboarding_add_papers_that_represent_your_research_interests_including()}
|
||||
</p>
|
||||
<div className="onb-paper-search flex flex-col gap-1.5 mt-3 [&_input]:w-full">
|
||||
<input
|
||||
@@ -444,12 +445,12 @@ export function Onboarding({
|
||||
value={paperQuery}
|
||||
onChange={(e) => setPaperQuery(e.target.value)}
|
||||
disabled={finishing}
|
||||
placeholder="Search alphaXiv by title to link a paper…"
|
||||
placeholder={m.onboarding_search_alpha_xiv_by_title_to_link_a()}
|
||||
/>
|
||||
{searchingPapers ? (
|
||||
<div className={ONB_CARD_META_CLASS_NAME}>Searching alphaXiv…</div>
|
||||
<div className={ONB_CARD_META_CLASS_NAME}>{m.onboarding_searching_alpha_xiv()}</div>
|
||||
) : paperHits.length > 0 ? (
|
||||
<div className="onb-paper-results flex flex-col border border-border rounded-md max-h-50 overflow-y-auto [&_button]:flex [&_button]:flex-col [&_button]:items-start [&_button]:gap-0.5 [&_button]:py-2 [&_button]:px-2.5 [&_button]:bg-none [&_button]:bg-transparent [&_button]:border-0 [&_button]:border-b [&_button]:border-b-border-variant [&_button]:text-left [&_button]:[font:inherit] [&_button]:text-text [&_button]:cursor-pointer [&_button:last-child]:border-b-0 [&_button:hover]:bg-surface [&_.title]:text-md [&_.title]:font-medium [&_.id]:font-mono [&_.id]:text-xs [&_.id]:text-muted">
|
||||
<div className="onb-paper-results flex flex-col border border-border rounded-md max-h-50 overflow-y-auto [&_button]:flex [&_button]:flex-col [&_button]:items-start [&_button]:gap-0.5 [&_button]:py-2 [&_button]:px-2.5 [&_button]:bg-none [&_button]:bg-transparent [&_button]:border-0 [&_button]:border-b [&_button]:border-b-border-variant [&_button]:text-start [&_button]:[font:inherit] [&_button]:text-text [&_button]:cursor-pointer [&_button:last-child]:border-b-0 [&_button:hover]:bg-surface [&_.title]:text-md [&_.title]:font-medium [&_.id]:font-mono [&_.id]:text-xs [&_.id]:text-muted">
|
||||
{paperHits.map((h) => (
|
||||
<button
|
||||
key={h.paperId}
|
||||
@@ -467,12 +468,12 @@ export function Onboarding({
|
||||
{papers.length > 0 && (
|
||||
<div className="onb-paper-chips flex flex-wrap gap-1.5 mt-2.5">
|
||||
{papers.map((p) => (
|
||||
<span key={p.paperId} className="onb-paper-chip inline-flex items-center gap-1.5 pt-1 pr-1 pb-1 pl-2.5 border border-border rounded-sm bg-surface text-sm max-w-full [&_.title]:font-medium [&_.title]:overflow-hidden [&_.title]:text-ellipsis [&_.title]:whitespace-nowrap [&_.title]:max-w-60 [&_.id]:font-mono [&_.id]:text-xs [&_.id]:text-muted [&_button]:inline-flex [&_button]:items-center [&_button]:justify-center [&_button]:p-0.5 [&_button]:border-0 [&_button]:bg-none [&_button]:bg-transparent [&_button]:text-muted [&_button]:cursor-pointer [&_button]:rounded-xs [&_button:hover]:text-text [&_button:hover]:bg-panel">
|
||||
<span key={p.paperId} className="onb-paper-chip inline-flex items-center gap-1.5 pt-1 pe-1 pb-1 ps-2.5 border border-border rounded-sm bg-surface text-sm max-w-full [&_.title]:font-medium [&_.title]:overflow-hidden [&_.title]:text-ellipsis [&_.title]:whitespace-nowrap [&_.title]:max-w-60 [&_.id]:font-mono [&_.id]:text-xs [&_.id]:text-muted [&_button]:inline-flex [&_button]:items-center [&_button]:justify-center [&_button]:p-0.5 [&_button]:border-0 [&_button]:bg-none [&_button]:bg-transparent [&_button]:text-muted [&_button]:cursor-pointer [&_button]:rounded-xs [&_button:hover]:text-text [&_button:hover]:bg-panel">
|
||||
<span className={PAPER_TITLE_CLASS_NAME}>{p.title || p.paperId}</span>
|
||||
<span className="id">{p.paperId}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Remove ${p.paperId}`}
|
||||
aria-label={m.a11y_remove_item({ name: ltr(p.paperId) })}
|
||||
onClick={() => removePaper(p.paperId)}
|
||||
disabled={finishing}
|
||||
>
|
||||
@@ -487,13 +488,13 @@ export function Onboarding({
|
||||
{!researchProfileValid && (
|
||||
<p className="onb-profile-hint text-accent-red text-sm mt-2 mx-0 mb-0">
|
||||
{researchAreas.length === 0
|
||||
? "Choose at least one research area to continue."
|
||||
: "Describe your research area to continue."}
|
||||
? m.onboarding_choose_area_to_continue()
|
||||
: m.onboarding_describe_area_to_continue()}
|
||||
</p>
|
||||
)}
|
||||
<div className="onb-actions flex items-center gap-2.5 mt-5.5">
|
||||
<button className={GHOST_BUTTON_CLASS_NAME} onClick={() => setStep(1)} disabled={finishing}>
|
||||
<ArrowLeft size={12} /> Back
|
||||
<ArrowLeft size={12} /> {m.onboarding_back()}
|
||||
</button>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button
|
||||
@@ -503,18 +504,18 @@ export function Onboarding({
|
||||
>
|
||||
{finishing ? (
|
||||
<>
|
||||
<span className={SPINNER_CLASS_NAME} /> Setting things up…
|
||||
<span className={SPINNER_CLASS_NAME} /> {m.onboarding_setting_things_up()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Get started <ArrowRight size={13} />
|
||||
{m.onboarding_get_started()} <ArrowRight size={13} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{preferredHarness === null && (
|
||||
<p className={FINISH_ERROR_CLASS_NAME}>
|
||||
Your selected agent is no longer ready. Go back to Step 1 and choose another.
|
||||
{m.onboarding_your_selected_agent_is_no_longer_ready_go()}
|
||||
</p>
|
||||
)}
|
||||
{finishError && <p className={FINISH_ERROR_CLASS_NAME}>{finishError}</p>}
|
||||
@@ -525,20 +526,6 @@ export function Onboarding({
|
||||
);
|
||||
}
|
||||
|
||||
function InlineLitSource({ source }: { source: LitSource }) {
|
||||
return (
|
||||
<span className="whitespace-nowrap">
|
||||
<LitSourceLogo
|
||||
source={source}
|
||||
size={18}
|
||||
decorative
|
||||
className="mr-1 inline-flex align-[-0.15em] !rounded-none !bg-transparent !p-0 !shadow-none"
|
||||
/>
|
||||
{LIT_SOURCE_NAME[source]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** Fast-search titles carry scrape cruft: "[1706.03762] Title - arXiv".
|
||||
* Kept in sync with NewProjectForm's cleanTitle. */
|
||||
function cleanPaperTitle(title: string): string {
|
||||
@@ -548,13 +535,13 @@ function cleanPaperTitle(title: string): string {
|
||||
/** Agent notes carry the command to run in backticks (`claude auth login`) —
|
||||
* render those spans as code so they read as something to type, not prose. */
|
||||
function agentBadge(h: Harness): { cls: string; label: string } {
|
||||
if (h.agentReady) return { cls: "st-done", label: "Signed in" };
|
||||
if (!h.installed) return { cls: "st-idle", label: "Not detected" };
|
||||
if (h.installBroken) return { cls: "st-starting", label: "Install broken" };
|
||||
if (h.authState === "unknown") return { cls: "st-starting", label: "Unable to verify" };
|
||||
if (h.authState === "unsupported") return { cls: "st-starting", label: "Update required" };
|
||||
if (h.installed) return { cls: "st-starting", label: "Not signed in" };
|
||||
return { cls: "st-idle", label: "Not detected" };
|
||||
if (h.agentReady) return { cls: "st-done", label: m.onboarding_signed_in() };
|
||||
if (!h.installed) return { cls: "st-idle", label: m.onboarding_not_detected() };
|
||||
if (h.installBroken) return { cls: "st-starting", label: m.onboarding_install_broken() };
|
||||
if (h.authState === "unknown") return { cls: "st-starting", label: m.onboarding_unable_to_verify() };
|
||||
if (h.authState === "unsupported") return { cls: "st-starting", label: m.onboarding_update_required() };
|
||||
if (h.installed) return { cls: "st-starting", label: m.onboarding_not_signed_in() };
|
||||
return { cls: "st-idle", label: m.onboarding_not_detected() };
|
||||
}
|
||||
|
||||
function selectionFor(harness: Harness): AgentSelection {
|
||||
@@ -581,7 +568,7 @@ function AgentCard({
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const badge = agentBadge(h);
|
||||
const visibleBadge = selected ? { cls: "st-done", label: "Selected" } : badge;
|
||||
const visibleBadge = selected ? { cls: "st-done", label: m.onboarding_selected() } : badge;
|
||||
const version = h.version?.replace(/\s*\(.*\)$/, "");
|
||||
const meta = [
|
||||
version,
|
||||
@@ -609,7 +596,7 @@ function AgentCard({
|
||||
// disabled button, so the copy button on its `agentNote` command stays live.
|
||||
if (!h.agentReady) {
|
||||
return (
|
||||
<div className="onb-card flex flex-col gap-2.5 bg-background border border-border rounded-lg py-5.5 px-6 onb-agent-choice w-full text-inherit [font:inherit] text-left transition-[border-color,box-shadow] duration-120 ease-standard [button&]:cursor-pointer [button&:hover]:border-muted [&.selected]:border-accent [&.selected]:shadow-[0_0_0_1px_var(--accent)]">
|
||||
<div className="onb-card flex flex-col gap-2.5 bg-background border border-border rounded-lg py-5.5 px-6 onb-agent-choice w-full text-inherit [font:inherit] text-start transition-[border-color,box-shadow] duration-120 ease-standard [button&]:cursor-pointer [button&:hover]:border-muted [&.selected]:border-accent [&.selected]:shadow-[0_0_0_1px_var(--accent)]">
|
||||
{head}
|
||||
<div className={ONB_CARD_META_CLASS_NAME}>{renderNote(h.agentNote)}</div>
|
||||
</div>
|
||||
@@ -618,13 +605,13 @@ function AgentCard({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`onb-card flex flex-col gap-2.5 bg-background border border-border rounded-lg py-5.5 px-6 onb-agent-choice w-full text-inherit [font:inherit] text-left transition-[border-color,box-shadow] duration-120 ease-standard [button&]:cursor-pointer [button&:hover]:border-muted [&.selected]:border-accent [&.selected]:shadow-[0_0_0_1px_var(--accent)]${selected ? " selected" : ""}`}
|
||||
className={`onb-card flex flex-col gap-2.5 bg-background border border-border rounded-lg py-5.5 px-6 onb-agent-choice w-full text-inherit [font:inherit] text-start transition-[border-color,box-shadow] duration-120 ease-standard [button&]:cursor-pointer [button&:hover]:border-muted [&.selected]:border-accent [&.selected]:shadow-[0_0_0_1px_var(--accent)]${selected ? " selected" : ""}`}
|
||||
aria-pressed={selected}
|
||||
onClick={onSelect}
|
||||
>
|
||||
{head}
|
||||
<div className={`onb-card-detail ${MONO_CLASS_NAME}`}>
|
||||
{h.account ?? "API key"}
|
||||
{h.account ?? m.onboarding_api_key()}
|
||||
{h.plan ? ` · ${h.plan}` : ""}
|
||||
</div>
|
||||
<div
|
||||
@@ -647,16 +634,16 @@ function LocalGitCard({
|
||||
return (
|
||||
<div className={ONB_CARD_CLASS_NAME}>
|
||||
<div className="onb-card-head flex items-center justify-between gap-3">
|
||||
<span className="onb-card-name font-semibold text-base">Local Git</span>
|
||||
<span className="onb-card-name font-semibold text-base">{m.onboarding_local_git()}</span>
|
||||
<span
|
||||
className={`${STATUS_BADGE_CLASS_NAME} ${gitVersion ? "st-done" : error || gitVersion === null ? "st-failed" : "st-starting"}`}
|
||||
>
|
||||
{gitVersion ? <Check size={12} strokeWidth={3} /> : <span className="dot" />}
|
||||
{gitVersion ? "Ready" : error ? "Check failed" : gitVersion === null ? "Not found" : "Checking"}
|
||||
{gitVersion ? m.onboarding_ready() : error ? m.onboarding_check_failed() : gitVersion === null ? m.onboarding_not_found() : m.onboarding_checking()}
|
||||
</span>
|
||||
</div>
|
||||
{(gitVersion || (!error && gitVersion === undefined)) && (
|
||||
<div className={ONB_CARD_META_CLASS_NAME}>{gitVersion ?? "Checking Git…"}</div>
|
||||
<div className={ONB_CARD_META_CLASS_NAME}>{gitVersion ?? m.onboarding_checking_git()}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
// Overleaf controls for a .tex tab, shown as a note strip under the file header
|
||||
// like the compile log and the install hint beside it.
|
||||
//
|
||||
@@ -10,31 +11,31 @@
|
||||
import { useState } from "react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import type { OverleafSync } from "../useOverleafSync";
|
||||
import { getLocale } from "../paraglide/runtime.js";
|
||||
import { autoDir, ltr } from "../i18n";
|
||||
import { BUTTON_CLASS_NAME, GHOST_BUTTON_CLASS_NAME, SPINNER_CLASS_NAME } from "../styleClasses";
|
||||
|
||||
const list = (paths: string[]) => paths.join(", ");
|
||||
const list = (paths: string[]) => autoDir(new Intl.ListFormat(getLocale()).format(paths.map(ltr)));
|
||||
|
||||
/** What the last sync did. A failure outranks everything below it, so the line
|
||||
* never reads "in step" above a red one saying otherwise; conflicts carry their
|
||||
* own rows. */
|
||||
function status(overleaf: OverleafSync): string {
|
||||
if (overleaf.error) return "The last sync did not finish.";
|
||||
if (overleaf.syncing) return "Syncing with Overleaf…";
|
||||
if (overleaf.blocked) return "Save this file to sync it with Overleaf.";
|
||||
if (overleaf.error) return m.overleaf_last_sync_failed();
|
||||
if (overleaf.syncing) return m.overleaf_syncing();
|
||||
if (overleaf.blocked) return m.overleaf_save_to_sync();
|
||||
const last = overleaf.last;
|
||||
if (!last) return "This paper stays in step with Overleaf.";
|
||||
const moved: string[] = [];
|
||||
if (last.pulled.length) moved.push(`pulled ${list(last.pulled)}`);
|
||||
if (last.pushed.length) moved.push(`pushed ${list(last.pushed)}`);
|
||||
if (!moved.length) return last.conflicts.length ? "Nothing could be synced." : "In step with Overleaf.";
|
||||
const sentence = moved.join(", ");
|
||||
return `${sentence[0].toUpperCase()}${sentence.slice(1)}.`;
|
||||
if (!last) return m.overleaf_paper_stays_in_sync();
|
||||
if (last.pulled.length && last.pushed.length) return m.overleaf_pulled_and_pushed({ pulled: list(last.pulled), pushed: list(last.pushed) });
|
||||
if (last.pulled.length) return m.overleaf_pulled({ paths: list(last.pulled) });
|
||||
if (last.pushed.length) return m.overleaf_pushed({ paths: list(last.pushed) });
|
||||
return last.conflicts.length ? m.overleaf_nothing_synced() : m.overleaf_in_sync();
|
||||
}
|
||||
|
||||
function UploadLink({ href }: { href: string }) {
|
||||
return (
|
||||
<a className="text-sm text-subtext whitespace-nowrap" href={href} target="_blank" rel="noreferrer">
|
||||
Upload a copy as a new project ↗
|
||||
{m.overleaf_panel_upload_a_copy_as_a_new_project()}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -85,15 +86,15 @@ export function OverleafPanel({ overleaf }: { overleaf: OverleafSync }) {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Open in Overleaf <ExternalLink size={11} />
|
||||
{m.overleaf_panel_open_in_overleaf()} <ExternalLink size={11} />
|
||||
</a>
|
||||
<button
|
||||
className={BUTTON_CLASS_NAME}
|
||||
disabled={overleaf.syncing || overleaf.blocked}
|
||||
data-tip={overleaf.blocked ? "Save the file first" : undefined}
|
||||
data-tip={overleaf.blocked ? m.overleaf_save_first() : undefined}
|
||||
onClick={() => overleaf.sync()}
|
||||
>
|
||||
Sync now
|
||||
{m.overleaf_panel_sync_now()}
|
||||
</button>
|
||||
<button
|
||||
className={GHOST_BUTTON_CLASS_NAME}
|
||||
@@ -104,28 +105,27 @@ export function OverleafPanel({ overleaf }: { overleaf: OverleafSync }) {
|
||||
})
|
||||
}
|
||||
>
|
||||
Unlink
|
||||
{m.overleaf_panel_unlink()}
|
||||
</button>
|
||||
</div>
|
||||
{conflicts.map((path) => (
|
||||
<div key={path} className="flex items-center flex-wrap gap-2 text-sm text-accent-red">
|
||||
<span className="flex-1 min-w-0">
|
||||
<code className="font-mono">{path}</code> changed here and on Overleaf. Both copies
|
||||
are untouched — choose which one to keep.
|
||||
<code className="font-mono">{path}</code> {m.overleaf_panel_changed_here_and_on_overleaf_both_copies_are()}
|
||||
</span>
|
||||
<button
|
||||
className={BUTTON_CLASS_NAME}
|
||||
disabled={overleaf.syncing || overleaf.blocked}
|
||||
onClick={() => overleaf.sync({ [path]: "keep-local" })}
|
||||
>
|
||||
Keep this copy
|
||||
{m.overleaf_panel_keep_this_copy()}
|
||||
</button>
|
||||
<button
|
||||
className={BUTTON_CLASS_NAME}
|
||||
disabled={overleaf.syncing || overleaf.blocked}
|
||||
onClick={() => overleaf.sync({ [path]: "take-overleaf" })}
|
||||
>
|
||||
Use Overleaf's
|
||||
{m.overleaf_panel_use_overleaf_apos_s()}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -138,7 +138,7 @@ export function OverleafPanel({ overleaf }: { overleaf: OverleafSync }) {
|
||||
<div className="flex items-center flex-wrap gap-3">
|
||||
<UploadLink href={overleaf.uploadUrl} />
|
||||
<button type="button" className={GHOST_BUTTON_CLASS_NAME} onClick={replaceToken}>
|
||||
Replace the Overleaf token
|
||||
{m.overleaf_panel_replace_the_overleaf_token()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,8 +149,8 @@ export function OverleafPanel({ overleaf }: { overleaf: OverleafSync }) {
|
||||
<form className="flex flex-col gap-1.5" onSubmit={submit}>
|
||||
<div className="text-sm text-subtext">
|
||||
{needsToken
|
||||
? "Paste an Overleaf Git authentication token to keep this paper in step with an Overleaf project. Create one in Overleaf under Account Settings — Git integration comes with a paid Overleaf plan."
|
||||
: "Paste the URL of the Overleaf project this paper belongs to. Overleaf cannot create one over Git, so open or create the project there first."}
|
||||
? m.overleaf_token_instructions()
|
||||
: m.overleaf_url_instructions()}
|
||||
</div>
|
||||
<div className="flex items-center flex-wrap gap-2">
|
||||
<input
|
||||
@@ -158,11 +158,11 @@ export function OverleafPanel({ overleaf }: { overleaf: OverleafSync }) {
|
||||
type={needsToken ? "password" : "text"}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder={needsToken ? "Overleaf Git token" : "https://www.overleaf.com/project/…"}
|
||||
placeholder={needsToken ? m.overleaf_git_token() : "https://www.overleaf.com/project/…"}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button type="submit" className={BUTTON_CLASS_NAME} disabled={busy || !value.trim()}>
|
||||
{busy ? (needsToken ? "Saving…" : "Checking…") : needsToken ? "Save token" : "Link and sync"}
|
||||
{busy ? (needsToken ? m.common_saving() : m.common_checking()) : needsToken ? m.overleaf_save_token() : m.overleaf_link_and_sync()}
|
||||
</button>
|
||||
<a
|
||||
className="text-sm text-subtext whitespace-nowrap"
|
||||
@@ -174,7 +174,7 @@ export function OverleafPanel({ overleaf }: { overleaf: OverleafSync }) {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{needsToken ? "Create a token ↗" : "My projects ↗"}
|
||||
{needsToken ? m.overleaf_create_token() : m.overleaf_my_projects()}
|
||||
</a>
|
||||
</div>
|
||||
{formError && <div className="text-sm text-accent-red whitespace-pre-wrap">{formError}</div>}
|
||||
@@ -186,14 +186,14 @@ export function OverleafPanel({ overleaf }: { overleaf: OverleafSync }) {
|
||||
className={GHOST_BUTTON_CLASS_NAME}
|
||||
onClick={() => setReplacingToken(false)}
|
||||
>
|
||||
Cancel
|
||||
{m.overleaf_panel_cancel()}
|
||||
</button>
|
||||
) : (
|
||||
// A token can be rejected before this paper is ever linked, so the
|
||||
// way to replace it has to live in this state too.
|
||||
overleaf.hasToken && (
|
||||
<button type="button" className={GHOST_BUTTON_CLASS_NAME} onClick={replaceToken}>
|
||||
Replace the Overleaf token
|
||||
{m.overleaf_panel_replace_the_overleaf_token()}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
import { ChevronDown, CornerDownLeft, ScrollText } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { tabOpenGestureHandlers, type TabOpenIntent } from "../tabPreview";
|
||||
@@ -28,10 +30,10 @@ const PROMPT_ACTIONS_CLASS_NAME = [
|
||||
"[&_.plan-strip-primary:hover:not(:disabled)]:bg-[color-mix(in_oklab,_var(--text)_85%,_var(--base))]",
|
||||
"[&_.plan-strip-primary:hover:not(:disabled)]:border-text",
|
||||
"[&_.plan-strip-primary:hover:not(:disabled)]:text-background",
|
||||
"[&_.plan-strip-caret]:rounded-tl-none [&_.plan-strip-caret]:rounded-bl-none",
|
||||
"[&_.plan-strip-caret]:rounded-ss-none [&_.plan-strip-caret]:rounded-es-none",
|
||||
"[&_.plan-strip-caret]:py-0 [&_.plan-strip-caret]:px-1.5 [&_.plan-strip-caret]:flex",
|
||||
"[&_.plan-strip-caret]:items-center",
|
||||
"[&_.plan-strip-caret]:border-l [&_.plan-strip-caret]:border-l-[color-mix(in_oklab,_var(--base)_35%,_var(--text))]",
|
||||
"[&_.plan-strip-caret]:border-s [&_.plan-strip-caret]:border-s-[color-mix(in_oklab,_var(--base)_35%,_var(--text))]",
|
||||
].join(" ");
|
||||
|
||||
/** Docked strip above the composer while a plan awaits the user's decision.
|
||||
@@ -48,7 +50,7 @@ const PROMPT_ACTIONS_CLASS_NAME = [
|
||||
* default accept action. The caret menu holds Accept and bypass all
|
||||
* (skip every gate, not just Auto's). No plain Accept edits tier here —
|
||||
* the app has no story for partial (edits-only) approval.
|
||||
* - Open plan: link in the title row → the right-pane plan tab. */
|
||||
* - Open plan: link in the title row → the end-pane plan tab. */
|
||||
export function PlanStrip({
|
||||
synthesized,
|
||||
agentLabel,
|
||||
@@ -105,25 +107,28 @@ export function PlanStrip({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="plan-strip relative w-full mt-0 mx-0 mb-2.5 py-[11px] px-[13px] flex flex-col items-stretch gap-2.5 border border-border border-l-[3px] border-l-accent-blue rounded-md bg-surface shadow-[0_2px_10px_rgb(0_0_0_/_6%)]">
|
||||
<div className="plan-strip relative w-full mt-0 mx-0 mb-2.5 py-[11px] px-[13px] flex flex-col items-stretch gap-2.5 border border-border border-s-[3px] border-s-accent-blue rounded-md bg-surface shadow-[0_2px_10px_rgb(0_0_0_/_6%)]">
|
||||
<div className="plan-strip-info flex items-baseline gap-2 min-w-0">
|
||||
<ScrollText size={14} className="plan-strip-icon text-accent-blue shrink-0 self-center" />
|
||||
<span className="plan-strip-title text-md font-semibold whitespace-nowrap">
|
||||
{synthesized ? `${agentLabel} is ready to proceed` : `${agentLabel} proposed a plan`}
|
||||
<span dir="auto" className="plan-strip-title text-md font-semibold whitespace-nowrap">
|
||||
{synthesized
|
||||
? m.plan_strip_agent_ready({ agent: ltr(agentLabel) })
|
||||
: m.plan_strip_agent_proposed({ agent: ltr(agentLabel) })}
|
||||
</span>
|
||||
<button
|
||||
className="plan-strip-open ml-auto p-0 border-0 bg-none bg-transparent text-accent-blue text-md cursor-pointer whitespace-nowrap shrink-0 [&:hover]:underline"
|
||||
className="plan-strip-open ms-auto p-0 border-0 bg-none bg-transparent text-accent-blue text-md cursor-pointer whitespace-nowrap shrink-0 [&:hover]:underline"
|
||||
{...tabOpenGestureHandlers<HTMLButtonElement>(onView)}
|
||||
>
|
||||
Open plan
|
||||
{m.plan_strip_open_plan()}
|
||||
</button>
|
||||
</div>
|
||||
{revising ? (
|
||||
<>
|
||||
<textarea
|
||||
dir="auto"
|
||||
ref={textareaRef}
|
||||
className="plan-strip-revise-input w-full resize-none border border-border rounded-md py-[9px] px-[11px] text-md font-[inherit] bg-background text-text [&:focus]:border-accent-blue"
|
||||
placeholder="What should change? (optional)"
|
||||
placeholder={m.plan_strip_what_should_change_optional()}
|
||||
rows={2}
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
@@ -146,11 +151,11 @@ export function PlanStrip({
|
||||
setRevising(false);
|
||||
}}
|
||||
>
|
||||
Back
|
||||
{m.plan_strip_back()}
|
||||
</button>
|
||||
<span className="plan-strip-spacer flex-1" />
|
||||
<button className="btn-primary plan-strip-primary" onClick={submitRevision}>
|
||||
Revise
|
||||
{m.plan_strip_revise()}
|
||||
<CornerDownLeft size={13} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -158,40 +163,40 @@ export function PlanStrip({
|
||||
) : (
|
||||
<div className={PROMPT_ACTIONS_CLASS_NAME}>
|
||||
<button className="btn-ghost" onClick={onReject}>
|
||||
Reject
|
||||
{m.plan_strip_reject()}
|
||||
</button>
|
||||
<button className="btn-ghost" onClick={() => setRevising(true)}>
|
||||
Revise…
|
||||
{m.plan_strip_revise_05bacc9()}
|
||||
</button>
|
||||
<span className="plan-strip-spacer flex-1" />
|
||||
{showResumeModes ? (
|
||||
<div className="plan-strip-approve relative flex [&_.btn-primary:first-child]:rounded-tr-none [&_.btn-primary:first-child]:rounded-br-none" ref={menuRef}>
|
||||
<button className="btn-primary plan-strip-primary" onClick={() => onApprove("auto")}>
|
||||
Accept and auto mode
|
||||
{m.plan_strip_accept_and_auto_mode()}
|
||||
</button>
|
||||
<button
|
||||
className="btn-primary plan-strip-primary plan-strip-caret"
|
||||
aria-label="More approval options"
|
||||
aria-label={m.plan_strip_more_approval_options()}
|
||||
onClick={() => setMenuOpen((o) => !o)}
|
||||
>
|
||||
<ChevronDown size={13} />
|
||||
</button>
|
||||
{menuOpen && (
|
||||
<div className="plan-strip-menu absolute right-0 bottom-[calc(100%_+_4px)] flex flex-col min-w-47.5 p-1 border border-border rounded-md bg-surface shadow-[0_6px_20px_rgb(0_0_0_/_12%)] z-6 [&_button]:text-left [&_button]:py-[7px] [&_button]:px-[9px] [&_button]:border-0 [&_button]:rounded-sm [&_button]:bg-transparent [&_button]:text-text [&_button]:text-md [&_button]:cursor-pointer [&_button:hover]:bg-[var(--surface-2,_rgb(0_0_0_/_5%))]">
|
||||
<div className="plan-strip-menu absolute end-0 bottom-[calc(100%_+_4px)] flex flex-col min-w-47.5 p-1 border border-border rounded-md bg-surface shadow-[0_6px_20px_rgb(0_0_0_/_12%)] z-6 [&_button]:text-start [&_button]:py-[7px] [&_button]:px-[9px] [&_button]:border-0 [&_button]:rounded-sm [&_button]:bg-transparent [&_button]:text-text [&_button]:text-md [&_button]:cursor-pointer [&_button:hover]:bg-[var(--surface-2,_rgb(0_0_0_/_5%))]">
|
||||
<button
|
||||
onClick={() => {
|
||||
setMenuOpen(false);
|
||||
onApprove("bypassPermissions");
|
||||
}}
|
||||
>
|
||||
Accept and bypass all
|
||||
{m.plan_strip_accept_and_bypass_all()}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button className="btn-primary plan-strip-primary" onClick={() => onApprove()}>
|
||||
Accept plan
|
||||
{m.plan_strip_accept_plan()}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { autoDir, ltr } from "../i18n";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { GitHubMark } from "./BackendLogos";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
deleteProject,
|
||||
fmtNumber,
|
||||
listProjectActivity,
|
||||
timeAgo,
|
||||
type Project,
|
||||
@@ -90,7 +93,7 @@ export function NewProjectDialog({
|
||||
aria-labelledby="new-project-dialog-title"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<h2 id="new-project-dialog-title">New project</h2>
|
||||
<h2 id="new-project-dialog-title">{m.projects_home_new_project()}</h2>
|
||||
<NewProjectForm onCancel={onClose} onCreated={onCreated} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,21 +178,20 @@ function DeleteProjectDialog({
|
||||
aria-describedby="delete-project-dialog-description"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<h2 id="delete-project-dialog-title" className="mt-0 mb-3 text-xl">Delete project?</h2>
|
||||
<h2 id="delete-project-dialog-title" className="mt-0 mb-3 text-xl">{m.projects_home_delete_project()}</h2>
|
||||
<div id="delete-project-dialog-description" className="flex flex-col gap-2 text-md leading-normal text-subtext">
|
||||
<p className="m-0">
|
||||
Delete <strong className="font-semibold text-text">{project.name}</strong> from OpenResearch?
|
||||
Its experiments, runs, and chats will be permanently removed.
|
||||
{m.projects_delete_from_app({ name: autoDir(project.name) })}
|
||||
</p>
|
||||
<p className="m-0">
|
||||
The local folder{hasSyncedRepository ? " and linked GitHub repository are" : " is"} kept.
|
||||
{hasSyncedRepository ? m.projects_home_local_and_github_kept() : m.projects_home_local_folder_kept()}
|
||||
</p>
|
||||
{error && <p className="m-0 text-accent-red" role="alert">{error}</p>}
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<button className={BUTTON_CLASS_NAME} disabled={deleting} onClick={onClose}>Cancel</button>
|
||||
<button className={BUTTON_CLASS_NAME} disabled={deleting} onClick={onClose}>{m.projects_home_cancel()}</button>
|
||||
<button className={`${BUTTON_CLASS_NAME} danger`} disabled={deleting} onClick={onConfirm}>
|
||||
{deleting ? "Deleting…" : "Delete project"}
|
||||
{deleting ? m.projects_home_deleting() : m.projects_home_delete_project_action()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,24 +273,24 @@ export function ProjectsHome({
|
||||
<div className="home flex-1 min-h-0 overflow-y-auto [scrollbar-gutter:stable_both-edges] bg-canvas">
|
||||
<div className="home-inner max-w-290 my-0 mx-auto pt-12 px-6 pb-16 [@media((max-width:_960px))]:pt-6 [@media((max-width:_960px))]:px-4">
|
||||
<div className="home-head flex items-center justify-between gap-3 mb-4.5 [&_h2]:m-0 [&_h2]:text-4xl [&_h2]:tracking-[-0.02em] [@media((max-width:_520px))]:items-start [@media((max-width:_520px))]:flex-col">
|
||||
<h2>Projects</h2>
|
||||
<h2>{m.projects_home_projects()}</h2>
|
||||
<button
|
||||
className={BUTTON_CLASS_NAME}
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
<Plus size={15} /> New project
|
||||
<Plus size={15} /> {m.projects_home_new_project()}
|
||||
</button>
|
||||
</div>
|
||||
<div className="home-list overflow-hidden rounded-lg border border-border bg-background">
|
||||
<div>
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_9rem_9rem_minmax(18rem,max-content)] items-center gap-3 border-b border-border bg-background py-2.5 pl-4 pr-2 text-2xs font-medium tracking-[0.06em] text-text uppercase [@media((max-width:_960px))]:hidden">
|
||||
<span>Project</span>
|
||||
<span>Agents</span>
|
||||
<span>Experiments</span>
|
||||
<span>Repository</span>
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_9rem_9rem_minmax(18rem,max-content)] items-center gap-3 border-b border-border bg-background py-2.5 ps-4 pe-2 text-2xs font-medium tracking-[0.06em] text-text uppercase [@media((max-width:_960px))]:hidden">
|
||||
<span>{m.projects_home_project()}</span>
|
||||
<span>{m.projects_home_agents()}</span>
|
||||
<span>{m.projects_home_experiments()}</span>
|
||||
<span>{m.projects_home_repository()}</span>
|
||||
</div>
|
||||
{projects.length === 0 ? (
|
||||
<div className="py-8 px-4 text-sm text-muted">No projects yet — create one to get started.</div>
|
||||
<div className="py-8 px-4 text-sm text-muted">{m.projects_home_no_projects_yet_create_one_to_get_started()}</div>
|
||||
) : (
|
||||
[...projects].sort((a, b) => {
|
||||
const aActivity = activityByProject[a.id]?.lastMessageAt ?? a.createdAt;
|
||||
@@ -309,46 +311,46 @@ export function ProjectsHome({
|
||||
.replace(/^https?:\/\/github\.com\//, "")
|
||||
.replace(/\.git$/, "")
|
||||
.replace(/\/$/, "")
|
||||
: "Local";
|
||||
: m.projects_local();
|
||||
const agentsLabel = summary
|
||||
? summary.activeAgents > 0
|
||||
? `${summary.activeAgents} active`
|
||||
: "Idle"
|
||||
? m.projects_active_count({ count: fmtNumber(summary.activeAgents) })
|
||||
: m.projects_idle()
|
||||
: "—";
|
||||
const agentTotal = summary
|
||||
? `${summary.totalAgents} total agent${summary.totalAgents === 1 ? "" : "s"}`
|
||||
? summary.totalAgents === 1 ? m.projects_one_agent() : m.projects_agents_count({ count: fmtNumber(summary.totalAgents) })
|
||||
: "—";
|
||||
const experimentLabel = !summary
|
||||
? "—"
|
||||
: summary.runningExperiments > 0
|
||||
? `${summary.runningExperiments} running`
|
||||
? m.projects_running_count({ count: fmtNumber(summary.runningExperiments) })
|
||||
: summary.totalExperiments === 0
|
||||
? "None"
|
||||
: `${summary.totalExperiments} total`;
|
||||
? m.settings_none()
|
||||
: m.projects_total_count({ count: fmtNumber(summary.totalExperiments) });
|
||||
const experimentTotal =
|
||||
summary && summary.runningExperiments > 0
|
||||
? `${summary.totalExperiments} total`
|
||||
? m.projects_total_count({ count: fmtNumber(summary.totalExperiments) })
|
||||
: null;
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className="group project-row relative grid cursor-pointer grid-cols-[minmax(0,1fr)_9rem_9rem_minmax(18rem,max-content)] items-center gap-3 border-b border-border-variant py-4 pl-4 pr-2 text-left transition-colors duration-120 ease-standard last:border-b-0 hover:bg-surface-bright focus-within:bg-surface-bright [@media((max-width:_960px))]:grid-cols-[minmax(0,0.8fr)_minmax(0,0.8fr)_minmax(0,1.4fr)] [@media((max-width:_960px))]:items-start [@media((max-width:_960px))]:gap-x-4 [@media((max-width:_960px))]:gap-y-3 [@media((max-width:_960px))]:py-4 [@media((max-width:_960px))]:px-4 [@media((max-width:_600px))]:grid-cols-2"
|
||||
className="group project-row relative grid cursor-pointer grid-cols-[minmax(0,1fr)_9rem_9rem_minmax(18rem,max-content)] items-center gap-3 border-b border-border-variant py-4 ps-4 pe-2 text-start transition-colors duration-120 ease-standard last:border-b-0 hover:bg-surface-bright focus-within:bg-surface-bright [@media((max-width:_960px))]:grid-cols-[minmax(0,0.8fr)_minmax(0,0.8fr)_minmax(0,1.4fr)] [@media((max-width:_960px))]:items-start [@media((max-width:_960px))]:gap-x-4 [@media((max-width:_960px))]:gap-y-3 [@media((max-width:_960px))]:py-4 [@media((max-width:_960px))]:px-4 [@media((max-width:_600px))]:grid-cols-2"
|
||||
>
|
||||
<button
|
||||
className="project-row-open absolute inset-0 z-0 cursor-pointer rounded-[inherit] focus-visible:outline focus-visible:outline-2 focus-visible:outline-text focus-visible:outline-offset-[-2px]"
|
||||
aria-label={`Open ${p.name}`}
|
||||
aria-label={m.a11y_open_item({ name: autoDir(p.name) })}
|
||||
onClick={() => onOpen(p.id)}
|
||||
/>
|
||||
{/* Cells stay click-transparent so the stretched button owns row navigation. */}
|
||||
<div className="relative z-1 flex min-w-0 flex-col gap-1 pointer-events-none [@media((max-width:_960px))]:col-span-3 [@media((max-width:_600px))]:col-span-2">
|
||||
<span className="project-row-title whitespace-normal break-words text-base font-semibold text-text pointer-events-none">{p.name}</span>
|
||||
<span dir="auto" className="project-row-title whitespace-normal break-words text-base font-semibold text-text pointer-events-none">{p.name}</span>
|
||||
<span className="relative z-2 flex items-center gap-1.5 text-xs text-muted [@media((max-width:_960px))]:flex-wrap">
|
||||
<span>Created {timeAgo(p.createdAt)}</span>
|
||||
<span>{m.projects_home_created()} {timeAgo(p.createdAt)}</span>
|
||||
{p.paperId && <span aria-hidden="true">·</span>}
|
||||
{p.paperId && <span>arXiv paper ID: {p.paperId}</span>}
|
||||
{p.paperId && <span>{m.projects_home_ar_xiv_paper_id()} {ltr(p.paperId)}</span>}
|
||||
<button
|
||||
className="project-row-secondary project-row-delete inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm leading-0 text-muted opacity-0 pointer-events-none transition-opacity hover:bg-surface hover:text-accent-red group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto focus:opacity-100 focus:pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-text"
|
||||
aria-label={`Delete ${p.name}`}
|
||||
aria-label={m.a11y_delete_item({ name: autoDir(p.name) })}
|
||||
disabled={deleting === p.id}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
@@ -361,7 +363,7 @@ export function ProjectsHome({
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative z-1 flex min-w-0 flex-col gap-1 pointer-events-none">
|
||||
<span className="hidden text-2xs font-medium tracking-[0.06em] text-text uppercase [@media((max-width:_960px))]:block">Agents</span>
|
||||
<span className="hidden text-2xs font-medium tracking-[0.06em] text-text uppercase [@media((max-width:_960px))]:block">{m.projects_home_agents()}</span>
|
||||
<span className="inline-flex items-center gap-2 text-md text-text">
|
||||
{summary && summary.activeAgents > 0 && (
|
||||
<LiveDot />
|
||||
@@ -371,7 +373,7 @@ export function ProjectsHome({
|
||||
<span className="text-xs text-muted">{agentTotal}</span>
|
||||
</div>
|
||||
<div className="relative z-1 flex min-w-0 flex-col gap-1 pointer-events-none">
|
||||
<span className="hidden text-2xs font-medium tracking-[0.06em] text-text uppercase [@media((max-width:_960px))]:block">Experiments</span>
|
||||
<span className="hidden text-2xs font-medium tracking-[0.06em] text-text uppercase [@media((max-width:_960px))]:block">{m.projects_home_experiments()}</span>
|
||||
<span className="inline-flex items-center gap-2 text-md text-text">
|
||||
{summary && summary.runningExperiments > 0 && (
|
||||
<LiveDot />
|
||||
@@ -381,17 +383,17 @@ export function ProjectsHome({
|
||||
{experimentTotal && <span className="text-xs text-muted">{experimentTotal}</span>}
|
||||
</div>
|
||||
<div className="relative z-1 min-w-0 pointer-events-none [@media((max-width:_600px))]:col-span-2">
|
||||
<span className="hidden text-2xs font-medium tracking-[0.06em] text-text uppercase [@media((max-width:_960px))]:mb-1 [@media((max-width:_960px))]:block">Repository</span>
|
||||
<span className="hidden text-2xs font-medium tracking-[0.06em] text-text uppercase [@media((max-width:_960px))]:mb-1 [@media((max-width:_960px))]:block">{m.projects_home_repository()}</span>
|
||||
{githubUrl ? (
|
||||
<a
|
||||
className="project-row-secondary inline-flex max-w-full items-center gap-2 text-sm text-text no-underline pointer-events-auto hover:underline underline-offset-2"
|
||||
href={githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={`Open ${p.name} on GitHub`}
|
||||
aria-label={m.a11y_open_on_github({ name: autoDir(p.name) })}
|
||||
>
|
||||
<span className="inline-flex shrink-0"><GitHubMark size={14} /></span>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap [@media((max-width:_960px))]:whitespace-normal [@media((max-width:_960px))]:break-all">{githubState}</span>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap [@media((max-width:_960px))]:whitespace-normal [@media((max-width:_960px))]:break-all">{ltr(githubState)}</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm text-text pointer-events-none">{githubState}</span>
|
||||
|
||||
+432
-436
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import {
|
||||
Fragment,
|
||||
useEffect,
|
||||
@@ -26,6 +27,8 @@ const MIRRORED_PROPERTIES = [
|
||||
"letter-spacing",
|
||||
"word-spacing",
|
||||
"text-transform",
|
||||
"direction",
|
||||
"unicode-bidi",
|
||||
"tab-size",
|
||||
"padding-top",
|
||||
"padding-right",
|
||||
@@ -172,7 +175,7 @@ function ComposerSkillToken({
|
||||
tabIndex={0}
|
||||
aria-controls={cardId}
|
||||
aria-expanded={open}
|
||||
aria-label={`Preview /${name} skill`}
|
||||
aria-label={m.a11y_preview_skill({ name })}
|
||||
className="composer-chip group/skill pointer-events-auto relative z-1 cursor-text rounded-md bg-background text-[var(--skill-blue)]"
|
||||
onMouseEnter={show}
|
||||
onMouseLeave={scheduleClose}
|
||||
@@ -222,7 +225,7 @@ function ComposerSkillToken({
|
||||
id={cardId}
|
||||
ref={cardRef}
|
||||
role="dialog"
|
||||
aria-label={`${name} skill`}
|
||||
aria-label={m.a11y_skill({ name })}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight: "min(28rem, calc(100vh - 2rem))",
|
||||
@@ -237,12 +240,12 @@ function ComposerSkillToken({
|
||||
<div className="sticky top-0 z-1 flex items-center gap-2 border-b border-border-variant bg-background px-4 py-3">
|
||||
<span className="text-md font-medium text-muted">/{name}</span>
|
||||
<span className="inline-flex h-4 items-center rounded-full border border-border-variant bg-canvas px-1.5 text-2xs font-semibold tracking-[0.05em] text-muted">
|
||||
SKILL
|
||||
{m.skill_chips_badge()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-4 text-sm text-text">
|
||||
{loading && content === null ? (
|
||||
<span className="text-muted">Loading skill…</span>
|
||||
<span className="text-muted">{m.skill_chips_loading_skill()}</span>
|
||||
) : (
|
||||
<Md text={content ?? skill.description} />
|
||||
)}
|
||||
@@ -316,7 +319,7 @@ export function ComposerSkillChips({
|
||||
const observer = new ResizeObserver(sync);
|
||||
observer.observe(textarea);
|
||||
return () => observer.disconnect();
|
||||
}, [textareaRef]);
|
||||
}, [text, textareaRef]);
|
||||
|
||||
// The chips ride the textarea's own scrolling — caret-driven (no scroll event
|
||||
// on the frame the text changes) as well as user-driven.
|
||||
@@ -334,7 +337,7 @@ export function ComposerSkillChips({
|
||||
return (
|
||||
<div
|
||||
ref={mirrorRef}
|
||||
className="composer-chips pointer-events-none absolute inset-y-0 left-0 z-2 box-border overflow-hidden whitespace-pre-wrap break-words border-solid border-transparent text-transparent select-none"
|
||||
className="composer-chips pointer-events-none absolute inset-y-0 start-0 z-2 box-border overflow-hidden whitespace-pre-wrap break-words border-solid border-transparent text-transparent select-none"
|
||||
>
|
||||
{/* Hover padding is painted outside the mirrored text box so it cannot
|
||||
* shift the textarea's following glyphs. */}
|
||||
|
||||
@@ -14,12 +14,12 @@ export function SkillMenu({
|
||||
onHover: (index: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="skill-menu absolute bottom-[calc(100%_+_8px)] left-0 min-w-85 max-w-full p-1.5 bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] z-50 overflow-hidden">
|
||||
<div className="skill-menu absolute bottom-[calc(100%_+_8px)] start-0 min-w-85 max-w-full p-1.5 bg-background border border-border rounded-lg shadow-[0_12px_32px_rgba(0,_0,_0,_0.18)] z-50 overflow-hidden">
|
||||
{skills.map((s, i) => (
|
||||
<button
|
||||
key={s.name}
|
||||
type="button"
|
||||
className={`skill-item flex flex-col gap-0.5 w-full text-left py-[7px] px-2 rounded-sm [&.active]:bg-surface [&_.skill-name]:text-md [&_.skill-desc]:text-sm [&_.skill-desc]:text-subtext ${i === activeIndex ? "active" : ""}`}
|
||||
className={`skill-item flex flex-col gap-0.5 w-full text-start py-[7px] px-2 rounded-sm [&.active]:bg-surface [&_.skill-name]:text-md [&_.skill-desc]:text-sm [&_.skill-desc]:text-subtext ${i === activeIndex ? "active" : ""}`}
|
||||
// mousedown + preventDefault keeps the textarea focused.
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
import { Download, FileUp, Trash2, Upload } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import {
|
||||
deleteLatexTemplate,
|
||||
deleteUserSkill,
|
||||
fmtBytes,
|
||||
fmtNumber,
|
||||
importHarnessSkill,
|
||||
listHarnessSkills,
|
||||
listLatexTemplates,
|
||||
@@ -58,7 +61,7 @@ function isAcceptedName(name: string): boolean {
|
||||
}
|
||||
|
||||
const SCOPE_BUTTON_CLASS_NAME =
|
||||
"flex-1 flex flex-col gap-0.5 py-2.5 px-3 border rounded-md text-left text-sm font-medium cursor-pointer transition-[border-color,background] duration-120 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
"flex-1 flex flex-col gap-0.5 py-2.5 px-3 border rounded-md text-start text-sm font-medium cursor-pointer transition-[border-color,background] duration-120 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
|
||||
function ScopePicker({
|
||||
scope,
|
||||
@@ -85,20 +88,20 @@ function ScopePicker({
|
||||
className={cls(scope === "global")}
|
||||
onClick={() => onScope("global")}
|
||||
>
|
||||
Global
|
||||
<span className="text-2xs font-normal text-muted">Every project</span>
|
||||
{m.skills_tab_global()}
|
||||
<span className="text-2xs font-normal text-muted">{m.skills_tab_every_project()}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={scope === "project"}
|
||||
className={cls(scope === "project")}
|
||||
disabled={!project}
|
||||
title={project ? undefined : "Open a project to scope this to one project"}
|
||||
title={project ? undefined : m.skills_open_project_to_scope()}
|
||||
onClick={() => onScope("project")}
|
||||
>
|
||||
This project
|
||||
{m.skills_tab_this_project()}
|
||||
<span className="text-2xs font-normal text-muted">
|
||||
{project ? project.name : "No project open"}
|
||||
{project ? project.name : m.skills_no_project_open()}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -164,15 +167,15 @@ function DropZone({
|
||||
{busy ? (
|
||||
<>
|
||||
<span className={SPINNER_CLASS_NAME} />
|
||||
<span>Uploading…</span>
|
||||
<span>{m.skills_tab_uploading()}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload size={20} strokeWidth={1.5} />
|
||||
<span>{prompt}</span>
|
||||
<span className="inline-flex items-center gap-1.5 text-2xs text-subtext">
|
||||
<FileUp size={12} /> Adding to{" "}
|
||||
<strong className="text-text font-semibold">{destination}</strong>
|
||||
<span dir="auto" className="inline-flex items-center gap-1.5 text-2xs text-subtext">
|
||||
<FileUp size={12} /> {m.skills_tab_adding_to()}{" "}
|
||||
<strong dir="auto" className="text-text font-semibold">{destination}</strong>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@@ -198,7 +201,7 @@ function SkillRow({
|
||||
<code className={SKILL_NAME_CLASS_NAME}>/{skill.name}</code>
|
||||
<p className={SKILL_DESC_CLASS_NAME}>{skill.description}</p>
|
||||
</div>
|
||||
<div className="shrink-0 text-right whitespace-nowrap pt-0.5">
|
||||
<div className="shrink-0 text-end whitespace-nowrap pt-0.5">
|
||||
<div className="text-2xs text-subtext">{fmtBytes(skill.bytes)}</div>
|
||||
{skill.updatedAt > 0 && (
|
||||
<div className="text-2xs text-muted">{timeAgo(skill.updatedAt)}</div>
|
||||
@@ -206,12 +209,12 @@ function SkillRow({
|
||||
</div>
|
||||
<button
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
data-tip="Delete skill"
|
||||
data-tip={m.skills_tab_delete_skill()}
|
||||
data-tip-align="end"
|
||||
aria-label={`Delete skill ${skill.name}`}
|
||||
aria-label={m.skills_delete_skill_label({ name: ltr(skill.name) })}
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
if (!window.confirm(`Delete the "${skill.name}" skill?`)) return;
|
||||
if (!window.confirm(m.skills_delete_skill_confirm({ name: ltr(skill.name) }))) return;
|
||||
setBusy(true);
|
||||
deleteUserSkill({
|
||||
scope: skill.scope,
|
||||
@@ -251,7 +254,7 @@ function SkillList({
|
||||
<h3>{title}</h3>
|
||||
<p className={CARD_SUB_CLASS_NAME}>{hint}</p>
|
||||
{skills.length === 0 ? (
|
||||
<div className="text-muted text-sm">No skills yet.</div>
|
||||
<div className="text-muted text-sm">{m.skills_tab_no_skills_yet()}</div>
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
{skills.map((s) => (
|
||||
@@ -293,7 +296,7 @@ function HarnessSkillRow({
|
||||
<button
|
||||
className={SMALL_BUTTON_CLASS_NAME}
|
||||
disabled={busy}
|
||||
title={`Import into ${scopeLabel}`}
|
||||
title={m.a11y_import_into({ name: scopeLabel })}
|
||||
onClick={async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
@@ -304,7 +307,7 @@ function HarnessSkillRow({
|
||||
}}
|
||||
>
|
||||
{busy ? <span className={SPINNER_CLASS_NAME} /> : <Download size={13} />}
|
||||
{alreadyImported ? "Re-import" : "Import"}
|
||||
{alreadyImported ? m.skills_reimport() : m.skills_import()}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -347,11 +350,11 @@ function LatexTemplatesCard({ project }: { project: Project | null }) {
|
||||
setError(null);
|
||||
const lower = file.name.toLowerCase();
|
||||
if (!lower.endsWith(".tex") && !lower.endsWith(".zip")) {
|
||||
setError("Upload a .tex file or a .zip of a template folder.");
|
||||
setError(m.skills_upload_template_error());
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_UPLOAD_BYTES) {
|
||||
setError("File too large (max 20 MB).");
|
||||
setError(m.skills_file_too_large());
|
||||
return;
|
||||
}
|
||||
busyRef.current = true;
|
||||
@@ -378,25 +381,18 @@ function LatexTemplatesCard({ project }: { project: Project | null }) {
|
||||
|
||||
return (
|
||||
<section className={CARD_CLASS_NAME}>
|
||||
<h3>LaTeX templates</h3>
|
||||
<h3>{m.skills_tab_la_te_x_templates()}</h3>
|
||||
<p className={`${CARD_SUB_CLASS_NAME} [&_code]:font-mono [&_code]:text-[0.92em] [&_code]:text-text`}>
|
||||
A conference class or house style the agent writes papers into instead of its default
|
||||
preamble. Upload a <code>.tex</code>, or a <code>.zip</code> carrying its{" "}
|
||||
<code>.cls</code> and <code>.sty</code> files. With exactly one template available the
|
||||
agent uses it without asking.
|
||||
{m.skills_templates_description()}
|
||||
</p>
|
||||
|
||||
<ScopePicker scope={scope} onScope={setScope} project={project} label="Template scope" />
|
||||
<ScopePicker scope={scope} onScope={setScope} project={project} label={m.skills_template_scope()} />
|
||||
|
||||
<DropZone
|
||||
accept=".tex,.zip"
|
||||
busy={busy}
|
||||
destination={scope === "global" ? "Global" : (project?.name ?? "")}
|
||||
prompt={
|
||||
<>
|
||||
Drop a <code>.tex</code> or <code>.zip</code> here, or click to choose
|
||||
</>
|
||||
}
|
||||
destination={scope === "global" ? m.skills_global_destination() : (project?.name ?? "")}
|
||||
prompt={m.skills_drop_template()}
|
||||
onFile={(file) => void upload(file)}
|
||||
/>
|
||||
|
||||
@@ -404,14 +400,14 @@ function LatexTemplatesCard({ project }: { project: Project | null }) {
|
||||
|
||||
{templates === null ? (
|
||||
<div className="flex items-center gap-2 text-subtext text-md pt-3">
|
||||
<span className={SPINNER_CLASS_NAME} /> Loading templates…
|
||||
<span className={SPINNER_CLASS_NAME} /> {m.skills_tab_loading_templates()}
|
||||
</div>
|
||||
) : loadError ? (
|
||||
<div className="text-accent-red text-sm pt-3">
|
||||
Could not load templates: {loadError}
|
||||
{m.skills_tab_could_not_load_templates()} {loadError}
|
||||
</div>
|
||||
) : applicable.length === 0 ? (
|
||||
<div className="text-muted text-sm pt-3">No templates yet.</div>
|
||||
<div className="text-muted text-sm pt-3">{m.skills_tab_no_templates_yet()}</div>
|
||||
) : (
|
||||
<div className="flex flex-col mt-1">
|
||||
{applicable.map((t) => (
|
||||
@@ -448,15 +444,15 @@ function LatexTemplateRow({
|
||||
<div className="flex items-center gap-2">
|
||||
<code className={SKILL_NAME_CLASS_NAME}>{template.name}</code>
|
||||
<span className={BADGE_CLASS_NAME}>
|
||||
{template.scope === "global" ? "Global" : "This project"}
|
||||
{template.scope === "global" ? m.skills_tab_global() : m.skills_tab_this_project()}
|
||||
</span>
|
||||
</div>
|
||||
<p className={SKILL_DESC_CLASS_NAME}>
|
||||
{template.entry}
|
||||
{support > 0 && ` + ${support} file${support === 1 ? "" : "s"}`}
|
||||
{support > 0 && (support === 1 ? m.skills_one_support_file() : m.skills_support_files({ count: fmtNumber(support) }))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="shrink-0 text-right whitespace-nowrap pt-0.5">
|
||||
<div className="shrink-0 text-end whitespace-nowrap pt-0.5">
|
||||
<div className="text-2xs text-subtext">{fmtBytes(template.bytes)}</div>
|
||||
{template.updatedAt > 0 && (
|
||||
<div className="text-2xs text-muted">{timeAgo(template.updatedAt)}</div>
|
||||
@@ -464,12 +460,12 @@ function LatexTemplateRow({
|
||||
</div>
|
||||
<button
|
||||
className={ICON_BUTTON_CLASS_NAME}
|
||||
data-tip="Delete template"
|
||||
data-tip={m.skills_tab_delete_template()}
|
||||
data-tip-align="end"
|
||||
aria-label={`Delete template ${template.name}`}
|
||||
aria-label={m.skills_delete_template_label({ name: ltr(template.name) })}
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
if (!window.confirm(`Delete the "${template.name}" template?`)) return;
|
||||
if (!window.confirm(m.skills_delete_template_confirm({ name: ltr(template.name) }))) return;
|
||||
setBusy(true);
|
||||
deleteLatexTemplate({
|
||||
scope: template.scope,
|
||||
@@ -529,11 +525,11 @@ export function SkillsTab({ project }: { project: Project | null }) {
|
||||
if (busyRef.current) return; // ignore a second drop/pick mid-upload
|
||||
setError(null);
|
||||
if (!isAcceptedName(file.name)) {
|
||||
setError("Upload a SKILL.md file or a .zip of a skill folder.");
|
||||
setError(m.skills_upload_skill_error());
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_UPLOAD_BYTES) {
|
||||
setError("File too large (max 20 MB).");
|
||||
setError(m.skills_file_too_large());
|
||||
return;
|
||||
}
|
||||
busyRef.current = true;
|
||||
@@ -577,7 +573,7 @@ export function SkillsTab({ project }: { project: Project | null }) {
|
||||
|
||||
const globalSkills = (skills ?? []).filter((s) => s.scope === "global");
|
||||
const projectSkills = (skills ?? []).filter((s) => s.scope === "project");
|
||||
const scopeLabel = scope === "global" ? "Global" : (project?.name ?? "this project");
|
||||
const scopeLabel = scope === "global" ? m.skills_tab_global() : (project?.name ?? m.skills_tab_this_project());
|
||||
// Names present in the scope an import would target — for the "Re-import" hint.
|
||||
const existingInScope = new Set(
|
||||
(skills ?? []).filter((s) => s.scope === scope).map((s) => s.name),
|
||||
@@ -585,29 +581,23 @@ export function SkillsTab({ project }: { project: Project | null }) {
|
||||
|
||||
return (
|
||||
<div className="settings-view max-w-readable my-0 mx-auto pt-6 px-8 pb-15 [&_h1]:mt-0 [&_h1]:mx-0 [&_h1]:mb-1.5 [&_h1]:text-3xl">
|
||||
<h1>Customize</h1>
|
||||
<h1>{m.skills_tab_customize()}</h1>
|
||||
<p className="mt-0 mx-0 mb-5 text-muted text-md leading-normal [&_code]:font-mono [&_code]:text-[0.92em] [&_code]:text-text">
|
||||
What the agent brings to every session: LaTeX templates it writes papers into, and{" "}
|
||||
<code>SKILL.md</code> skills it discovers automatically and you invoke with{" "}
|
||||
<code>/name</code> in chat. Both apply everywhere, or to just this project.
|
||||
{m.skills_overview_description()}
|
||||
</p>
|
||||
|
||||
<LatexTemplatesCard project={project} />
|
||||
|
||||
<section className={CARD_CLASS_NAME}>
|
||||
<h3>Add a skill</h3>
|
||||
<h3>{m.skills_tab_add_a_skill()}</h3>
|
||||
|
||||
<ScopePicker scope={scope} onScope={setScope} project={project} label="Skill scope" />
|
||||
<ScopePicker scope={scope} onScope={setScope} project={project} label={m.skills_skill_scope()} />
|
||||
|
||||
<DropZone
|
||||
accept=".md,.markdown,.zip"
|
||||
busy={busy}
|
||||
destination={scope === "global" ? "Global" : (project?.name ?? "")}
|
||||
prompt={
|
||||
<>
|
||||
Drop a <code>SKILL.md</code> or <code>.zip</code> here, or click to choose
|
||||
</>
|
||||
}
|
||||
destination={scope === "global" ? m.skills_global_destination() : (project?.name ?? "")}
|
||||
prompt={m.skills_drop_skill()}
|
||||
onFile={(file) => void upload(file)}
|
||||
/>
|
||||
|
||||
@@ -616,10 +606,10 @@ export function SkillsTab({ project }: { project: Project | null }) {
|
||||
|
||||
{harnessSkills.length > 0 && (
|
||||
<section className={CARD_CLASS_NAME}>
|
||||
<h3>Import from your agent</h3>
|
||||
<p className={`${CARD_SUB_CLASS_NAME} [&_code]:font-mono [&_code]:text-[0.92em] [&_code]:text-text [&_strong]:text-text [&_strong]:font-semibold`}>
|
||||
Skills already installed in your coding agents. Import a copy into{" "}
|
||||
<strong>{scopeLabel}</strong> so it's managed here and invocable with <code>/name</code>.
|
||||
<h3>{m.skills_tab_import_from_your_agent()}</h3>
|
||||
<p dir="auto" className={`${CARD_SUB_CLASS_NAME} [&_code]:font-mono [&_code]:text-[0.92em] [&_code]:text-text [&_strong]:text-text [&_strong]:font-semibold`}>
|
||||
{m.skills_tab_skills_already_installed_in_your_coding_agents_import()}{" "}
|
||||
<strong>{scopeLabel}</strong> {m.skills_tab_so_it_s_managed_here_and_invocable_with()} <code>/name</code>.
|
||||
</p>
|
||||
<div className="flex flex-col">
|
||||
{harnessSkills.map((s) => (
|
||||
@@ -637,21 +627,21 @@ export function SkillsTab({ project }: { project: Project | null }) {
|
||||
|
||||
{skills === null ? (
|
||||
<div className="flex items-center gap-2 text-subtext text-md p-3">
|
||||
<span className={SPINNER_CLASS_NAME} /> Loading skills…
|
||||
<span className={SPINNER_CLASS_NAME} /> {m.skills_tab_loading_skills()}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<SkillList
|
||||
title="Global skills"
|
||||
hint="Available to the agent in every project."
|
||||
title={m.skills_tab_global_skills()}
|
||||
hint={m.skills_global_hint()}
|
||||
skills={globalSkills}
|
||||
onChanged={refresh}
|
||||
onError={setError}
|
||||
/>
|
||||
{project && (
|
||||
<SkillList
|
||||
title={`${project.name} skills`}
|
||||
hint="Available only in this project's sessions. Shadows a global skill of the same name."
|
||||
title={m.a11y_named_skills({ name: project.name })}
|
||||
hint={m.skills_project_hint()}
|
||||
skills={projectSkills}
|
||||
projectId={project.id}
|
||||
onChanged={refresh}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// coloring across the table, graph and drawer.
|
||||
|
||||
import { STATUS_BADGE_CLASS_NAME } from "../styleClasses";
|
||||
import { m } from "../paraglide/messages.js";
|
||||
|
||||
export interface StatusStyle {
|
||||
className: string;
|
||||
@@ -24,7 +25,20 @@ export function statusStyle(status: string): StatusStyle {
|
||||
return STATUS_STYLES[status] ?? STATUS_STYLES.idle;
|
||||
}
|
||||
|
||||
function sentenceCase(s: string): string {
|
||||
const STATUS_LABELS: Record<string, () => string> = {
|
||||
done: m.status_done,
|
||||
failed: m.status_failed,
|
||||
running: m.status_running,
|
||||
starting: m.status_starting,
|
||||
cancelling: m.status_cancelling,
|
||||
cancelled: m.status_cancelled,
|
||||
editing: m.status_editing,
|
||||
idle: m.status_idle,
|
||||
};
|
||||
|
||||
export function statusLabel(s: string): string {
|
||||
const localized = STATUS_LABELS[s];
|
||||
if (localized) return localized();
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
@@ -33,7 +47,7 @@ export function StatusBadge({ status, label }: { status: string; label?: string
|
||||
return (
|
||||
<span className={`${STATUS_BADGE_CLASS_NAME} ${style.className}${style.live ? " live" : ""}`}>
|
||||
<span className="dot" />
|
||||
{label ?? sentenceCase(status)}
|
||||
{label ?? statusLabel(status)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { getChatMessages, type ChatMessage, type ChatPart } from "../api";
|
||||
import { onChatEvent } from "../events";
|
||||
@@ -133,7 +134,7 @@ export function SubagentTab({
|
||||
return (
|
||||
<div className={TAB_BODY_CLASS_NAME}>
|
||||
<div className={PANE_CONTENT_CLASS_NAME}>
|
||||
<div className="subagent-empty py-[3px] px-1 text-md text-muted">Loading…</div>
|
||||
<div className="subagent-empty py-[3px] px-1 text-md text-muted">{m.subagent_tab_loading()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -168,7 +169,7 @@ export function SubagentTab({
|
||||
onOpenSubagent={onOpenSubagent}
|
||||
/>
|
||||
) : (
|
||||
<div className="subagent-empty py-[3px] px-1 text-md text-muted">This sub-agent is no longer available.</div>
|
||||
<div className="subagent-empty py-[3px] px-1 text-md text-muted">{m.subagent_tab_this_sub_agent_is_no_longer_available()}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+16
-16
@@ -1,3 +1,4 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { X } from "lucide-react";
|
||||
@@ -25,7 +26,7 @@ export function DemoWelcomeModal({
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
void action()
|
||||
.catch(() => setError("Couldn't save your progress. Try again."))
|
||||
.catch(() => setError(m.tour_save_error()))
|
||||
.finally(() => setSaving(false));
|
||||
},
|
||||
[saving],
|
||||
@@ -89,45 +90,44 @@ export function DemoWelcomeModal({
|
||||
tabIndex={-1}
|
||||
>
|
||||
<button
|
||||
className={`${ICON_BUTTON_CLASS_NAME} !absolute top-3.5 right-3.5`}
|
||||
aria-label="Close"
|
||||
className={`${ICON_BUTTON_CLASS_NAME} !absolute top-3.5 end-3.5`}
|
||||
aria-label={m.tour_close()}
|
||||
onClick={() => run(onClose)}
|
||||
disabled={saving}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
<div className="mb-5 flex items-center gap-3 pr-8">
|
||||
<div className="mb-5 flex items-center gap-3 pe-8">
|
||||
<span className="block h-9 w-9 shrink-0 [&_svg]:block [&_svg]:h-full [&_svg]:w-full">
|
||||
<BrandMark />
|
||||
</span>
|
||||
<div>
|
||||
<div className="mb-0.5 text-xs font-semibold tracking-[0.08em] text-primary uppercase">
|
||||
Demo project
|
||||
{m.tour_demo_project()}
|
||||
</div>
|
||||
<h2
|
||||
id="demo-welcome-title"
|
||||
className="m-0 text-3xl leading-tight tracking-[-0.02em]"
|
||||
>
|
||||
Welcome to OpenResearch
|
||||
{m.tour_welcome_to_open_research()}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base leading-relaxed text-text [&_p]:m-0 [&_p_+_p]:mt-3">
|
||||
<p>
|
||||
This is a demo project showing how OpenResearch works. This demo uses Andrej
|
||||
Karpathy's{" "}
|
||||
<p dir="auto">
|
||||
{m.tour_this_is_a_demo_project_showing_how_open()}{" "}
|
||||
<a
|
||||
dir="ltr"
|
||||
href="https://github.com/karpathy/nanochat"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-semibold text-primary underline decoration-border-strong underline-offset-3 hover:decoration-primary"
|
||||
>
|
||||
nanochat
|
||||
</a>, a repo for training a mini-GPT from scratch.
|
||||
{m.tour_nanochat()}
|
||||
</a>{m.tour_a_repo_for_training_a_mini_gpt_from()}
|
||||
</p>
|
||||
<p>
|
||||
Look through the agent conversations, experiments, runs, and artifacts to see how a
|
||||
project on OpenResearch comes together.
|
||||
<p dir="auto">
|
||||
{m.tour_look_through_the_agent_conversations_experiments_runs_and()}
|
||||
</p>
|
||||
</div>
|
||||
{error && <p className="mt-3 mb-0 text-sm text-accent-red">{error}</p>}
|
||||
@@ -137,14 +137,14 @@ export function DemoWelcomeModal({
|
||||
onClick={() => run(onCreateProject)}
|
||||
disabled={saving}
|
||||
>
|
||||
Create a new project
|
||||
{m.tour_create_a_new_project()}
|
||||
</button>
|
||||
<button
|
||||
className={PRIMARY_BUTTON_CLASS_NAME}
|
||||
onClick={() => run(onClose)}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? "Saving…" : "Explore the demo"}
|
||||
{saving ? m.common_saving() : m.tour_explore_demo()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
@@ -13,6 +15,7 @@ import { GitHubMark } from "./BackendLogos";
|
||||
import { memo, useMemo, useRef } from "react";
|
||||
import {
|
||||
githubBranchUrl,
|
||||
fmtNumber,
|
||||
runDisplayStatus,
|
||||
timeAgo,
|
||||
type Experiment,
|
||||
@@ -22,7 +25,7 @@ import {
|
||||
import type { ExperimentView } from "./DetailDrawer";
|
||||
import type { CodeView } from "./CodeTab";
|
||||
import { ExpHoverCard, dismissTreeHoverCards, useHoverIntent } from "./ExpHoverCard";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
import { statusLabel, StatusBadge } from "./StatusBadge";
|
||||
import { tabOpenGestureHandlers, type TabOpenIntent } from "../tabPreview";
|
||||
|
||||
const EMPTY_STATE_CLASS_NAME = [
|
||||
@@ -181,7 +184,7 @@ const ExpNode = memo(function ExpNode({ data }: NodeProps<ExpFlowNode>) {
|
||||
const { exp, latestRun, runs, isBaseline, parentSlug, githubOwner, githubRepo, onOpenView, onOpenCode } = data;
|
||||
const status = latestRun ? runDisplayStatus(latestRun) : undefined;
|
||||
const live = status === "running" || status === "starting" || status === "cancelling";
|
||||
const kind = isBaseline ? "Baseline" : live ? "Running" : "Experiment";
|
||||
const kind = isBaseline ? m.tree_baseline() : live ? m.tree_running() : m.tree_experiment();
|
||||
const squares = runs.slice(-MAX_SQUARES);
|
||||
|
||||
// `data` is rebuilt on every experiments/runs change (a superset of the
|
||||
@@ -194,7 +197,7 @@ const ExpNode = memo(function ExpNode({ data }: NodeProps<ExpFlowNode>) {
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={`exp-node w-66 border border-border rounded-md bg-background py-2.5 px-3 shadow-[0_1px_2px_rgba(0,_0,_0,_0.04)] text-md transition-[box-shadow] duration-120 ease-standard [&:hover]:shadow-[0_2px_8px_rgba(0,_0,_0,_0.08)] [&.live]:border-accent-teal [&.live]:shadow-[0_2px_12px_rgba(32,_154,_132,_0.2)] [&_.node-overview-link]:block [&_.node-overview-link]:w-full [&_.node-overview-link]:p-0 [&_.node-overview-link]:border-0 [&_.node-overview-link]:bg-transparent [&_.node-overview-link]:text-inherit [&_.node-overview-link]:[font:inherit] [&_.node-overview-link]:text-left [&_.node-overview-link]:cursor-pointer [&_.node-overview-link:hover_.node-slug]:underline [&_.node-overview-link:hover_.node-slug]:underline-offset-[3px] [&_.node-overview-link:focus-visible]:outline-2 [&_.node-overview-link:focus-visible]:outline-solid [&_.node-overview-link:focus-visible]:outline-accent [&_.node-overview-link:focus-visible]:outline-offset-4 [&_.node-overview-link:focus-visible]:rounded-xs [&_.node-eyebrow]:flex [&_.node-eyebrow]:items-center [&_.node-eyebrow]:justify-between [&_.node-eyebrow]:gap-2 [&_.node-eyebrow]:mb-1.5 [&_.node-eyebrow]:text-2xs [&_.node-eyebrow]:font-medium [&_.node-eyebrow]:text-muted [&_.node-head]:flex [&_.node-head]:items-center [&_.node-head]:gap-[7px] [&_.node-head]:min-w-0 [&_.node-status]:w-2 [&_.node-status]:h-2 [&_.node-status]:rounded-full [&_.node-status]:shrink-0 [&_.node-slug]:font-mono [&_.node-slug]:text-sm [&_.node-slug]:font-semibold [&_.node-slug]:text-text [&_.node-slug]:flex-1 [&_.node-slug]:min-w-0 [&_.node-slug]:overflow-hidden [&_.node-slug]:text-ellipsis [&_.node-slug]:whitespace-nowrap [&_.node-title]:mt-1 [&_.node-title]:text-text [&_.node-title]:text-sm [&_.node-title]:line-clamp-2 [&_.node-meta]:mt-2 [&_.node-meta]:flex [&_.node-meta]:items-center [&_.node-meta]:gap-2 [&_.node-meta]:text-2xs [&_.node-meta]:text-muted [&_.node-actions]:mt-2 [&_.node-actions]:pt-1.5 [&_.node-actions]:border-t [&_.node-actions]:border-t-border-variant [&_.node-actions]:flex [&_.node-actions]:items-center [&_.node-actions]:gap-[3px] [&_.node-action]:inline-flex [&_.node-action]:items-center [&_.node-action]:gap-[5px] [&_.node-action]:py-[3px] [&_.node-action]:px-1.5 [&_.node-action]:text-xs [&_.node-action]:font-medium [&_.node-action]:text-text [&_.node-action]:rounded-sm [&_.node-action]:no-underline [&_.node-action:hover]:text-text [&_.node-action:hover]:bg-surface [&_.node-action-ext]:ml-auto [&_.node-action-ext]:py-[3px] [&_.node-action-ext]:px-[5px] ${live ? "live" : ""}`}
|
||||
className={`exp-node w-66 border border-border rounded-md bg-background py-2.5 px-3 shadow-[0_1px_2px_rgba(0,_0,_0,_0.04)] text-md transition-[box-shadow] duration-120 ease-standard [&:hover]:shadow-[0_2px_8px_rgba(0,_0,_0,_0.08)] [&.live]:border-accent-teal [&.live]:shadow-[0_2px_12px_rgba(32,_154,_132,_0.2)] [&_.node-overview-link]:block [&_.node-overview-link]:w-full [&_.node-overview-link]:p-0 [&_.node-overview-link]:border-0 [&_.node-overview-link]:bg-transparent [&_.node-overview-link]:text-inherit [&_.node-overview-link]:[font:inherit] [&_.node-overview-link]:text-start [&_.node-overview-link]:cursor-pointer [&_.node-overview-link:hover_.node-slug]:underline [&_.node-overview-link:hover_.node-slug]:underline-offset-[3px] [&_.node-overview-link:focus-visible]:outline-2 [&_.node-overview-link:focus-visible]:outline-solid [&_.node-overview-link:focus-visible]:outline-accent [&_.node-overview-link:focus-visible]:outline-offset-4 [&_.node-overview-link:focus-visible]:rounded-xs [&_.node-eyebrow]:flex [&_.node-eyebrow]:items-center [&_.node-eyebrow]:justify-between [&_.node-eyebrow]:gap-2 [&_.node-eyebrow]:mb-1.5 [&_.node-eyebrow]:text-2xs [&_.node-eyebrow]:font-medium [&_.node-eyebrow]:text-muted [&_.node-head]:flex [&_.node-head]:items-center [&_.node-head]:gap-[7px] [&_.node-head]:min-w-0 [&_.node-status]:w-2 [&_.node-status]:h-2 [&_.node-status]:rounded-full [&_.node-status]:shrink-0 [&_.node-slug]:font-mono [&_.node-slug]:text-sm [&_.node-slug]:font-semibold [&_.node-slug]:text-text [&_.node-slug]:flex-1 [&_.node-slug]:min-w-0 [&_.node-slug]:overflow-hidden [&_.node-slug]:text-ellipsis [&_.node-slug]:whitespace-nowrap [&_.node-title]:mt-1 [&_.node-title]:text-text [&_.node-title]:text-sm [&_.node-title]:line-clamp-2 [&_.node-meta]:mt-2 [&_.node-meta]:flex [&_.node-meta]:items-center [&_.node-meta]:gap-2 [&_.node-meta]:text-2xs [&_.node-meta]:text-muted [&_.node-actions]:mt-2 [&_.node-actions]:pt-1.5 [&_.node-actions]:border-t [&_.node-actions]:border-t-border-variant [&_.node-actions]:flex [&_.node-actions]:items-center [&_.node-actions]:gap-[3px] [&_.node-action]:inline-flex [&_.node-action]:items-center [&_.node-action]:gap-[5px] [&_.node-action]:py-[3px] [&_.node-action]:px-1.5 [&_.node-action]:text-xs [&_.node-action]:font-medium [&_.node-action]:text-text [&_.node-action]:rounded-sm [&_.node-action]:no-underline [&_.node-action:hover]:text-text [&_.node-action:hover]:bg-surface [&_.node-action-ext]:ms-auto [&_.node-action-ext]:py-[3px] [&_.node-action-ext]:px-[5px] ${live ? "live" : ""}`}
|
||||
onMouseEnter={hover.onMouseEnter}
|
||||
onMouseLeave={hover.onMouseLeave}
|
||||
>
|
||||
@@ -218,19 +221,19 @@ const ExpNode = memo(function ExpNode({ data }: NodeProps<ExpFlowNode>) {
|
||||
<div className="node-title">{exp.title || exp.description}</div>
|
||||
)}
|
||||
<div className="node-meta">
|
||||
<span>Runs</span>
|
||||
<span>{m.tree_view_runs()}</span>
|
||||
{squares.length > 0 ? (
|
||||
<span className="run-squares flex items-center gap-[3px]">
|
||||
{squares.map((run) => (
|
||||
<span
|
||||
key={run.id}
|
||||
className={`run-sq w-[9px] h-[9px] shrink-0 [&.pass]:bg-accent-green [&.fail]:border-[1.5px] [&.fail]:border-[color-mix(in_oklab,_var(--accent-red)_55%,_transparent)] [&.live]:bg-accent-teal [&.live]:animate-[or-pulse_1.2s_ease-in-out_infinite] [&.other]:border-[1.5px] [&.other]:border-border ${runSquareClass(runDisplayStatus(run))}`}
|
||||
title={runDisplayStatus(run)}
|
||||
title={statusLabel(runDisplayStatus(run))}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
) : (
|
||||
<span>no runs</span>
|
||||
<span>{m.tree_view_no_runs()}</span>
|
||||
)}
|
||||
<span style={{ flex: 1 }} />
|
||||
{latestRun && <span>{timeAgo(latestRun.createdAt)}</span>}
|
||||
@@ -241,30 +244,30 @@ const ExpNode = memo(function ExpNode({ data }: NodeProps<ExpFlowNode>) {
|
||||
{runs.length > 0 && (
|
||||
<button
|
||||
className="node-action"
|
||||
title="Open logs"
|
||||
title={m.tree_view_open_logs()}
|
||||
{...tabOpenGestureHandlers<HTMLButtonElement>((intent) =>
|
||||
onOpenView(exp.id, "terminal", intent),
|
||||
)}
|
||||
>
|
||||
<Terminal size={13} />
|
||||
Logs
|
||||
{m.tree_view_logs()}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="node-action"
|
||||
title={`Browse code on ${exp.branchName}`}
|
||||
title={m.a11y_browse_code_on({ branch: ltr(exp.branchName) })}
|
||||
{...tabOpenGestureHandlers<HTMLButtonElement>((intent) =>
|
||||
onOpenCode(exp.id, exp.branchName, "files", intent),
|
||||
)}
|
||||
>
|
||||
<FolderTree size={13} />
|
||||
Code
|
||||
{m.tree_view_code()}
|
||||
</button>
|
||||
{/* Icon-only: labeled actions + the link overflow the card's fixed width. */}
|
||||
{githubOwner && githubRepo && <a
|
||||
className="node-action node-action-ext"
|
||||
title={`Open ${exp.branchName} on GitHub`}
|
||||
aria-label={`Open ${exp.branchName} on GitHub`}
|
||||
title={m.a11y_open_on_github({ name: ltr(exp.branchName) })}
|
||||
aria-label={m.a11y_open_on_github({ name: ltr(exp.branchName) })}
|
||||
href={githubBranchUrl(githubOwner, githubRepo, exp.branchName)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -303,10 +306,10 @@ const ElidedNode = memo(function ElidedNode({ data }: NodeProps<ElidedFlowNode>)
|
||||
// body is a single action.
|
||||
return (
|
||||
<div
|
||||
className="elided-node w-37 h-11 flex items-center gap-2 py-1.5 px-2.5 border border-dashed border-border rounded-md bg-[color-mix(in_oklab,_var(--text)_3%,_transparent)] text-muted text-2xs font-medium text-left transition-[border-color,color] duration-120 ease-standard [&:hover]:border-text [&:hover]:text-text [&_.elided-node-label]:flex [&_.elided-node-label]:flex-col [&_.elided-node-label]:leading-[1.3] [&_.elided-node-sub]:text-muted"
|
||||
className="elided-node w-37 h-11 flex items-center gap-2 py-1.5 px-2.5 border border-dashed border-border rounded-md bg-[color-mix(in_oklab,_var(--text)_3%,_transparent)] text-muted text-2xs font-medium text-start transition-[border-color,color] duration-120 ease-standard [&:hover]:border-text [&:hover]:text-text [&_.elided-node-label]:flex [&_.elided-node-label]:flex-col [&_.elided-node-label]:leading-[1.3] [&_.elided-node-sub]:text-muted"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Switch to Entire project to see all experiments"
|
||||
title={m.tree_view_switch_to_entire_project_to_see_all_experiments()}
|
||||
onClick={onShowProjectScope}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
@@ -318,8 +321,8 @@ const ElidedNode = memo(function ElidedNode({ data }: NodeProps<ElidedFlowNode>)
|
||||
<Handle type="target" position={Position.Top} />
|
||||
<Ellipsis size={14} />
|
||||
<span className="elided-node-label">
|
||||
{count} {count === 1 ? "experiment" : "experiments"}
|
||||
<span className="elided-node-sub">other tasks</span>
|
||||
{count === 1 ? m.tree_one_experiment() : m.tree_experiment_count({ count: fmtNumber(count) })}
|
||||
<span className="elided-node-sub">{m.tree_view_other_tasks()}</span>
|
||||
</span>
|
||||
<Handle type="source" position={Position.Bottom} />
|
||||
</div>
|
||||
@@ -454,8 +457,8 @@ export function TreeView({
|
||||
if (experiments.length === 0) {
|
||||
return (
|
||||
<div className={EMPTY_STATE_CLASS_NAME}>
|
||||
<p className="empty-state-title">No experiments yet</p>
|
||||
<p className="empty-state-hint">Ask the agent in chat to create and run your first experiment.</p>
|
||||
<p className="empty-state-title">{m.tree_view_no_experiments_yet()}</p>
|
||||
<p className="empty-state-hint">{m.tree_view_ask_the_agent_in_chat_to_create_and()}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -464,9 +467,9 @@ export function TreeView({
|
||||
if (nodes.length === 0 && agentSessionId) {
|
||||
return (
|
||||
<div className={EMPTY_STATE_CLASS_NAME}>
|
||||
<p className="empty-state-title">No experiments from the current task yet</p>
|
||||
<p className="empty-state-title">{m.tree_view_no_experiments_from_the_current_task_yet()}</p>
|
||||
<p className="empty-state-hint">
|
||||
Ask in this task to create one, or switch to Entire project to see all experiments.
|
||||
{m.tree_view_ask_in_this_task_to_create_one_or()}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
import { RefreshCw, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getUpdateStatus, type UpdateStatus } from "../api";
|
||||
@@ -54,12 +56,12 @@ export function UpdateBanner() {
|
||||
>
|
||||
<RefreshCw size={13} className="shrink-0 text-subtext" />
|
||||
<span className="min-w-0">
|
||||
Updated to <strong className="font-semibold">{version}</strong>. Restart to use it.
|
||||
{m.update_banner_complete({ version: ltr(version) })}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-auto shrink-0 p-1 rounded-sm text-subtext hover:text-text hover:bg-highlight"
|
||||
aria-label="Dismiss"
|
||||
className="ms-auto shrink-0 p-1 rounded-sm text-subtext hover:text-text hover:bg-highlight"
|
||||
aria-label={m.update_banner_dismiss()}
|
||||
onClick={() => setDismissed(version)}
|
||||
>
|
||||
<X size={13} />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
// The pinned Files home for the active chat session's private worktree — what
|
||||
// the agent is changing right now, before any run/commit exists. The Code tab
|
||||
// remains committed-state only.
|
||||
@@ -48,7 +50,7 @@ export function WorktreeTab({
|
||||
sessionId?: string;
|
||||
project: Project;
|
||||
/** Which segmented view is showing (lives on the tab def, so it survives the
|
||||
* unmount/remount when another right-pane tab fronts this one). */
|
||||
* unmount/remount when another end-pane tab fronts this one). */
|
||||
view: WorktreeView;
|
||||
/** Files-view dirs flipped away from their depth default (on the tab def). */
|
||||
toggled: ReadonlySet<string>;
|
||||
@@ -180,11 +182,11 @@ export function WorktreeTab({
|
||||
const liveWorktree = sessionId && wt?.exists ? wt : null;
|
||||
const checkedOut =
|
||||
liveWorktree?.branch ??
|
||||
(liveWorktree?.baselineBranch ? `detached @ ${liveWorktree.baselineBranch}` : "detached");
|
||||
(liveWorktree?.baselineBranch ? m.worktree_detached_at({ branch: ltr(liveWorktree.baselineBranch) }) : m.settings_detached());
|
||||
const fileCount = liveWorktree?.files?.length ?? 0;
|
||||
const branchChip = liveWorktree
|
||||
? `Current worktree · ${checkedOut}${fileCount > 0 ? "*" : ""}`
|
||||
: `Default branch · ${project.baselineBranch}`;
|
||||
? m.worktree_current({ branch: ltr(`${checkedOut}${fileCount > 0 ? "*" : ""}`) })
|
||||
: m.worktree_default_branch({ branch: ltr(project.baselineBranch) });
|
||||
const githubBranch = liveWorktree ? liveWorktree.branch : project.baselineBranch;
|
||||
|
||||
return (
|
||||
@@ -200,19 +202,19 @@ export function WorktreeTab({
|
||||
? githubBranchUrl(project.githubOwner, project.githubRepo, githubBranch)
|
||||
: undefined
|
||||
}
|
||||
githubTitle={githubBranch ? `Open ${githubBranch} on GitHub` : undefined}
|
||||
githubTitle={githubBranch ? m.a11y_open_branch_github({ branch: ltr(githubBranch) }) : undefined}
|
||||
refreshing={loading}
|
||||
onRefresh={load}
|
||||
/>
|
||||
{error && (wt || tree) && <div className={CODE_TAB_NOTE_CLASS_NAME}>Refresh failed: {error}</div>}
|
||||
{error && (wt || tree) && <div className={CODE_TAB_NOTE_CLASS_NAME}>{m.worktree_tab_refresh_failed()} {ltr(error)}</div>}
|
||||
{!tree || (sessionId && !wt) ? (
|
||||
<div className={CODE_TAB_BODY_CLASS_NAME}>
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>{error ? `Failed to load: ${error}` : "Loading…"}</div>
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>{error ? m.common_failed_to_load({ error: ltr(error) }) : m.common_loading()}</div>
|
||||
</div>
|
||||
) : liveWorktree && view === "changes" ? (
|
||||
<div className={`${CODE_TAB_BODY_CLASS_NAME} wt-changes pt-0 px-4 pb-6 [&_>_:first-child]:mt-3.5`}>
|
||||
{fileCount === 0 || !liveWorktree.diff ? (
|
||||
<div className="changes-note text-sm text-muted">No changes yet.</div>
|
||||
<div className="changes-note text-sm text-muted">{m.worktree_tab_no_changes_yet()}</div>
|
||||
) : (
|
||||
<>
|
||||
{liveWorktree.diff.truncated && (
|
||||
@@ -231,12 +233,12 @@ export function WorktreeTab({
|
||||
) : (
|
||||
<div className={CODE_TAB_BODY_CLASS_NAME}>
|
||||
{tree.truncated && (
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>Listing truncated.</div>
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>{m.worktree_tab_listing_truncated()}</div>
|
||||
)}
|
||||
{!filesTree ? (
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>Loading…</div>
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>{m.worktree_tab_loading()}</div>
|
||||
) : filesTree.dirs.size === 0 && filesTree.files.length === 0 ? (
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>No files.</div>
|
||||
<div className={CODE_TAB_NOTE_CLASS_NAME}>{m.worktree_tab_no_files()}</div>
|
||||
) : (
|
||||
<div className="file-tree py-1.5 px-0 text-md">
|
||||
<TreeLevel
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { MONO_CLASS_NAME } from "../styleClasses";
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
|
||||
/** A backtick command from an `agentNote`, rendered as a code pill with its own
|
||||
* copy button so the user can grab it without retyping. */
|
||||
@@ -21,8 +23,8 @@ function CommandPill({ cmd }: { cmd: string }) {
|
||||
})
|
||||
.catch(() => {});
|
||||
}}
|
||||
aria-label={copied ? "Copied" : `Copy ${cmd}`}
|
||||
title={copied ? "Copied" : "Copy"}
|
||||
aria-label={copied ? m.common_copied() : m.a11y_copy_value({ value: ltr(cmd) })}
|
||||
title={copied ? m.common_copied() : m.md_copy()}
|
||||
>
|
||||
{copied ? <Check size={11} strokeWidth={3} /> : <Copy size={11} />}
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// The nested file-tree primitives shared by the code browsers: the committed
|
||||
// CodeTab and the live WorktreeTab's Files view. A flat, sorted, repo-relative
|
||||
// path list (from a git listing) becomes a nested dir tree that renders as
|
||||
import { m } from "../paraglide/messages.js";
|
||||
import { ltr } from "../i18n";
|
||||
// collapsible rows; clicking a file bubbles its repo-relative path up. Kept
|
||||
// source-agnostic — the caller decides which checkout the paths came from and
|
||||
// how a file open resolves.
|
||||
@@ -11,7 +13,7 @@ import { tabOpenGestureHandlers, type TabOpenIntent } from "../tabPreview";
|
||||
|
||||
const FILE_TREE_ROW_CLASS_NAME = [
|
||||
"file-tree-row flex items-center gap-1.5 w-full py-[3px] px-2.5 border-0",
|
||||
"bg-transparent text-text text-left cursor-pointer font-[inherit]",
|
||||
"bg-transparent text-text text-start cursor-pointer font-[inherit]",
|
||||
"text-[length:inherit] [&:hover]:bg-panel [&_>_svg]:shrink-0",
|
||||
"[&_>_svg]:text-subtext [&_>_svg.file-tree-chevron]:text-muted",
|
||||
].join(" ");
|
||||
@@ -85,7 +87,7 @@ function DirRow({
|
||||
<button
|
||||
type="button"
|
||||
className={FILE_TREE_ROW_CLASS_NAME}
|
||||
style={{ paddingLeft: 8 + depth * 14 }}
|
||||
style={{ paddingInlineStart: 8 + depth * 14 }}
|
||||
onClick={() => onToggle(path)}
|
||||
title={path}
|
||||
>
|
||||
@@ -151,11 +153,11 @@ export function TreeLevel({
|
||||
key={`f:${path}`}
|
||||
type="button"
|
||||
className={FILE_TREE_ROW_CLASS_NAME}
|
||||
style={{ paddingLeft: 8 + depth * 14 }}
|
||||
style={{ paddingInlineStart: 8 + depth * 14 }}
|
||||
{...tabOpenGestureHandlers<HTMLButtonElement>((intent) =>
|
||||
onOpenFile(path, intent),
|
||||
)}
|
||||
title={`${path} — double-click or Enter to keep open`}
|
||||
title={m.a11y_keep_open({ name: ltr(path) })}
|
||||
>
|
||||
<FileTypeIcon name={name} />
|
||||
<span className="file-tree-name flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">{name}</span>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { getLocale } from "./paraglide/runtime.js";
|
||||
|
||||
// LRI/FSI through PDI keep interpolated text from reordering its surrounding sentence.
|
||||
export const ltr = (value: string | number) => `\u2066${value}\u2069`;
|
||||
export const autoDir = (value: string | number) => `\u2068${value}\u2069`;
|
||||
|
||||
export const fmtNumber = (value: number) => new Intl.NumberFormat(getLocale()).format(value);
|
||||
@@ -1,8 +1,13 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { getLocale } from "./paraglide/runtime.js";
|
||||
import "./tailwind.css";
|
||||
|
||||
const locale = getLocale();
|
||||
document.documentElement.lang = locale;
|
||||
document.documentElement.dir = "ltr";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { SkillInfo } from "./api";
|
||||
import { m } from "./paraglide/messages.js";
|
||||
|
||||
export const PLAN_COMMAND: SkillInfo = {
|
||||
name: "plan",
|
||||
description: "Toggle Plan mode for this chat",
|
||||
description: m.plan_command_description(),
|
||||
source: "command",
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export const ELEVATED_SURFACE_SHADOW_CLASS_NAME =
|
||||
export const ICON_BUTTON_BASE_CLASS_NAME = [
|
||||
"icon-btn relative inline-flex items-center justify-center",
|
||||
"text-subtext [&:hover]:text-text [&:hover]:bg-surface",
|
||||
"[.chat-header.rail-hidden_>_&:first-child]:mr-3 [&.active]:text-primary",
|
||||
"[.chat-header.rail-hidden_>_&:first-child]:me-3 [&.active]:text-primary",
|
||||
"[&.active]:bg-surface",
|
||||
].join(" ");
|
||||
|
||||
@@ -25,7 +25,7 @@ export const COMPOSER_ICON_CONTROL_CLASS_NAME = [
|
||||
|
||||
export const MODEL_ITEM_CLASS_NAME = [
|
||||
"model-item [&.danger]:text-accent-red [&.danger:hover]:text-accent-red flex",
|
||||
"items-center justify-between gap-2 w-full text-left py-1.5 px-2",
|
||||
"items-center justify-between gap-2 w-full text-start py-1.5 px-2",
|
||||
"text-md rounded-sm [&:hover]:bg-surface",
|
||||
"[&_.model-id]:block [&_.model-id]:font-mono [&_.model-id]:text-2xs",
|
||||
"[&_.model-id]:text-muted",
|
||||
@@ -114,7 +114,7 @@ export const STATUS_BADGE_CLASS_NAME = [
|
||||
export const SETTINGS_SWITCH_CLASS_NAME = [
|
||||
"settings-switch relative flex-none w-9.5 h-5.5 border border-border rounded-full",
|
||||
"bg-surface transition-[background,border-color] duration-120 ease-standard",
|
||||
"[&_span]:absolute [&_span]:top-[3px] [&_span]:left-[3px] [&_span]:w-3.5 [&_span]:h-3.5",
|
||||
"[&_span]:absolute [&_span]:top-[3px] [&_span]:start-[3px] [&_span]:w-3.5 [&_span]:h-3.5",
|
||||
"[&_span]:rounded-full [&_span]:bg-muted [&_span]:transition-[translate,background]",
|
||||
"[&_span]:duration-120 [&_span]:ease-standard [&.on]:border-primary [&.on]:bg-primary",
|
||||
"[&.on_span]:bg-background [&.on_span]:translate-x-4 [&:disabled]:opacity-45",
|
||||
|
||||
@@ -18,6 +18,25 @@
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
:root[lang="fa"] #root :where(p, h1, h2, h3, h4, h5, h6, button, label, li, th, td, dt, dd, [role="status"], [role="alert"]) {
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
.md :where(p, h1, h2, h3, h4, li, th, td, blockquote),
|
||||
:root[lang="fa"] #root .file-view-note {
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
/* The exception preserves DirFooter's RTL front-ellipsis truncation trick. */
|
||||
:where(pre, code:not(.path-front-ellipsis), .font-mono, .xterm, .openresearch-diff) {
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
.path-front-ellipsis {
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-*: initial;
|
||||
--color-transparent: transparent;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { compileLatex, getLatexEngine } from "./api";
|
||||
import { m } from "./paraglide/messages.js";
|
||||
|
||||
interface CompiledPdf {
|
||||
/** Checkout-relative path of the PDF, for the raw-file URL. */
|
||||
@@ -133,7 +134,7 @@ export function useLatexCompile({
|
||||
setBuiltWithErrors(false);
|
||||
setNote(result.note);
|
||||
setShowPdf(false);
|
||||
setLog(result.log?.trim() || "The engine exited without producing a PDF or a log.");
|
||||
setLog(result.log?.trim() || m.latex_no_pdf_or_log());
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
setCompiled(null);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { cp, mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import test from "node:test";
|
||||
|
||||
async function fixture() {
|
||||
const root = await mkdtemp(join(tmpdir(), "orx-i18n-"));
|
||||
await mkdir(join(root, "scripts"));
|
||||
await cp(new URL("../scripts/check-i18n.mjs", import.meta.url), join(root, "scripts/check-i18n.mjs"));
|
||||
await cp(new URL("../messages", import.meta.url), join(root, "messages"), { recursive: true });
|
||||
await cp(new URL("../project.inlang", import.meta.url), join(root, "project.inlang"), { recursive: true });
|
||||
return root;
|
||||
}
|
||||
|
||||
const run = (root) => spawnSync(process.execPath, [join(root, "scripts/check-i18n.mjs")], { encoding: "utf8" });
|
||||
const catalog = async (root, locale) => JSON.parse(await readFile(join(root, "messages", `${locale}.json`), "utf8"));
|
||||
const save = (root, locale, value) => writeFile(join(root, "messages", `${locale}.json`), `${JSON.stringify(value, null, 2)}\n`);
|
||||
const expectFailure = (root, expected, message) => {
|
||||
const result = run(root);
|
||||
assert.notEqual(result.status, 0, message);
|
||||
assert.match(result.stderr, expected, message);
|
||||
};
|
||||
|
||||
test("catalog lint rejects missing files, key drift, and invalid values", async () => {
|
||||
const roots = [];
|
||||
try {
|
||||
const missingFile = await fixture(); roots.push(missingFile);
|
||||
await unlink(join(missingFile, "messages/fa.json"));
|
||||
expectFailure(missingFile, /fa: .*ENOENT/, "missing locale file");
|
||||
|
||||
const missingKey = await fixture(); roots.push(missingKey);
|
||||
const zhMissing = await catalog(missingKey, "zh-CN");
|
||||
delete zhMissing.common_save;
|
||||
await save(missingKey, "zh-CN", zhMissing);
|
||||
expectFailure(missingKey, /zh-CN: missing keys: common_save/, "missing key");
|
||||
|
||||
const unexpectedKey = await fixture(); roots.push(unexpectedKey);
|
||||
const faUnexpected = await catalog(unexpectedKey, "fa");
|
||||
faUnexpected.unexpected_test_key = "اضافی";
|
||||
await save(unexpectedKey, "fa", faUnexpected);
|
||||
expectFailure(unexpectedKey, /fa: unexpected keys: unexpected_test_key/, "unexpected key");
|
||||
|
||||
for (const [name, value, expected] of [
|
||||
["empty", "", /empty or non-string values: common_save/],
|
||||
["non-string", 7, /empty or non-string values: common_save/],
|
||||
]) {
|
||||
const root = await fixture(); roots.push(root);
|
||||
const fa = await catalog(root, "fa");
|
||||
fa.common_save = value;
|
||||
await save(root, "fa", fa);
|
||||
expectFailure(root, expected, name);
|
||||
}
|
||||
} finally {
|
||||
await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })));
|
||||
}
|
||||
});
|
||||
@@ -9,7 +9,7 @@ test("Tinker is always visible in compute and environment settings", async () =>
|
||||
source("../src/components/SettingsPage.tsx"),
|
||||
source("../src/components/BackendLogos.tsx"),
|
||||
]);
|
||||
assert.match(settings, /tinker: "Tinker"/);
|
||||
assert.match(settings, /tinker: m\.compute_target_tinker/);
|
||||
assert.match(settings, /target\.id === "tinker"/);
|
||||
assert.match(settings, /"TINKER_API_KEY", "HF_TOKEN", "WANDB_API_KEY"/);
|
||||
assert.match(logos, /case "tinker_job":\s+return <TinkerLogo/);
|
||||
|
||||
+13
-1
@@ -1,3 +1,4 @@
|
||||
import { paraglideVitePlugin } from "@inlang/paraglide-js";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { defineConfig } from "vite";
|
||||
@@ -7,7 +8,18 @@ import { defineConfig } from "vite";
|
||||
const backend = process.env.ORX_BACKEND ?? "http://127.0.0.1:4791";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
plugins: [
|
||||
paraglideVitePlugin({
|
||||
project: "./project.inlang",
|
||||
outdir: "./src/paraglide",
|
||||
outputStructure: "message-modules",
|
||||
emitTsDeclarations: true,
|
||||
localStorageKey: "orx:locale",
|
||||
strategy: ["localStorage", "preferredLanguage", "baseLocale"],
|
||||
}),
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
build: { outDir: "dist" },
|
||||
server: {
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user