1c5521cf2e
UI
- No animation runs forever: Sagi moods, the custom pet sprite, status dots
(pulse-dot, pulse-slow, ping) and the chat shimmer play a few cycles and
rest. An infinite animation kept the browser drawing 60 frames a second.
- useSagiBrain: frames that only move lastActivityAt by <1 s keep the old
state; the 1 s tick re-renders only when the mood changes.
- ConversationView and MessageList are memoized and PaneChat passes stable
callbacks: typing in the chat box no longer re-renders the conversation.
- The chat's 700 ms menu poll reads plain text first and scans the terminal
cell by cell (reusing one cell) only when a row could be a menu cursor row.
Server
- GET /sessions/:id/transcript?after=N skips lines <= N before JSON.parse.
- GET /sessions/:id/outline is cached until the session's events change.
- broadcastLane uses the cached tmux.liveSessions(); tmux.hasSession answers
"yes" from the fresh shared list instead of a synchronous spawn (50-150 ms
per psmux call on Windows, blocking terminal echo).
- pane-mode polls every 150 ms; writeHourly writes only changed hour buckets.
- /api/metrics: agenthub_event_loop_delay_seconds{quantile}.
Hooks
- PreToolUse/PostToolUse run with "async": true (settings installer,
plugin.json, S3 managed settings): Claude Code no longer waits a Node start
before and after every tool call.
Committed with --no-verify at the owner's request: the pre-commit full server
suite fails on 9 tests that fail identically on 0c2f498 (lane-sync merge: no
git identity on this host; read-pool; resolveHost; codex-usage 1 ms flake).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
6.0 KiB
JavaScript
146 lines
6.0 KiB
JavaScript
/**
|
|
* @file tailwind.config.js
|
|
* @description Tailwind CSS configuration — content globs and the dashboard's
|
|
* dark/light color tokens. Token values live in `src/index.css` as CSS
|
|
* variables (`:root` = light, `.dark` = dark); this file only wires Tailwind
|
|
* class names to them. RGB-triplet vars (not hex) so `<alpha-value>` keeps
|
|
* opacity modifiers like `bg-surface-2/70` working under either theme.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
/**
|
|
* A status/accent FILL is pre-mixed against one fixed base, never translucent.
|
|
*
|
|
* `bg-status-success/10` used to emit `rgb(var(--status-success) / 0.1)`, which
|
|
* composites with whatever is behind it — so the one "success" chip rendered as
|
|
* five different colours over the five surface steps, and washed out badly on
|
|
* the light theme's near-white cards. Mixing against `--tint-base` (the card
|
|
* surface of the active theme) gives an OPAQUE colour: the same chip reads the
|
|
* same on the page, in a card, in a modal and inside an input row.
|
|
*
|
|
* Applied to `backgroundColor`/`borderColor` only. `text-status-warning/80` and
|
|
* `ring-accent/40` stay genuinely translucent — text and rings sit on top of a
|
|
* fill rather than replacing it — and the surface family keeps its alpha too,
|
|
* or every modal scrim would turn solid.
|
|
*/
|
|
const tint =
|
|
(variable) =>
|
|
({ opacityValue }) => {
|
|
const alpha = Number(opacityValue);
|
|
// No modifier at all: Tailwind passes `var(--tw-bg-opacity, 1)` here.
|
|
if (!Number.isFinite(alpha) || alpha >= 1) return `rgb(var(${variable}))`;
|
|
return `color-mix(in srgb, rgb(var(${variable})) ${alpha * 100}%, rgb(var(--tint-base)))`;
|
|
};
|
|
|
|
const tinted = {
|
|
accent: {
|
|
DEFAULT: tint("--accent"),
|
|
hover: tint("--accent-hover"),
|
|
muted: "color-mix(in srgb, rgb(var(--accent)) 15%, rgb(var(--tint-base)))",
|
|
},
|
|
// The fill behind white text (`.btn-primary`, Switch on, Checkbox tick):
|
|
// `--accent` is a TEXT colour, too light in dark themes to carry white.
|
|
"accent-solid": {
|
|
DEFAULT: tint("--accent-solid"),
|
|
hover: tint("--accent-solid-hover"),
|
|
},
|
|
status: {
|
|
success: tint("--status-success"),
|
|
danger: tint("--status-danger"),
|
|
warning: tint("--status-warning"),
|
|
},
|
|
diff: { mod: tint("--diff-mod") },
|
|
kept: tint("--kept"),
|
|
};
|
|
|
|
/** @type {import('tailwindcss').Config} */
|
|
export default {
|
|
darkMode: "class",
|
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
surface: {
|
|
0: "rgb(var(--surface-0) / <alpha-value>)",
|
|
1: "rgb(var(--surface-1) / <alpha-value>)",
|
|
2: "rgb(var(--surface-2) / <alpha-value>)",
|
|
3: "rgb(var(--surface-3) / <alpha-value>)",
|
|
4: "rgb(var(--surface-4) / <alpha-value>)",
|
|
5: "rgb(var(--surface-5) / <alpha-value>)",
|
|
},
|
|
border: {
|
|
DEFAULT: "rgb(var(--border) / <alpha-value>)",
|
|
light: "rgb(var(--border-light) / <alpha-value>)",
|
|
},
|
|
accent: {
|
|
DEFAULT: "rgb(var(--accent) / <alpha-value>)",
|
|
hover: "rgb(var(--accent-hover) / <alpha-value>)",
|
|
muted: "rgb(var(--accent) / 0.15)",
|
|
},
|
|
"accent-solid": {
|
|
DEFAULT: "rgb(var(--accent-solid) / <alpha-value>)",
|
|
hover: "rgb(var(--accent-solid-hover) / <alpha-value>)",
|
|
},
|
|
fg: {
|
|
primary: "rgb(var(--fg-primary) / <alpha-value>)",
|
|
secondary: "rgb(var(--fg-secondary) / <alpha-value>)",
|
|
muted: "rgb(var(--fg-muted) / <alpha-value>)",
|
|
},
|
|
syntax: {
|
|
comment: "rgb(var(--syn-comment) / <alpha-value>)",
|
|
string: "rgb(var(--syn-string) / <alpha-value>)",
|
|
number: "rgb(var(--syn-number) / <alpha-value>)",
|
|
keyword: "rgb(var(--syn-keyword) / <alpha-value>)",
|
|
builtin: "rgb(var(--syn-builtin) / <alpha-value>)",
|
|
function: "rgb(var(--syn-function) / <alpha-value>)",
|
|
operator: "rgb(var(--syn-operator) / <alpha-value>)",
|
|
punctuation: "rgb(var(--syn-punctuation) / <alpha-value>)",
|
|
property: "rgb(var(--syn-property) / <alpha-value>)",
|
|
tag: "rgb(var(--syn-tag) / <alpha-value>)",
|
|
plain: "rgb(var(--syn-plain) / <alpha-value>)",
|
|
add: "rgb(var(--syn-add) / <alpha-value>)",
|
|
del: "rgb(var(--syn-del) / <alpha-value>)",
|
|
},
|
|
status: {
|
|
success: "rgb(var(--status-success) / <alpha-value>)",
|
|
danger: "rgb(var(--status-danger) / <alpha-value>)",
|
|
warning: "rgb(var(--status-warning) / <alpha-value>)",
|
|
},
|
|
diff: { mod: "rgb(var(--diff-mod) / <alpha-value>)" },
|
|
kept: "rgb(var(--kept) / <alpha-value>)",
|
|
},
|
|
backgroundColor: tinted,
|
|
borderColor: tinted,
|
|
// Text a step heavier than stock: regular (and `font-normal`) is 500.
|
|
fontWeight: { normal: "500" },
|
|
fontFamily: {
|
|
sans: ["Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "sans-serif"],
|
|
mono: ["JetBrains Mono", "Fira Code", "Consolas", "monospace"],
|
|
// Tailwind's stock stack minus Georgia: Windows picks Georgia, which has
|
|
// no precomposed Vietnamese glyphs, so Chrome splits "ế" into "ê" + a
|
|
// tone mark it cannot place and the chat reads "Chuyê ́n".
|
|
serif: ["ui-serif", "Cambria", "Times New Roman", "Times", "serif"],
|
|
},
|
|
animation: {
|
|
// Status indicators pulse a few times and rest (never `infinite`: an
|
|
// always-on animation keeps the browser drawing 60 frames a second).
|
|
"pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) 3",
|
|
ping: "ping 1s cubic-bezier(0, 0, 0.2, 1) 4",
|
|
"fade-in": "fadeIn 0.3s ease-out",
|
|
"slide-up": "slideUp 0.3s ease-out",
|
|
},
|
|
keyframes: {
|
|
fadeIn: {
|
|
"0%": { opacity: "0" },
|
|
"100%": { opacity: "1" },
|
|
},
|
|
slideUp: {
|
|
"0%": { opacity: "0", transform: "translateY(8px)" },
|
|
"100%": { opacity: "1", transform: "translateY(0)" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
};
|