feat: edit in the Changes panel, git graph history, plan quota (v2.3.0)

- Changes panel: a working file opens in a CodeMirror editor with its diff
  inline (DiffEditor.tsx, @codemirror/merge, lazy-loaded); Changes folds the
  unchanged lines, Whole file shows them; side by side at 50/50 and full
  width; a Revert per block; original from HEAD or the branch's merge base
  (`mergeBase` on GET /git/changes?range=branch)
- Every diff: green = line added whole, red = removed whole, blue = edited
  in place (--diff-mod)
- History drawn as a commit graph (#58: `parents` on GET /git/log,
  --date-order; lib/commitGraph.ts)
- Per-workspace never-stage list, drawn orange in the Files tree (--kept);
  closed Terminal rows past three deleted
- Status bar plan quota for Claude (per account) and Codex
  (GET /api/pricing/quota, server/lib/quota.js); compact hover panel that
  stays open, Refresh button with a spinner
- Settings → Appearance holds language, theme and pet; Ctrl+Shift+C copies;
  compact sidebar navigation and short session times
This commit is contained in:
2026-10-06 15:38:25 +07:00
parent f0500dc592
commit daf5b7be92
65 changed files with 2414 additions and 587 deletions
+8 -5
View File
@@ -337,23 +337,26 @@ export function formatMs(ms: number): string {
* Formats how long ago an ISO/SQLite timestamp was, as a translated relative
* string ("just now", "5m ago", "3h ago", "2d ago") using {@link i18n}.
* @param iso A past timestamp (see {@link parseDate}).
* @param short Drop the "ago" ("now", "5m") — the `common:time.*Short` keys.
* @returns A localized relative-time phrase; i18next handles pluralization via `count`.
* @remarks Thresholds cascade seconds -> minutes -> hours -> days (days is the largest
* bucket, so a 40-day-old event reads "40d ago"). Under a minute collapses to the
* "just now" key. Uses translated keys, not `Intl.RelativeTimeFormat`, so the exact
* wording is controlled by the `common:time.*` translation resources.
*/
export function timeAgo(iso: string): string {
export function timeAgo(iso: string, short = false): string {
const ms = Date.now() - parseDate(iso).getTime();
if (Number.isNaN(ms)) return "-";
// `short` drops the "ago" ("17m"), for a narrow column like the sidebar's.
const k = short ? "Short" : "";
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return i18n.t("common:time.justNow");
if (seconds < 60) return i18n.t(`common:time.justNow${k}`);
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return i18n.t("common:time.mAgo", { count: minutes });
if (minutes < 60) return i18n.t(`common:time.mAgo${k}`, { count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return i18n.t("common:time.hAgo", { count: hours });
if (hours < 24) return i18n.t(`common:time.hAgo${k}`, { count: hours });
const days = Math.floor(hours / 24);
return i18n.t("common:time.dAgo", { count: days });
return i18n.t(`common:time.dAgo${k}`, { count: days });
}
// ===========================================================================