Compare commits

...

5 Commits

Author SHA1 Message Date
Claude
b38e410fa3
Merge remote-tracking branch 'origin/main' into claude/amazing-thompson-bgplnq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSVQGR4WU92LaddTP19GcQ
2026-10-03 15:12:07 +00:00
cynarAI
5d65b8ef97
ci: resolve attestation eligibility from repository API (#106)
* ci: resolve attestation eligibility from repository API

The publish guard in #103 used github.repository_owner_type, which is not
part of the documented github context and evaluates empty on push. That
made the OR condition true and the attest step always ran.

Query repos/{owner}/{repo} for owner.type and private, skip attestation
only for User-owned private repositories, and gate the attest action on
the resolved step output.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

* test: escape workflow placeholders for Biome noTemplateCurlyInString

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-03 17:09:58 +02:00
Claude
2736380934
docs(agents): make routine calls without asking
Agents in this repository stopped to ask the maintainer about routine
steps such as reopening a pull request or linking CI screenshots. The
rule asks them to decide these themselves, verify the result and report
it, and to ask only before irreversible actions or for product decisions
without a sensible default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSVQGR4WU92LaddTP19GcQ
2026-10-03 14:58:49 +00:00
Claude
53ec88ac7d
fix(web): keep secondary shell text readable in the light theme
Secondary shell text was muted text dimmed further with opacity: the
section labels, the projects caption, the sidebar "+" icons, the privacy
panel, the trading subtitle and the Heute brief's Mission Control link.
Muted text measures 4.4:1 on the light sidebar, so these fell to 1.9 to
3.4:1 in the light theme; a few were below 4.5:1 in dark as well.

- Secondary sidebar text and icons share `rk-sidebar-muted`, the sidebar
  foreground at 65% that row times already used (5.0:1 on the light
  sidebar). Row times now use the class too.
- Section labels and space or project group titles use the dimmed
  foreground in light and keep the 80% muted color in dark; group titles
  move up from 60%, which was 3.4:1 in dark.
- The privacy panel heading is plain text with a success-colored check
  instead of emerald text, and its details pass 4.5:1 in both themes.
- Needs-you chips keep the warning tint and border; their label uses the
  foreground color in light.
- The Mission Control link uses the full muted color.
- E2E: the shell test checks every text in the sidebar except the logo,
  and the Heute brief, in both themes, and captures
  `shell-contrast-{light,dark}-heute`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSVQGR4WU92LaddTP19GcQ
2026-10-03 14:58:48 +00:00
cynarAI
dc1d28c670
fix(web): make the shell follow the light theme (#107)
The composer, the shell frame and sidebar, the right side panel and the
create-bot modal were hard-coded dark while the text on them follows the
theme, so in the light theme the typed draft, the sidebar rows and the
side panel forms rendered dark on dark.

- Composer: the island uses the card and foreground tokens; attach, stop
  and chips use border and foreground tokens, and send is the default
  primary button. The focus and shadow utilities on the bar are removed:
  .apple-island always overrode them, so they never rendered.
- Shell frame and page background use the background token. The frame's
  radial gradient never rendered under Tailwind v4 and is dropped.
- Sidebar: the panel uses the sidebar tokens on desktop too. The main
  conversation, roster, project and Heute rows share one token-based
  row style; muted text measures 4.4:1 on the light sidebar, so row
  times use a dimmed foreground instead.
- Side panel and create-bot modal use background, popover and overlay
  tokens. Settings helper texts and inactive tabs use the full muted
  color, which stays above 4.5:1 in light.
- Stage vignette, stage pill, ensemble and hero orb, and the transcript
  paths behind the call overlay use tokens.
- E2E: chat-contrast.spec.ts gains a shell test that measures the
  composer draft, sidebar rows, the new bot form and the bot settings
  form in both themes and captures screenshots.


Claude-Session: https://claude.ai/code/session_01SSVQGR4WU92LaddTP19GcQ

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-03 15:27:51 +02:00
16 changed files with 288 additions and 175 deletions

View File

@ -256,9 +256,23 @@ jobs:
grep -Fqx "$want" <<< "$platforms" || { echo "$first_tag is missing $want" >&2; exit 1; }
done
echo "digest=$(docker buildx imagetools inspect "$first_tag" --format '{{.Manifest.Digest}}')" >> "$GITHUB_OUTPUT"
# GitHub artifact attestations are unavailable for private repos on personal accounts.
# Owner type is not exposed on the github context; resolve it from the repository metadata API.
- name: Resolve registry attestation eligibility
id: attestation
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
owner_type="$(gh api "repos/${GITHUB_REPOSITORY}" --jq -r '.owner.type')"
private="$(gh api "repos/${GITHUB_REPOSITORY}" --jq -r '.private')"
if [[ "$owner_type" == "User" && "$private" == "true" ]]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "Skipping registry attestation: unavailable for private repositories on personal accounts."
else
echo "enabled=true" >> "$GITHUB_OUTPUT"
fi
- name: Attest the published image
if: github.event.repository.private != true || github.repository_owner_type != 'User'
if: steps.attestation.outputs.enabled == 'true'
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-name: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }}

View File

@ -10,6 +10,7 @@
- Keep code simple: reuse existing primitives and one source of truth, remove duplication and unused flexibility, and avoid speculative abstractions. Add an interface when it protects a real external or platform boundary, not for its own sake.
- Treat auth, secret handling, sandbox boundaries, host commands, and integrations as security-sensitive. Keep tests deterministic and offline by default.
- The desktop Playwright suite (`pnpm --filter @rakazo/desktop test:e2e`) opens real Electron windows and steals focus on macOS. Do not run it on a maintainer's machine as routine verification; run the unit tests locally and let CI's virtual-display job run the e2e on push.
- Work autonomously. Make routine calls yourself, such as opening or reopening a pull request, linking CI screenshots, or fixing gaps you found that belong to the task, then verify and report the outcome. Ask the maintainer only before irreversible actions or for product decisions without a sensible default.
- After creating a pull request, stay with it until CI and automated review bots have finished; passing checks alone do not mean the review is complete. Follow `.agents/skills/pr-watch/SKILL.md`: run its `pr-digest --watch` helper as a single backgrounded call that blocks until the checks on the current head commit are terminal, then act on the verdict it prints. Do not poll in the foreground on a timer, which mostly buys repeated snapshots of an unchanged PR and dies at the tool timeout. Address every actionable issue, push the fixes, and repeat until no actionable feedback remains. Do not merge while review bots are still pending or review issues remain unresolved.
- PR descriptions say why the change exists, what changed, and how it was tested. Write Why as the product reason (what was wrong and what this changes); never "Elie wants" or a third-person briefing of a named person; the maintainer is the author.
- Commit messages, PR descriptions, issues, and review replies are public too. Describe test results in words instead of pasting tool output, and never include anything that identifies a person, machine, or account: local paths, usernames, hostnames, signing identities, legal entity names, Apple Team IDs, API key IDs, issuer or tenant IDs, account emails, or the accounts, machines, and files that secret values came from. Naming the secrets a workflow reads is fine; use placeholders when a value must be shown.

View File

@ -1,58 +1,121 @@
import { expect, type Locator, type Page, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, rpc, signup, waitForChatComposer } from "./helpers";
import {
captureScreenshot,
completeOnboarding,
createNamedBot,
openNewBot,
rpc,
signup,
waitForChatComposer,
} from "./helpers";
// WCAG AA for body text.
const MIN_CONTRAST = 4.5;
test.use({ viewport: { width: 1280, height: 1000 } });
/** Contrast of an element's text against the surface it is painted on. */
function textContrast(locator: Locator) {
return locator.evaluate((element) => {
const context = document.createElement("canvas").getContext("2d", { willReadFrequently: true });
if (!context) throw new Error("No 2D canvas context");
// The canvas normalizes every CSS color syntax (oklab, color-mix, ...) to sRGB.
const rgba = (color: string) => {
context.clearRect(0, 0, 1, 1);
context.fillStyle = color;
context.fillRect(0, 0, 1, 1);
const [r = 0, g = 0, b = 0, a = 0] = context.getImageData(0, 0, 1, 1).data;
return [r / 255, g / 255, b / 255, a / 255] as const;
};
// One pixel of `color`, painted inside `from` and composited through every ancestor's
// background color and opacity onto the white canvas.
const paint = (color: readonly [number, number, number, number], from: Element) => {
let [r, g, b, a] = color;
for (let node: Element | null = from; node; node = node.parentElement) {
const style = getComputedStyle(node);
const [br, bg, bb, ba] = rgba(style.backgroundColor);
const alpha = a + ba * (1 - a);
if (alpha > 0) {
r = (r * a + br * ba * (1 - a)) / alpha;
g = (g * a + bg * ba * (1 - a)) / alpha;
b = (b * a + bb * ba * (1 - a)) / alpha;
/**
* Texts in `locator` below WCAG AA against the surface they are painted on. With `inside`, every
* visible text and typed value within it counts, except in disabled controls, which WCAG exempts,
* and in elements matching `skip`.
*/
function unreadableTexts(locator: Locator, inside: boolean, skip: string | null) {
return locator.evaluate(
(root, { inside, skip, min }) => {
const context = document
.createElement("canvas")
.getContext("2d", { willReadFrequently: true });
if (!context) throw new Error("No 2D canvas context");
// The canvas normalizes every CSS color syntax (oklab, color-mix, ...) to sRGB.
const rgba = (color: string) => {
context.clearRect(0, 0, 1, 1);
context.fillStyle = color;
context.fillRect(0, 0, 1, 1);
const [r = 0, g = 0, b = 0, a = 0] = context.getImageData(0, 0, 1, 1).data;
return [r / 255, g / 255, b / 255, a / 255] as const;
};
// One pixel of `color`, painted inside `from` and composited through every ancestor's
// background color and opacity onto the white canvas.
const paint = (color: readonly [number, number, number, number], from: Element) => {
let [r, g, b, a] = color;
for (let node: Element | null = from; node; node = node.parentElement) {
const style = getComputedStyle(node);
const [br, bg, bb, ba] = rgba(style.backgroundColor);
const alpha = a + ba * (1 - a);
if (alpha > 0) {
r = (r * a + br * ba * (1 - a)) / alpha;
g = (g * a + bg * ba * (1 - a)) / alpha;
b = (b * a + bb * ba * (1 - a)) / alpha;
}
a = alpha * Number(style.opacity);
}
a = alpha * Number(style.opacity);
}
return [r * a + 1 - a, g * a + 1 - a, b * a + 1 - a] as const;
};
const luminance = (rgb: readonly [number, number, number]) => {
const [r = 0, g = 0, b = 0] = rgb.map((channel) =>
channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4,
);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const text = luminance(paint(rgba(getComputedStyle(element).color), element));
const surface = luminance(paint([0, 0, 0, 0], element));
return (Math.max(text, surface) + 0.05) / (Math.min(text, surface) + 0.05);
});
return [r * a + 1 - a, g * a + 1 - a, b * a + 1 - a] as const;
};
const luminance = (rgb: readonly [number, number, number]) => {
const [r = 0, g = 0, b = 0] = rgb.map((channel) =>
channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4,
);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const contrast = (element: Element) => {
const text = luminance(paint(rgba(getComputedStyle(element).color), element));
const surface = luminance(paint([0, 0, 0, 0], element));
return (Math.max(text, surface) + 0.05) / (Math.min(text, surface) + 0.05);
};
// What the element itself shows: a typed value, or its own text nodes.
const ownText = (element: Element) => {
if (element instanceof HTMLTextAreaElement) return element.value;
if (element instanceof HTMLInputElement) {
return /^(text|search|url|email|tel|number|password)$/.test(element.type)
? element.value
: "";
}
return Array.from(element.childNodes, (node) =>
node.nodeType === Node.TEXT_NODE ? node.textContent : "",
)
.join("")
.trim();
};
const shown = (element: Element) => {
const { width, height } = element.getBoundingClientRect();
return width > 1 && height > 1 && element.checkVisibility();
};
const elements = inside
? [root, ...root.querySelectorAll("*")].filter(
(element) =>
ownText(element) &&
shown(element) &&
!element.closest(":disabled") &&
!(skip && element.closest(skip)),
)
: [root];
return elements.flatMap((element) => {
const ratio = contrast(element);
const text = ownText(element) || element.textContent?.trim() || "";
return ratio < min ? [`${ratio.toFixed(2)}:1 ${text.slice(0, 48)}`] : [];
});
},
{ inside, skip, min: MIN_CONTRAST },
);
}
async function expectReadable(locator: Locator, what: string) {
async function expectReadable(
locator: Locator,
what: string,
{ inside = false, skip = null }: { inside?: boolean; skip?: string | null } = {},
) {
await expect(locator).toBeVisible();
await expect
.poll(() => textContrast(locator), { message: `${what} is readable` })
.toBeGreaterThanOrEqual(MIN_CONTRAST);
.poll(() => unreadableTexts(locator, inside, skip), { message: `${what} is readable` })
.toEqual([]);
}
async function inEachTheme(page: Page, check: (theme: "light" | "dark") => Promise<void>) {
for (const theme of ["light", "dark"] as const) {
await page.emulateMedia({ colorScheme: theme });
await expect(page.locator("html")).toHaveAttribute("data-theme", theme);
await check(theme);
}
}
function runStatus(page: Page, groupId: string) {
@ -128,10 +191,7 @@ test("conversation text stays readable in light and dark themes", async ({ page
const brief = task.locator(".rk-chat-markdown p");
await expect(brief).toHaveText("collect three owl facts");
for (const theme of ["light", "dark"] as const) {
await page.emulateMedia({ colorScheme: theme });
await expect(page.locator("html")).toHaveAttribute("data-theme", theme);
await inEachTheme(page, async (theme) => {
await firstMessage.scrollIntoViewIfNeeded();
await captureScreenshot(page, testInfo, `chat-contrast-${theme}-reply`);
await expectReadable(firstMessage, `${theme}: the user's own message`);
@ -150,5 +210,55 @@ test("conversation text stays readable in light and dark themes", async ({ page
await captureScreenshot(page, testInfo, `chat-contrast-${theme}-ask`);
await expectReadable(question, `${theme}: the ask card question`);
await expectReadable(lastMessage, `${theme}: the latest user message`);
}
});
});
test("shell surfaces stay readable in light and dark themes", async ({ page }, testInfo) => {
await page.emulateMedia({ colorScheme: "light" });
await signup(page, `shell-contrast-${Date.now()}@rakazo.test`, "password12", "Contrast QA");
await completeOnboarding(page);
await createNamedBot(page, "Researcher");
const composer = page.getByRole("combobox", { name: "Message" });
await composer.fill("Draft a note about owls");
const sidebar = page.getByTestId("bots-sidebar");
const sidePanel = page.getByTestId("side-panel");
await expect(sidebar.locator("[data-main-conversation]")).toBeVisible();
await expect(sidebar.getByRole("button", { name: "Researcher, 1:1 conversation" })).toBeVisible();
await expect(page.getByTestId("trading-mode-status")).toBeVisible();
await inEachTheme(page, async (theme) => {
await captureScreenshot(page, testInfo, `shell-contrast-${theme}`);
await expectReadable(composer, `${theme}: the composer draft`);
await expectReadable(page.getByTestId("composer-bar"), `${theme}: the composer`, {
inside: true,
});
// The header carries the logo, which WCAG exempts; its create button is checked on its own.
await expectReadable(sidebar, `${theme}: the sidebar`, { inside: true, skip: ".app-drag" });
await expectReadable(page.getByTestId("create-menu-trigger"), `${theme}: the create button`);
});
await openNewBot(page);
await page.getByTestId("create-bot-form").locator("label:has-text('Name') input").fill("Owl");
await inEachTheme(page, async (theme) => {
await captureScreenshot(page, testInfo, `shell-contrast-${theme}-new-bot`);
await expectReadable(sidePanel, `${theme}: the new bot form`, { inside: true });
});
await page.getByRole("button", { name: "Cancel new bot" }).click();
await page.getByTestId("bot-settings-trigger").click();
const advanced = page.getByTestId("bot-settings-advanced");
await advanced.locator("summary").click();
await expect(advanced.getByText("Open work")).toBeVisible();
await inEachTheme(page, async (theme) => {
await captureScreenshot(page, testInfo, `shell-contrast-${theme}-settings`);
await expectReadable(sidePanel, `${theme}: the bot settings form`, { inside: true });
});
await sidebar.getByRole("button", { name: "Heute", exact: true }).click();
const heute = page.getByTestId("heute-brief");
await expect(heute.getByTestId("heute-mission-control-link")).toBeVisible();
await inEachTheme(page, async (theme) => {
await captureScreenshot(page, testInfo, `shell-contrast-${theme}-heute`);
await expectReadable(heute, `${theme}: the Heute brief`, { inside: true });
});
});

View File

@ -10,16 +10,16 @@ const TRUST_ITEMS = [
export const DataTrustPanel = memo(function DataTrustPanel() {
return (
<div className="hidden xl:flex flex-col items-start gap-1.5 rounded-xl border border-border/40 bg-muted/30 px-3 py-2.5">
<div className="flex items-center gap-1.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">
<CheckCircle2 className="size-3.5" />
<div className="flex items-center gap-1.5 text-xs font-medium text-sidebar-foreground">
<CheckCircle2 className="size-3.5 text-success" />
<span>Datenschutz aktiv</span>
</div>
<div className="flex flex-col gap-1">
{TRUST_ITEMS.map(({ icon: Icon, label, desc }) => (
<div key={label} className="flex items-center gap-1.5">
<Icon className="size-3 text-muted-foreground/60" />
<span className="text-[11px] leading-tight text-muted-foreground">{label}</span>
<span className="text-[10px] leading-tight text-muted-foreground/50">· {desc}</span>
<div key={label} className="flex items-center gap-1.5 rk-sidebar-muted">
<Icon className="size-3" />
<span className="text-[11px] leading-tight">{label}</span>
<span className="text-[10px] leading-tight">· {desc}</span>
</div>
))}
</div>

View File

@ -45,7 +45,7 @@ export function TradingModeIndicator({
<TradingModeLabel label={presentation.label} />
</span>
{presentation.subtitle ? (
<span className="block truncate text-ui-body-sm text-muted-foreground/70">
<span className="block truncate text-ui-body-sm rk-sidebar-muted">
<TradingModeSubtitleText subtitle={presentation.subtitle} />
</span>
) : null}

View File

@ -175,7 +175,7 @@ export function OoStage({
{(hasMessages || phase !== "idle") && (
<div className={`flex w-full justify-center ${hasMessages ? "mb-1" : "mb-3"}`}>
<div
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm"
className="inline-flex items-center gap-2 rounded-full border border-border bg-foreground/5 px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm"
style={
reducedMotion
? undefined
@ -280,7 +280,7 @@ function StageEnsemble({
return (
<li key={member.botId} className="flex flex-col items-center gap-1" title={member.name}>
<div
className={`relative flex size-14 items-center justify-center rounded-full border border-white/12 bg-slate-950/40 backdrop-blur-md ${
className={`relative flex size-14 items-center justify-center rounded-full border border-border bg-muted/40 backdrop-blur-md ${
isWorking && !reducedMotion ? "animate-pulse" : ""
}`}
>
@ -484,7 +484,7 @@ export function OoGroupStage({
className={`relative z-10 mx-auto flex w-full max-w-3xl flex-col items-center px-4 md:px-8 ${hasMessages ? "py-2 md:py-3" : "py-5 md:py-6"}`}
>
<div className="mb-3 w-full min-w-0 text-center">
<div className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm">
<div className="inline-flex items-center gap-2 rounded-full border border-border bg-foreground/5 px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm">
<span
className={`size-1.5 rounded-full ${
running ? "bg-violet-400" : "bg-emerald-400"

View File

@ -55,7 +55,7 @@ export function StageHero({
) : null}
<div
className={`relative flex ${orbSize} items-center justify-center rounded-full border border-white/14 bg-slate-950/45 shadow-[0_16px_48px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.12)] backdrop-blur-xl`}
className={`relative flex ${orbSize} items-center justify-center rounded-full border border-border bg-muted/45 shadow-[0_16px_48px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.12)] backdrop-blur-xl`}
style={
reducedMotion
? undefined

View File

@ -35,7 +35,7 @@ export function TextStageCanvas({
return (
<div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_100%_70%_at_50%_0%,rgba(12,16,24,0.55)_0%,transparent_68%)]" />
<div className="absolute inset-0 bg-radial-[ellipse_100%_70%_at_50%_0%] from-card/55 to-transparent to-68%" />
<div
className={`absolute left-1/2 top-0 h-[min(48vh,320px)] w-[min(72vw,480px)] -translate-x-1/2 rounded-full blur-[72px] ${reducedMotion ? "" : "voice-bloom-emerald"}`}
style={{

View File

@ -178,7 +178,7 @@ export function HeuteBrief({
href={MISSION_CONTROL_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-ui-label text-muted-foreground/70 transition-colors hover:text-muted-foreground"
className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-ui-label text-muted-foreground transition-colors hover:text-foreground"
data-testid="heute-mission-control-link"
>
<Trans>Mission Control</Trans>
@ -235,7 +235,7 @@ function EmptyDay({ onStartChat, hasBots }: { onStartChat: () => void; hasBots:
function ActionableChip({ label }: { label: string }) {
return (
<span className="shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-warning">
<span className="shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-foreground dark:text-warning">
{label}
</span>
);

View File

@ -106,7 +106,7 @@ export function ScratchpadSection({ botId }: { botId: string }) {
<Trans>Open work</Trans>
</div>
{items.length === 0 ? (
<div className="py-1 text-ui-body text-muted-foreground/80">
<div className="py-1 text-ui-body text-muted-foreground">
<Trans>None yet</Trans>
</div>
) : (

View File

@ -3354,7 +3354,7 @@ export function ShellPage() {
<div
data-testid="shell-root"
data-ready={shellReady}
className="relative flex h-full min-w-0 overflow-hidden bg-black bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-zinc-900 via-black to-black text-foreground/90 p-0 md:p-3 gap-0 md:gap-3"
className="relative flex h-full min-w-0 overflow-hidden bg-background text-foreground/90 p-0 md:p-3 gap-0 md:gap-3"
onTouchStartCapture={(event) => {
if (
mobileSidebarOpen ||
@ -3428,7 +3428,7 @@ export function ShellPage() {
width: mobileSidebarOpen ? undefined : botsSidebarCollapsed ? 0 : leftSidebarWidth,
maxWidth: mobileSidebarOpen ? 316 : undefined,
}}
className={`absolute inset-y-0 start-0 z-40 flex w-[calc(100%-48px)] shrink-0 flex-col md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl bg-sidebar md:static md:z-auto md:translate-x-0 ${
className={`absolute inset-y-0 start-0 z-40 flex w-[calc(100%-48px)] shrink-0 flex-col md:rounded-2xl md:border md:border-sidebar-border md:shadow-2xl bg-sidebar md:static md:z-auto md:translate-x-0 ${
mobileSidebarOpen ? "translate-x-0" : "-translate-x-full rtl:translate-x-full"
} ${
botsSidebarCollapsed
@ -3456,7 +3456,7 @@ export function ShellPage() {
className={`app-no-drag flex size-control-sm items-center justify-center rounded-full ${
activityMode
? "bg-primary text-primary-foreground"
: "text-muted-foreground/70 hover:text-foreground/75"
: "rk-sidebar-muted hover:text-foreground/75"
}`}
>
<Bell
@ -3469,7 +3469,7 @@ export function ShellPage() {
<Popover open={createMenuOpen} onOpenChange={setCreateMenuOpen}>
<PopoverTrigger
className="app-no-drag text-[21px] text-muted-foreground/70 hover:text-foreground/75"
className="app-no-drag text-[21px] rk-sidebar-muted hover:text-foreground/75"
title={t`Create`}
data-testid="create-menu-trigger"
>
@ -3565,13 +3565,9 @@ export function ShellPage() {
setActiveNavPillar("heute");
setMobileSidebarOpen(false);
}}
className={`flex w-full items-center gap-3 rounded-2xl px-3 py-3 text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
activeNavPillar === "heute"
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
: "hover:bg-white/[0.04]"
}`}
className={rosterRowClass(activeNavPillar === "heute")}
>
<div className="relative grid size-control-lg shrink-0 place-items-center rounded-full bg-emerald-500/10 text-emerald-400">
<div className="relative grid size-control-lg shrink-0 place-items-center rounded-full bg-muted text-muted-foreground">
<Calendar strokeWidth={2.2} className="size-icon-md" />
{heuteActionCount > 0 ? (
<span
@ -3596,7 +3592,7 @@ export function ShellPage() {
type="button"
aria-label={t`Neue Unterhaltung`}
data-testid="conversation-picker-trigger"
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
className="rk-sidebar-muted hover:text-foreground/90 transition-colors"
>
<Plus strokeWidth={2.5} className="size-icon-md" />
</PopoverTrigger>
@ -3637,7 +3633,7 @@ export function ShellPage() {
</div>
) : null}
{conversationSidebarGroups.length === 0 && !mainConversationEntry ? (
<div className="p-4 text-center text-ui-label text-muted-foreground">
<div className="p-4 text-center text-ui-label rk-sidebar-muted">
<p>{t`Noch keine Unterhaltung.`}</p>
<Button
variant="outline"
@ -3661,7 +3657,7 @@ export function ShellPage() {
<div className="flex items-center pt-3 pb-0.5">
<button
type="button"
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg px-2.5 py-1 text-ui-label font-semibold tracking-wider uppercase text-muted-foreground/60 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg px-2.5 py-1 text-ui-label font-semibold tracking-wider uppercase text-foreground/65 dark:text-muted-foreground/80 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
onClick={() => {
if (group.emptySpaceId) {
openSpaceChat(group.emptySpaceId, "/onboarding");
@ -3825,12 +3821,10 @@ export function ShellPage() {
status={item.chat.status}
/>
<span
className={`absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full border-2 border-[#0b0c10] ${
rosterStatusDot(item.chat.status) === "needs-you"
? "bg-warning animate-pulse"
: rosterStatusDot(item.chat.status) === "working"
? "bg-amber-400 animate-pulse"
: "bg-emerald-400"
className={`absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full border-2 border-sidebar ${
rosterStatusDot(item.chat.status) === "idle"
? "bg-success"
: "bg-warning animate-pulse"
}`}
/>
</div>
@ -3847,11 +3841,11 @@ export function ShellPage() {
{item.chat.name}
</span>
{needsYouChipLabel(item.chat.status) ? (
<span className="shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-warning">
<span className="shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-foreground dark:text-warning">
{needsYouChipLabel(item.chat.status)}
</span>
) : item.chat.title ? (
<span className="min-w-0 truncate rounded-full border border-emerald-500/25 bg-emerald-500/10 px-2 py-0.5 text-ui-label font-medium text-emerald-400">
<span className="min-w-0 truncate rounded-full border border-border bg-muted px-2 py-0.5 text-ui-label font-medium text-muted-foreground">
{item.chat.title}
</span>
) : null}
@ -3893,7 +3887,7 @@ export function ShellPage() {
[group.key]: !prev[group.key],
}))
}
className="mt-1 w-full flex items-center justify-center gap-1.5 rounded-lg px-2 py-1.5 text-ui-label font-medium text-muted-foreground hover:bg-sidebar-accent hover:text-foreground transition-colors"
className="mt-1 w-full flex items-center justify-center gap-1.5 rounded-lg px-2 py-1.5 text-ui-label font-medium rk-sidebar-muted hover:bg-sidebar-accent hover:text-foreground transition-colors"
>
{expandedTeamGroups[group.key]
? "Weniger anzeigen"
@ -3905,10 +3899,10 @@ export function ShellPage() {
})}
<div className="sidebar-projekte">
{/* PROJEKTE SECTION */}
<div className="flex items-center justify-between pt-4 pb-1 mt-3 border-t border-white/5 px-2.5">
<div className="flex items-center justify-between pt-4 pb-1 mt-3 border-t border-sidebar-border px-2.5">
<div>
<div className="rk-shell-section-label">{t`Projekte`}</div>
<p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70">
<p className="mt-0.5 px-0.5 text-ui-caption rk-sidebar-muted">
{t`Threads by topic · folders`}
</p>
</div>
@ -3918,14 +3912,14 @@ export function ShellPage() {
aria-label={t`Create a project`}
data-testid="create-project-trigger"
onClick={() => openNewSpaceDialog("project")}
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
className="rk-sidebar-muted hover:text-foreground/90 transition-colors"
>
<Plus strokeWidth={2.5} className="size-icon-md" />
</button>
</div>
</div>
{projectSidebarGroups.length === 0 ? (
<div className="p-4 text-center text-ui-label text-muted-foreground">
<div className="p-4 text-center text-ui-label rk-sidebar-muted">
<p>{t`No projects yet.`}</p>
<Button
variant="outline"
@ -3963,7 +3957,7 @@ export function ShellPage() {
</ProjectDragGrip>
<button
type="button"
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg pl-8 py-1 text-ui-label font-semibold tracking-wider uppercase text-muted-foreground/60 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg pl-8 py-1 text-ui-label font-semibold tracking-wider uppercase text-foreground/65 dark:text-muted-foreground/80 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
onClick={() => {
if (group.emptySpaceId) {
openSpaceChat(group.emptySpaceId, "/app");
@ -4092,15 +4086,11 @@ export function ShellPage() {
position: { x: event.clientX, y: event.clientY },
});
}}
className={`flex w-full items-center gap-3 rounded-2xl px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
item.kind === "bot" && !inGroup && active?.id === item.chat.id
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
: item.kind === "group" &&
inGroup &&
activeGroup?.id === item.chat.id
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
: "hover:bg-white/[0.05] border border-transparent"
}`}
className={rosterRowClass(
item.kind === "bot"
? !inGroup && active?.id === item.chat.id
: inGroup && activeGroup?.id === item.chat.id,
)}
>
{item.kind === "bot" ? (
<BotAvatar
@ -4117,34 +4107,30 @@ export function ShellPage() {
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
<span
dir="auto"
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${
item.chat.unread
? "font-bold text-white"
: "font-semibold text-white/95"
}`}
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${rosterNameClass(
item.chat.unread,
)}`}
>
{item.chat.name}
</span>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<span className="text-ui-body-sm text-zinc-400 tabular-nums font-medium">
<span className={rosterTimeClass}>
{formatRosterTime(item.chat.updatedAt)}
</span>
{item.chat.unread ? (
<span
aria-hidden="true"
className="inline-block h-2 w-2 rounded-full bg-emerald-400"
className={rosterUnreadDotClass}
/>
) : null}
</div>
</div>
<div
dir="auto"
className={`mt-0.5 truncate text-ui-body leading-snug ${
item.chat.unread
? "font-medium text-white"
: "text-zinc-300"
}`}
className={`mt-0.5 truncate text-ui-body leading-snug ${rosterPreviewClass(
item.chat.unread,
)}`}
>
{cleanRosterPreview(item.chat.preview)}
</div>
@ -4167,7 +4153,7 @@ export function ShellPage() {
type="button"
aria-expanded={archivedOpen}
onClick={() => setArchivedOpen((open) => !open)}
className="flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-ui-body text-muted-foreground hover:bg-sidebar-accent"
className="flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-ui-body rk-sidebar-muted hover:bg-sidebar-accent"
>
<span>
<Trans>Archived</Trans>
@ -4718,7 +4704,7 @@ export function ShellPage() {
? rightSidebarWidth
: 0,
}}
className={`absolute inset-y-0 end-0 z-20 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl md:relative ${
className={`absolute inset-y-0 end-0 z-20 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:rounded-2xl md:border md:border-border md:shadow-2xl md:relative ${
panel &&
(
active ||
@ -4822,8 +4808,8 @@ export function ShellPage() {
/>
{isDesktop ? (
devServerRunning ? (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-ui-label font-medium text-emerald-400">
<span className="size-1.5 rounded-full bg-emerald-400" />
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-ui-label font-medium text-foreground">
<span className="size-1.5 rounded-full bg-success" />
<Trans>Dev</Trans>
</span>
) : (
@ -4998,11 +4984,11 @@ export function ShellPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center">
<button
type="button"
className="absolute inset-0 bg-black/60"
className="absolute inset-0 bg-overlay"
aria-label={t`Close`}
onClick={() => setShowCreateBotModal(false)}
/>
<div className="relative z-10 max-h-[90vh] w-[480px] overflow-y-auto rounded-2xl border border-white/10 bg-[#0A0A0C] p-6 shadow-2xl">
<div className="relative z-10 max-h-[90vh] w-[480px] overflow-y-auto rounded-2xl border border-border bg-popover p-6 text-popover-foreground shadow-2xl">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-foreground">
<Trans>Create new Bot</Trans>
@ -6107,10 +6093,10 @@ const Transcript = memo(function Transcript({
<div className="relative mb-5 flex items-center justify-center p-3">
<EvooMascot size={76} pose="greeting" interactive trackPointer="hover" />
</div>
<h2 className="text-3xl font-bold tracking-tight text-white mb-2">
{t`Guten Tag.`} <span className="text-emerald-400">evOo</span> {t`ist bereit.`}
<h2 className="text-3xl font-bold tracking-tight text-foreground mb-2">
{t`Guten Tag.`} evOo {t`ist bereit.`}
</h2>
<p className="max-w-md text-ui-body leading-relaxed text-zinc-300 mb-8 font-normal">
<p className="max-w-md text-ui-body leading-relaxed text-foreground/85 mb-8 font-normal">
{activeName
? t`Frag ${activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews.`
: t`Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt.`}
@ -6121,13 +6107,13 @@ const Transcript = memo(function Transcript({
onClick={() => {
onPromptSelect?.(t`⚡ Führe einen autonomen Portfolio- und Risiko-Check durch`);
}}
className="apple-glass-card group flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] hover:border-emerald-500/40 cursor-pointer"
className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer"
>
<span className="text-xl mb-2">⚡</span>
<span className="text-[14.5px] font-semibold text-white group-hover:text-emerald-400 transition-colors">
<span className="text-[14.5px] font-semibold text-foreground">
{t`Portfolio-Check`}
</span>
<span className="text-ui-body-sm text-zinc-400 mt-1 leading-snug">
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
{t`Echtzeit-Risiko & Drawdown-Analyse`}
</span>
</button>
@ -6136,13 +6122,13 @@ const Transcript = memo(function Transcript({
onClick={() => {
onPromptSelect?.(t`📈 Scanne den Markt nach aktuellen Alpha-Signalen`);
}}
className="apple-glass-card group flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] hover:border-sky-500/40 cursor-pointer"
className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer"
>
<span className="text-xl mb-2">📈</span>
<span className="text-[14.5px] font-semibold text-white group-hover:text-sky-400 transition-colors">
<span className="text-[14.5px] font-semibold text-foreground">
{t`Alpha-Signale`}
</span>
<span className="text-ui-body-sm text-zinc-400 mt-1 leading-snug">
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
{t`Volatilität & Orderbuch-Liquidität`}
</span>
</button>
@ -6151,13 +6137,13 @@ const Transcript = memo(function Transcript({
onClick={() => {
onPromptSelect?.(t`💎 Führe ein Steve Jobs UX & Architektur-Audit durch`);
}}
className="apple-glass-card group flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] hover:border-purple-500/40 cursor-pointer"
className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer"
>
<span className="text-xl mb-2">💎</span>
<span className="text-[14.5px] font-semibold text-white group-hover:text-purple-400 transition-colors">
<span className="text-[14.5px] font-semibold text-foreground">
{t`Steve Jobs Audit`}
</span>
<span className="text-ui-body-sm text-zinc-400 mt-1 leading-snug">
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
{t`Schlichtheit, Eleganz & Detailtiefe`}
</span>
</button>
@ -6928,7 +6914,7 @@ const Composer = memo(function Composer({
) : null}
<div
data-testid="composer-bar"
className="apple-island relative flex items-center gap-3.5 rounded-[30px] py-3 pe-3.5 ps-4 transition-all duration-200 focus-within:border-emerald-500/40 focus-within:shadow-[0_20px_50px_-10px_rgba(0,0,0,0.85),inset_0_1px_0_rgba(255,255,255,0.22),0_0_0_1px_rgba(16,185,129,0.35)] shadow-[0_16px_40px_-8px_rgba(0,0,0,0.7),inset_0_1px_0_rgba(255,255,255,0.14)]"
className="apple-island relative flex items-center gap-3.5 rounded-[30px] py-3 pe-3.5 ps-4 transition-all duration-200"
>
<input
ref={fileInputRef}
@ -6944,7 +6930,7 @@ const Composer = memo(function Composer({
aria-label={t`Attach file`}
disabled={disabled}
onClick={() => fileInputRef.current?.click()}
className="tactile-press size-10 shrink-0 rounded-full border border-white/10 bg-white/[0.06] text-foreground/80 transition-all hover:bg-white/[0.12] hover:text-white flex items-center justify-center cursor-pointer"
className="tactile-press size-10 shrink-0 rounded-full border border-border bg-foreground/5 text-foreground/80 transition-all hover:bg-foreground/10 hover:text-foreground flex items-center justify-center cursor-pointer"
>
<Plus strokeWidth={2.2} className="size-icon-lg" />
</Button>
@ -6952,9 +6938,9 @@ const Composer = memo(function Composer({
{selectedSkill ? (
<span
data-testid="skill-chip"
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-border bg-foreground/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
>
<Box strokeWidth={1.8} className="shrink-0 text-blue-400 size-icon-md" />
<Box strokeWidth={1.8} className="shrink-0 text-muted-foreground size-icon-md" />
<span dir="auto" className="truncate">
{selectedSkill.name}
</span>
@ -6973,7 +6959,7 @@ const Composer = memo(function Composer({
key={mentionChipKey(mention)}
data-testid="mention-chip"
data-mention-kind={mention.kind}
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-border bg-foreground/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
>
<MentionChipIcon mention={mention} />
<span dir="auto" className="truncate">
@ -7053,7 +7039,7 @@ const Composer = memo(function Composer({
autoComplete="off"
dir="auto"
rows={1}
className="max-h-36 min-h-[28px] min-w-[8rem] flex-1 resize-none overflow-y-auto bg-transparent py-1 text-[16.5px] leading-[1.55] text-foreground outline-none placeholder:text-muted-foreground/75 disabled:opacity-40 font-normal selection:bg-emerald-500/30"
className="max-h-36 min-h-[28px] min-w-[8rem] flex-1 resize-none overflow-y-auto bg-transparent py-1 text-[16.5px] leading-[1.55] text-foreground outline-none placeholder:text-muted-foreground/75 disabled:opacity-40 font-normal"
/>
</div>
{onVoice ? (
@ -7086,7 +7072,7 @@ const Composer = memo(function Composer({
aria-label={t`Send`}
disabled={!canSend || disabled}
onClick={send}
className="tactile-press size-10 rounded-full bg-emerald-400 text-slate-950 hover:bg-emerald-300 shadow-[0_0_16px_rgba(16,185,129,0.35)] transition-transform active:scale-90 flex items-center justify-center cursor-pointer"
className="tactile-press size-10 rounded-full transition-transform active:scale-90 flex items-center justify-center cursor-pointer"
>
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
</Button>
@ -7096,7 +7082,7 @@ const Composer = memo(function Composer({
aria-label={t`Stop`}
disabled={sending}
onClick={() => void onStop()}
className="tactile-press size-10 rounded-full border border-white/15 bg-white/[0.08] text-foreground/90 shadow-sm transition-colors hover:bg-white/[0.15] hover:text-white flex items-center justify-center cursor-pointer"
className="tactile-press size-10 rounded-full border border-border bg-foreground/[0.08] text-foreground/90 shadow-sm transition-colors hover:bg-foreground/15 hover:text-foreground flex items-center justify-center cursor-pointer"
>
<Square strokeWidth={0} fill="currentColor" className="size-icon-sm" />
</Button>
@ -7107,7 +7093,7 @@ const Composer = memo(function Composer({
aria-label={t`Send`}
disabled={!canSend || disabled}
onClick={send}
className="tactile-press size-10 shrink-0 rounded-full bg-emerald-400 text-slate-950 hover:bg-emerald-300 shadow-[0_0_16px_rgba(16,185,129,0.35)] transition-all active:scale-90 disabled:bg-white/[0.08] disabled:text-white/20 disabled:shadow-none flex items-center justify-center cursor-pointer"
className="tactile-press size-10 shrink-0 rounded-full transition-all active:scale-90 disabled:bg-foreground/[0.08] disabled:text-foreground/20 flex items-center justify-center cursor-pointer"
>
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
</Button>
@ -7504,19 +7490,22 @@ const MessageView = memo(function MessageView({
{effectiveSpeakerName && message.role === "bot" && !encounterMode ? (
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="relative flex items-center justify-center size-7 rounded-xl bg-white/[0.08] border border-white/[0.12] shadow-xs">
<div className="relative flex items-center justify-center size-7 rounded-xl bg-foreground/[0.08] border border-border shadow-xs">
<EvooMascot size={22} interactive={false} pose={isLive ? "working" : "idle"} />
</div>
<div className="flex items-center gap-2">
<span className="text-[14.5px] font-semibold text-white tracking-tight" dir="auto">
<span
className="text-[14.5px] font-semibold text-foreground tracking-tight"
dir="auto"
>
{effectiveSpeakerName}
</span>
<span className="rounded-full bg-emerald-500/15 border border-emerald-500/25 px-2 py-0.5 text-ui-label font-medium text-emerald-400">
<span className="rounded-full border border-border bg-muted px-2 py-0.5 text-ui-label font-medium text-muted-foreground">
Oo
</span>
</div>
</div>
<span className="text-ui-label font-mono text-zinc-400 tabular-nums">
<span className="text-ui-label font-mono text-muted-foreground tabular-nums">
{new Date(message.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",

View File

@ -441,7 +441,7 @@ export function BotSettings({
}}
/>
</div>
<label htmlFor={`${ids}-name`} className="mt-4 block text-ui-body text-muted-foreground/80">
<label htmlFor={`${ids}-name`} className={fieldLabelClass}>
<Trans>Name</Trans>
<Input
id={`${ids}-name`}
@ -484,10 +484,7 @@ export function BotSettings({
>
<Trans>Notifications</Trans>
</div>
<div
id={`${ids}-notify-finish-desc`}
className="text-ui-body-sm text-muted-foreground/70"
>
<div id={`${ids}-notify-finish-desc`} className="text-ui-body-sm text-muted-foreground">
<Trans>Get notified when this Bot finishes or needs input</Trans>
</div>
</div>

View File

@ -1,20 +1,20 @@
/** One look for sidebar conversation rows: the main conversation entry and the bot roster. */
export function rosterRowClass(selected: boolean): string {
return `flex w-full items-center gap-3 rounded-2xl px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
return `flex w-full items-center gap-3 rounded-2xl border px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
selected
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
: "hover:bg-white/[0.05] border border-transparent"
? "border-sidebar-border bg-sidebar-accent shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)]"
: "border-transparent hover:bg-sidebar-accent/50"
}`;
}
export function rosterNameClass(unread: boolean): string {
return unread ? "font-bold text-white" : "font-semibold text-white/95";
return unread ? "font-bold text-sidebar-foreground" : "font-semibold text-sidebar-foreground";
}
export const rosterTimeClass = "text-ui-body-sm text-zinc-400 tabular-nums font-medium";
export const rosterTimeClass = "text-ui-body-sm rk-sidebar-muted tabular-nums font-medium";
export const rosterUnreadDotClass = "inline-block h-2 w-2 rounded-full bg-emerald-400";
export const rosterUnreadDotClass = "inline-block h-2 w-2 rounded-full bg-sidebar-foreground";
export function rosterPreviewClass(unread: boolean): string {
return unread ? "font-medium text-white" : "text-zinc-300";
return unread ? "font-medium text-sidebar-foreground" : "text-sidebar-foreground/85";
}

View File

@ -11,13 +11,7 @@ body,
#root {
height: 100%;
margin: 0;
background: #07090e;
background-image: radial-gradient(
circle at 50% 0%,
rgba(16, 185, 129, 0.07) 0%,
rgba(14, 165, 233, 0.04) 40%,
transparent 80%
);
background: var(--background);
color: var(--foreground);
font-family:
"Inter Variable", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Geist Variable",
@ -41,8 +35,8 @@ body,
.apple-island {
backdrop-filter: blur(36px) saturate(210%);
-webkit-backdrop-filter: blur(36px) saturate(210%);
background: rgba(12, 16, 24, 0.88);
border: 1px solid rgba(255, 255, 255, 0.13);
background: color-mix(in srgb, var(--card) 88%, transparent);
border: 1px solid color-mix(in srgb, var(--foreground) 13%, transparent);
box-shadow:
0 18px 44px -8px rgba(0, 0, 0, 0.75),
inset 0 1px 0 rgba(255, 255, 255, 0.16);
@ -79,7 +73,7 @@ body,
/* Shell layout utilities — token-backed, use instead of one-off pixel classes */
@layer components {
.rk-shell-section-label {
@apply text-ui-label font-bold tracking-wider uppercase text-muted-foreground/80;
@apply text-ui-label font-bold tracking-wider uppercase text-foreground/65 dark:text-muted-foreground/80;
}
.rk-shell-icon-btn {
@ -90,6 +84,12 @@ body,
@apply text-ui-body-sm text-muted-foreground;
}
/* Secondary sidebar text and icons. Muted text measures 4.4:1 on the light sidebar;
a dimmed foreground stays above 4.5:1. */
.rk-sidebar-muted {
@apply text-sidebar-foreground/65;
}
.rk-shell-row-title {
@apply text-ui-body font-medium text-foreground;
}
@ -224,11 +224,11 @@ body,
background: transparent;
}
.rk-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.14);
background: var(--scrollbar);
border-radius: 3px;
}
.rk-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.28);
background: var(--scrollbar-hover);
}
.app-drag {

View File

@ -38,12 +38,14 @@ describe("server image publish workflow", () => {
expect(publish.needs).toBe("build");
expect(workflowText).toContain("push-by-digest=true");
expect(workflowText).toContain("docker buildx imagetools create");
expect(workflowText).toContain('if [[ "${#sources[@]}" -ne 1 ]]');
expect(workflowText).toContain(`if [[ "\${#sources[@]}" -ne 1 ]]`);
expect(workflowText).toContain("for want in linux/amd64; do");
expect(workflowText).toContain("actions/attest-build-provenance@");
expect(workflowText).toContain(
"if: github.event.repository.private != true || github.repository_owner_type != 'User'",
);
expect(workflowText).not.toContain("github.repository_owner_type");
expect(workflowText).toContain(`gh api "repos/\${GITHUB_REPOSITORY}"`);
expect(workflowText).toContain(".owner.type");
expect(workflowText).toContain("if: steps.attestation.outputs.enabled == 'true'");
expect(workflowText).toContain('[[ "$owner_type" == "User" && "$private" == "true" ]]');
});
it("stores digest markers under RUNNER_TEMP so self-hosted runners do not leak stale files", () => {
@ -52,11 +54,11 @@ describe("server image publish workflow", () => {
});
it("names every image under the lowercase repository namespace", () => {
expect(workflowText).not.toContain("ghcr.io/${{ github.repository }}");
expect(workflowText).not.toContain(`ghcr.io/\${{ github.repository }}`);
const references =
workflowText.match(/ghcr\.io\/\$\{\{ steps\.ns\.outputs\.repo \}\}\//g) ?? [];
expect(references.length).toBeGreaterThanOrEqual(6);
expect(workflowText).toContain("repo=${GITHUB_REPOSITORY,,}");
expect(workflowText).toContain(`repo=\${GITHUB_REPOSITORY,,}`);
});
it("keeps pull requests read-only and every action pinned to a commit", () => {

View File

@ -49,7 +49,7 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-ui-body-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-icon-md",
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-ui-body-sm font-medium whitespace-nowrap text-muted-foreground transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-icon-md",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",